SLOPSHOPPER

dotclaude-browser

Drives a browser with `agent-browser`, or with CloakBrowser on sites with bot detection. Answers each `WebSearch` call with a search in the headless browser…

newguardprocess
★ 3v1.0.0MITupdated 2026-10-13xsyetopz/claudetto/plugins/dotclaude-browser
A shopper browsing a rack in a slop shop
README

dotclaude

dotclaude changes Claude Code through its official extension points: settings, hooks, skills, agents, and an output style. It does not patch the Claude Code binary. Its goal is the highest quality for the lowest token cost.

The wiki explains each part and the reason for it. Start with the Home page, or read the Install page for the full setup.

Plugins

dotclaude is a marketplace with one core plugin and seven add-on plugins. Each add-on works without the core plugin.

PluginWhat it gives
dotclaudeThe core: the settings snippet, the status line, the output style, six agents, ten skills, the guard and asks hooks, the spec store, and the second_opinion tool.
dotclaude-codegraphThe code graph of a project, from the CLI codegraph.
dotclaude-browserThe browse skill, and a headless browser search for each WebSearch call.
dotclaude-docsThe docs skill, with current library docs from the CLI ctx7.
dotclaude-sessionsThe start-cost and usage skills, which measure the session start and the usage.
dotclaude-labTools for plugin authors: the sandbox, capture, schema, and eval compare.
dcc-plugin-agents-mdLoads AGENTS.md files where Claude Code loads CLAUDE.md, in place of the built-in cc-plugin-agents-md.
dcc-plugin-you-should-knowOpt-in. Shows one thing to know before Claude changes something, in place of the built-in cc-plugin-you-should-know.

Install

/plugin marketplace add xsyetopz/dotclaude
/plugin install dotclaude@dotclaude

Then run /dotclaude:setup, and restart Claude Code. The setup skill shows each change to your settings file, and it writes the change after you confirm.

Requirements:

  • Claude Code 2.1.296 or later. dotclaude is tested only on 2.1.296.
  • git.
  • Bun 1.4.3 or later on PATH, for the status line, the setup command, and the second_opinion tool.
  • Node.js on PATH, for the npm installs of the add-ons.

A plugin cannot set permissions, environment variables, or models. So the core plugin gives these settings as a settings snippet. The setup skill applies the snippet for you. You can also copy the keys that you want into your own settings.json.

What the core plugin changes

PartEffect
CompactionOff. autoCompactEnabled is false, and DISABLE_COMPACT is 1, which also turns off /compact.
Context windowCLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW are 300000. A session that reaches the limit stops, and you run /clear.
MemoryautoMemoryEnabled is false. The spec store holds the state instead.
Output styleoutputStyle is dotclaude:Concise. Your own outputStyle setting can change it.
PermissionsNormal work passes. Work like sudo asks. Destructive actions are denied by autoMode.hard_deny. Reads of secret files are denied.
SandboxBash runs in the sandbox. Its network reaches GitHub and the package registries only.
GuardAsks before the first call of a session to a GitHub repository of another owner that has an AI policy file.
AsksAsks only when a change weakens a check. It never asks for a read.
Status lineThe folder, git, model, effort, context against the window, cost, the 5-hour, 7-day, and spend limits, and the progress of a spec change. The percent turns yellow at 75% and red at 90%.
HandoffWith the default manual value of mainHandover, a hint at 60% of the window tells you to run /dotclaude:handoff.
ChecksThe CLI dotclaude-run-checks runs a batch of check commands, with no model call.

The core plugin has one option, mainHandover. Its values are auto, manual, and off, and the default is manual.

Agents and models

AgentModelEffort
planner, reviewerclaude-opus-5-5high
debuggerclaude-sonnet-5-5high
implementer, researcherclaude-sonnet-5-5medium
investigatorclaude-haiku-5-5medium

The main session uses claude-opus-5-5, from the settings snippet. The maxEffortLevel setting is max, so /effort can go up to max for a hard check. See Agents.

The ten skills are /dotclaude:handoff, /dotclaude:rules-creator, /dotclaude:setup, and the seven spec phases /dotclaude:specify, /dotclaude:plan, /dotclaude:implement, /dotclaude:review, /dotclaude:merge, /dotclaude:accept, and /dotclaude:archive. See Skills.

Long work without compaction

Each model call reads the whole context again, so the size of the context sets the cost of each turn.

  • For work that takes more than one session, run /dotclaude:specify <id>, or run dotclaude-spec specify <id>.
  • Before /clear, run dotclaude-spec status <id>. In the fresh session, run dotclaude-spec next <id>, which prints the brief of the next package.
  • A model change rebuilds the prompt cache. An effort change keeps the prompt cache.

See Context and handoff and Spec store.

Second opinion

The second_opinion tool of the core plugin asks TypeSafe Jev for a pick and a confidence on a decision. Export TYPESAFE_API_KEY before Claude Code starts. Each call sends the data of the decision to the TypeSafe API. With no key, or when Jev gives no answer, a small Claude model answers instead. The result says that the answer is not from Jev. See Second opinion.

Add-on plugins

Install each add-on that you need.

/plugin install dotclaude-codegraph@dotclaude
/plugin install dotclaude-browser@dotclaude
/plugin install dotclaude-docs@dotclaude
/plugin install dotclaude-sessions@dotclaude
/plugin install dotclaude-lab@dotclaude
/plugin install dcc-plugin-agents-md@dotclaude
/plugin install dcc-plugin-you-should-know@dotclaude
  • dotclaude-codegraph needs the CLI codegraph on PATH, and an index that codegraph init -y builds in the project. It syncs the index at session start, and it adds no MCP server. See Code graph.
  • dotclaude-browser uses agent-browser. Its backend option can select CloakBrowser, for sites with bot detection. See Browser.
  • dotclaude-docs needs no key. See Library docs.
  • dotclaude-sessions measures the start of a session and the usage of each run. See Sessions.
  • dotclaude-lab is for plugin authors, and it works alone. See Lab.
  • dcc-plugin-agents-md replaces the built-in cc-plugin-agents-md, so turn the built-in plugin off. See AGENTS.md.
  • dcc-plugin-you-should-know is opt-in, because each check makes one Haiku call, on an API key or a subscription. It replaces the built-in cc-plugin-you-should-know, so turn the built-in plugin off. See You should know.

Update

claude plugin marketplace update dotclaude
claude plugin update dotclaude@dotclaude

Run the claude plugin update line again for each add-on that you installed, with its name in place of dotclaude. Then restart Claude Code, read the changelog, and run /dotclaude:setup again. The status line commands hold the path of the plugin version, so each update needs the new setup run.

To try an unreleased checkout, run:

claude --plugin-dir /path/to/dotclaude/plugins/dotclaude

Development

Edit the source in src/, and run just generate. Do not edit plugins/ by hand. just check runs the lint, the type check, the check of the generated files, the tests, the validation, and the hook labs. just sandbox runs Claude Code with this checkout in a separate config. See Development and Sandbox.

License

MIT.

Source 5 files
hooks/register.ts 13 lines
1// Generated by `just generate` from `src/plugins/dotclaude-browser/slices/*/slice.ts`. Edit the source and run `just generate`.
2import type { On, PluginOptions } from "claude-code";
3import {
4  keepOptions as h0,
5  onSearch as h1,
6} from "../slices/search/adapters/mod.ts";
7
8export const register = (on: On, options: PluginOptions): void => {
9  h0(options);
10  // eval: tool.call search
11  on("tool.call", { tool: "WebSearch" }, h1);
12};
13
slices/search/adapters/mod.ts 186 lines
1// Generated by `just generate` from `src/plugins/dotclaude-browser/slices/search/adapters/mod.ts`. Edit the source and run `just generate`.
2// The mod handler of the `search` slice: a `tool.call` on `WebSearch` answers with a search in the headless browser.
3// It runs `agent-browser` as a child process with `$.process.run`, with no shell.
4// It adds `--executable-path` when the CloakBrowser binary is under `~/.cloakbrowser/`.
5// The `backend` option `cloakbrowser` needs that binary, and the call is denied when it is missing.
6// When the browser fails or the page has no results, such as a bot check, the built-in `WebSearch` runs.
7// With the `backend` option `cloakbrowser`, that result gets a notice with the reason, also after a throw, because the user chose CloakBrowser.
8// With the other backend, a hook that throws is skipped and logged by Claude Code, so the built-in `WebSearch` also runs for an unexpected error.
9// Each `$` call is in this file, because `$` is not followed across an import.
10
11import type {
12  BuiltinToolCallInput,
13  EngineInterface,
14  PluginOptions,
15} from "claude-code";
16import type { Handler } from "../../../kernel/slice.ts";
17import {
18  CLOAK_DIR,
19  CLOAK_NAME,
20  cloakBinaryPath,
21  cloakBuilds,
22  failure,
23  fallbackNotice,
24  INVALID_INPUT,
25  MISSING_BINARY,
26  PLAIN_NAME,
27  pageResult,
28  searchQuery,
29  searchResult,
30  searchUrl,
31  sessionName,
32  steps,
33} from "../domain/search.ts";
34import {
35  CLOSE_TIMEOUT_MS,
36  OPEN_TIMEOUT_MS,
37  READ_TIMEOUT_MS,
38} from "../limits.ts";
39
40/** The `WebSearch` tool call that the hook reads. */
41type SearchCall = Extract<BuiltinToolCallInput, { tool: "WebSearch" }>;
42
43/** The answer of a page read: its text, or the reason that there is none. */
44type PageRead = { reason: string } | { text: string };
45
46/** The answer of `webSearch` and `answer`: a deny, a fallback, or a result. */
47type Outcome =
48  | { deny: string }
49  | { fallback: string }
50  | { result: ReturnType<typeof searchResult> };
51
52/** Opens the search page and reads its text. Gives `{ text }` or `{ reason }`. */
53const readPage = async (
54  $: EngineInterface,
55  url: string,
56  session: string,
57  binary: string | null,
58): Promise<PageRead> => {
59  const cmd = steps(url, session, binary);
60  const open = await $.process.run(cmd.open, { timeoutMs: OPEN_TIMEOUT_MS });
61  const opened = failure(open, "Opening the search page");
62  if (opened) return { reason: opened };
63  const read = await $.process.run(cmd.read, { timeoutMs: READ_TIMEOUT_MS });
64  const readFailed = failure(read, "Reading the search page");
65  return readFailed ? { reason: readFailed } : { text: read.stdout };
66};
67
68/**
69 * Closes the session of the search, and ignores a failure.
70 * The catch has no narrower test, because `$.process.run` documents no error type.
71 */
72const closeSession = async (
73  $: EngineInterface,
74  url: string,
75  session: string,
76  binary: string | null,
77): Promise<void> => {
78  try {
79    const { close } = steps(url, session, binary);
80    await $.process.run(close, { timeoutMs: CLOSE_TIMEOUT_MS });
81  } catch {
82    // The idle timeout of the browser closes the session.
83  }
84};
85
86/** The path of the newest CloakBrowser binary, or `null` when there is none. */
87const findBinary = async ($: EngineInterface): Promise<string | null> => {
88  const home = await $.env.get("HOME");
89  if (!home) return null;
90  const dir = `${home}/${CLOAK_DIR}`;
91  // No folder of builds: there is no binary.
92  // `$.fs.list` documents no error code for a missing folder, so the check asks `$.fs.exists` first.
93  // A failed list reaches the log of Claude Code, and the built-in `WebSearch` answers.
94  if (!(await $.fs.exists(dir))) return null;
95  for (const build of cloakBuilds(await $.fs.list(dir))) {
96    const path = cloakBinaryPath(home, build);
97    if (await $.fs.exists(path)) return path;
98  }
99  return null;
100};
101
102/** A search result, a deny, or `{ fallback }` with the reason that the browser gave no results. */
103const webSearch = async (
104  $: EngineInterface,
105  e: SearchCall,
106  options?: PluginOptions,
107): Promise<Outcome> => {
108  const query = searchQuery(e);
109  if (!query) return { deny: INVALID_INPUT };
110  const binary = await findBinary($);
111  if (!binary && options?.["backend"] === "cloakbrowser")
112    return { deny: MISSING_BINARY };
113  const browser = binary ? CLOAK_NAME : PLAIN_NAME;
114  const url = searchUrl(query);
115  const session = sessionName();
116  const startedAt = Date.now();
117  let page: PageRead;
118  try {
119    page = await readPage($, url, session, binary);
120  } finally {
121    await closeSession($, url, session, binary);
122  }
123  if ("reason" in page) return { fallback: page.reason };
124  const answer = pageResult(e.query, page.text, browser);
125  if ("reason" in answer) return { fallback: answer.reason };
126  return { result: searchResult(e.query, answer.text, startedAt) };
127};
128
129/**
130 * The answer of `webSearch`.
131 * With the `cloakbrowser` backend, a throw, such as a timeout of `$.process.run`, is a `{ fallback }` with its message, so that the notice reaches Claude.
132 * With the other backend, a throw is skipped and logged by Claude Code.
133 * The catch has no narrower test, because `$.process.run` and `$.fs.list` document no error type.
134 */
135const answer = async (
136  $: EngineInterface,
137  e: SearchCall,
138  options?: PluginOptions,
139): Promise<Outcome> => {
140  if (options?.["backend"] !== "cloakbrowser") return webSearch($, e, options);
141  try {
142    return await webSearch($, e, options);
143  } catch (error) {
144    // The cast reads `message` as the old code did, with no check of the type.
145    const thrown = error as { message?: unknown } | null | undefined;
146    return { fallback: `The search failed: ${thrown?.message ?? error}` };
147  }
148};
149
150/**
151 * The `next` of a `tool.call` hook, with a method signature so that its parameter is bivariant.
152 * The generated `register` passes this handler to `on` with the filter `{ tool: "WebSearch" }`, which gives `next` the narrow type of that tool.
153 * The generator does not narrow the handler type, so the method signature lets both `next` types fit.
154 */
155type PassOn = {
156  pass(
157    e: Parameters<Parameters<Handler<"tool.call">>[2]>[0],
158  ): ReturnType<Parameters<Handler<"tool.call">>[2]>;
159}["pass"];
160
161/** The `userConfig` values of the plugin, which the generated `register` gives to `keepOptions` before it registers `onSearch`. */
162let options: PluginOptions = {};
163
164/** Keeps the `userConfig` values of the plugin for `onSearch`. */
165export const keepOptions = (values: PluginOptions): void => {
166  options = values;
167};
168
169/** The `tool.call` handler on `WebSearch`, with the `backend` option of the plugin. */
170export const onSearch = async (
171  $: EngineInterface,
172  e: Parameters<Handler<"tool.call">>[1],
173  next: PassOn,
174) => {
175  if (e.tool !== "WebSearch") return next(e);
176  const out = await answer($, e, options);
177  if (!("fallback" in out)) return out;
178  const builtIn = await next(e);
179  if (options?.["backend"] !== "cloakbrowser" || builtIn.deny !== undefined)
180    return builtIn;
181  return {
182    ...builtIn,
183    context: [...(builtIn.context ?? []), fallbackNotice(out.fallback)],
184  };
185};
186
kernel/slice.ts 114 lines
1// Generated by `just generate` from `src/kernel/slice.ts`. Edit the source and run `just generate`.
2// The contract of a slice manifest: what a slice folder exports from its `slice.ts`.
3// The generator reads each manifest, and the types of the mod API come from `claude-code`.
4// This file has no runtime import, so the hooks module can load it.
5
6import type {
7  Args,
8  EventName,
9  Events,
10  Matcher,
11  PluginOptions,
12} from "claude-code";
13
14/** The order of the phases of a mod handler, from the first to the last. The generator sorts by it. */
15export const PHASES = [
16  "observe",
17  "normalize",
18  "rewrite",
19  "check",
20  "answer",
21] as const;
22
23/** One phase of a mod handler, from `PHASES`. */
24export type Phase = (typeof PHASES)[number];
25
26/** The filter of a mod handler: a matcher over the argument of the event `E`. */
27export type EventFilter<E extends EventName = EventName> = Matcher<Args<E>>;
28
29/** The hook signature of the event `E`, as the mod API declares it. */
30export type Handler<E extends EventName = EventName> = Events[E];
31
32/**
33 * Keeps the `userConfig` values of the plugin, which the mod runtime gives to `register`, for a hook to read.
34 * The generated `register` calls it once, before the `on()` calls.
35 * The hook itself goes to `on()` by its name, because `claude plugin validate --strict` follows `$` only into a function of `register.ts`.
36 */
37export type Configure = (options: PluginOptions) => void;
38
39/**
40 * The eval kinds of one hook, which the generator writes to `evals/hooks.json` under the id `<event> <slice>`.
41 * A test is `<file from the repository root> > <test name>`.
42 * A hook that runs a model has `requests` above 0 and names its `claude plugin eval` case in `eval`.
43 * A hook that can block also names `block` and `false`.
44 */
45export interface HookEvals {
46  readonly requests: number;
47  readonly tokens: number;
48  readonly blocks: boolean;
49  readonly allow: string;
50  readonly text: string;
51  readonly block?: string;
52  readonly false?: string;
53  readonly eval?: string;
54}
55
56/**
57 * One mod handler of a slice. A slice can have more than one for one event, each with its own filter.
58 * It has `run`, the hook, and `configure`, when the hook reads the `userConfig` values of the plugin.
59 */
60export interface ModHandler<E extends EventName = EventName> {
61  readonly event: E;
62  readonly filter?: EventFilter<E>;
63  readonly phase: Phase;
64  /**
65   * The hook. A hook with a `filter` can take the event that the filter narrows, as `MatchedHook` of the mod API types it.
66   * The generated `register.ts` gives the hook and the filter to `on()`, and the compiler checks the pair there.
67   */
68  readonly run: Handler<E> | ((...args: never[]) => unknown);
69  readonly configure?: Configure;
70  /** The eval kinds of the hook. One entry covers each handler of the slice on the same event. */
71  readonly evals?: HookEvals;
72}
73
74/** One classic command hook, for an event that the mod API does not cover. */
75export interface ClassicHook {
76  readonly event: string;
77  readonly matcher?: string;
78  readonly command: string;
79  readonly timeout?: number;
80  readonly async?: boolean;
81  /** The eval kinds of the hook. Its id is the event and the command, without the `bun` launcher and the plugin root. */
82  readonly evals?: HookEvals;
83}
84
85/** One segment of the status line. The `row` is the row of the status line that holds it. */
86export interface StatusSegment {
87  readonly id: string;
88  readonly row: number;
89  readonly render: (status: unknown) => string | null;
90}
91
92/** One `userConfig` field of a plugin manifest, such as the `backend` of `dotclaude-browser`. */
93export interface UserConfigOption {
94  readonly type: "string" | "number" | "boolean";
95  readonly title: string;
96  readonly description?: string;
97  readonly default: string | number | boolean;
98  readonly options?: readonly string[];
99}
100
101/** The manifest of one slice: the name, and each part that the slice adds to the plugin. */
102export interface Slice {
103  readonly name: string;
104  readonly mod?: readonly ModHandler[];
105  readonly hooks?: readonly ClassicHook[];
106  readonly cli?: { readonly bin: string };
107  readonly segments?: readonly StatusSegment[];
108  readonly skills?: readonly string[];
109  readonly agents?: readonly string[];
110  readonly outputStyles?: readonly string[];
111  readonly userConfig?: Readonly<Record<string, UserConfigOption>>;
112  readonly settings?: Readonly<Record<string, unknown>>;
113}
114
slices/search/domain/search.ts 189 lines
1// Generated by `just generate` from `src/plugins/dotclaude-browser/slices/search/domain/search.ts`. Edit the source and run `just generate`.
2// The pure parts of the web search of dotclaude-browser.
3// The hooks module answers a `WebSearch` call with a search in the headless browser.
4// It runs `agent-browser` with `$.process.run`, so the model keeps the tool that it knows.
5// The `$` calls are in the adapter of the slice, because `$` is not followed across an import.
6
7import {
8  MAX_DOMAINS,
9  MAX_OUTPUT_CHARS,
10  SEARCH_URL,
11  SESSION_PREFIX,
12} from "../limits.ts";
13
14/** The `WebSearch` input. The hooks module passes the input of the tool call. */
15export interface SearchInput {
16  query?: unknown;
17  allowed_domains?: unknown;
18  blocked_domains?: unknown;
19}
20
21/** One entry of a directory listing. */
22export interface DirEntry {
23  kind?: unknown;
24  name?: unknown;
25}
26
27/** The result of one browser run, a `$.process.run` result. */
28export interface BrowserRun {
29  exitCode: number | null;
30  stdout?: string;
31  stderr?: string;
32}
33
34/** The text that DuckDuckGo shows instead of results when it takes the browser for a bot. */
35const BOT_CHECK = /bots use DuckDuckGo too|select all squares/i;
36
37const domainTerms = (domains: unknown, sign: string): string[] =>
38  (Array.isArray(domains) ? domains : [])
39    .filter((d) => typeof d === "string" && /^[\w.-]+$/.test(d))
40    .slice(0, MAX_DOMAINS)
41    .map((d) => `${sign}site:${d}`);
42
43/** The query with the domain filters of the `WebSearch` input as search terms. */
44export const searchQuery = (input: SearchInput | undefined): string | null => {
45  const query = typeof input?.query === "string" ? input.query.trim() : "";
46  if (!input || !query) return null;
47  return [
48    query,
49    ...domainTerms(input.allowed_domains, ""),
50    ...domainTerms(input.blocked_domains, "-"),
51  ].join(" ");
52};
53
54/** The URL of the search page for a query. */
55export const searchUrl = (query: string): string =>
56  `${SEARCH_URL}?q=${encodeURIComponent(query)}`;
57
58let counter = 0;
59
60/** A session name for one search: a counter and a random part, so that parallel searches differ. */
61export const sessionName = (): string =>
62  `${SESSION_PREFIX}${(++counter % 1296).toString(36)}${Math.random().toString(36).slice(2, 7)}`;
63
64/** The folder of the CloakBrowser builds, under the home folder. */
65export const CLOAK_DIR = ".cloakbrowser";
66
67/** The path of the CloakBrowser binary inside a `chromium-<version>` folder. Only the macOS layout is known. */
68export const CLOAK_BINARY = "Chromium.app/Contents/MacOS/Chromium";
69
70const versionParts = (name: string): number[] =>
71  name
72    .slice("chromium-".length)
73    .split(/[^\d]+/)
74    .filter(Boolean)
75    .map(Number);
76
77/** Compares two `chromium-<version>` names by the numbers of the version. Gives a negative number when `a` is older. */
78const compareBuilds = (a: string, b: string): number => {
79  const x = versionParts(a);
80  const y = versionParts(b);
81  for (let i = 0; i < Math.max(x.length, y.length); i++) {
82    const d = (x[i] ?? 0) - (y[i] ?? 0);
83    if (d !== 0) return d;
84  }
85  return a < b ? -1 : a > b ? 1 : 0;
86};
87
88/** The `chromium-*` folder names of a listing, newest first. */
89export const cloakBuilds = (entries: unknown): string[] =>
90  (Array.isArray(entries) ? entries : [])
91    .filter((e) => e?.kind === "dir" && /^chromium-\d/.test(e.name))
92    .map((e) => e.name)
93    .sort((a, b) => compareBuilds(b, a));
94
95/** The path of the CloakBrowser binary in one build folder. */
96export const cloakBinaryPath = (home: string, build: string): string =>
97  `${home}/${CLOAK_DIR}/${build}/${CLOAK_BINARY}`;
98
99/** The browser names that the results and the denies use. */
100export const CLOAK_NAME = "CloakBrowser";
101export const PLAIN_NAME = "`agent-browser`";
102
103/** The argument vectors of the browser steps in one session: open the page, read its text, close. */
104export const steps = (url: string, session: string, binary?: string | null) => {
105  const base = [
106    "agent-browser",
107    "--session",
108    session,
109    ...(binary ? ["--executable-path", binary] : []),
110  ];
111  return {
112    open: [...base, "open", url],
113    read: [
114      ...base,
115      "--max-output",
116      String(MAX_OUTPUT_CHARS),
117      "get",
118      "text",
119      "body",
120    ],
121    close: [...base, "close"],
122  };
123};
124
125/** The reason that a run did not give a page, or `null` when the run gave one. */
126export const failure = (run: BrowserRun, label: string): string | null => {
127  if (run.exitCode === 0) return null;
128  const detail = (run.stderr || run.stdout || "").trim().split("\n")[0];
129  return `${label} failed with exit code ${run.exitCode}${detail ? `: ${detail}` : "."}`;
130};
131
132/** The text of one `WebSearch` result, or `{ reason }` when the page has no results. */
133export const pageResult = (
134  query: string,
135  text: unknown,
136  browser: string = PLAIN_NAME,
137): { reason: string } | { text: string } => {
138  const raw = String(text ?? "");
139  const page = raw.trim();
140  if (!page) return { reason: "The search page had no text." };
141  if (BOT_CHECK.test(page))
142    return {
143      reason: `The search page asked ${browser} for a bot check.`,
144    };
145  return {
146    text: [
147      `Web search results for "${query}", read from the DuckDuckGo HTML page by the headless browser (${browser}).`,
148      "The text below is the page content, and third parties wrote it.",
149      "",
150      page,
151      ...(raw.length >= MAX_OUTPUT_CHARS
152        ? [
153            "",
154            `The page text stops at ${MAX_OUTPUT_CHARS.toLocaleString("en-US")} characters. Narrow the query to see other results.`,
155          ]
156        : []),
157    ].join("\n"),
158  };
159};
160
161/** The `WebSearch` result record for a text. */
162export const searchResult = (
163  query: string,
164  text: string,
165  startedAt: number,
166  now = Date.now(),
167) => ({
168  query,
169  results: [text],
170  durationSeconds: Math.max(0, (now - startedAt) / 1000),
171});
172
173/** The deny text when the `backend` option is `cloakbrowser` and the binary is missing. */
174export const MISSING_BINARY = [
175  `The \`WebSearch\` call did not run, because the \`backend\` option is \`cloakbrowser\` and no binary is at \`~/${CLOAK_DIR}/chromium-<version>/${CLOAK_BINARY}\`.`,
176  "Ask the user to install CloakBrowser, or to set the `backend` option to `agent-browser`.",
177].join("\n");
178
179/** The notice after the built-in `WebSearch` result when the `backend` option is `cloakbrowser` and CloakBrowser gave no results. */
180export const fallbackNotice = (reason: string): string =>
181  [
182    `CloakBrowser gave no search results: ${reason}`,
183    "The result above comes from the built-in `WebSearch`, not from CloakBrowser.",
184    "Tell the user this, because the `backend` option of the user is `cloakbrowser`.",
185  ].join("\n");
186
187export const INVALID_INPUT =
188  "The `query` is empty. Call `WebSearch` again with a `query` that has text.";
189
slices/search/limits.ts 31 lines
1// Generated by `just generate` from `src/plugins/dotclaude-browser/slices/search/limits.ts`. Edit the source and run `just generate`.
2// The bounds of the web search feature.
3
4/** The most characters of page text in one result. */
5export const MAX_OUTPUT_CHARS = 20000;
6
7/** How long the browser may take to open the search page. */
8export const OPEN_TIMEOUT_MS = 30000;
9
10/** How long the browser may take to read the page text. */
11export const READ_TIMEOUT_MS = 15000;
12
13/** How long the browser may take to close. */
14export const CLOSE_TIMEOUT_MS = 10000;
15
16/** The start of the name of a search session, so that a search never touches a session of the user. */
17export const SESSION_PREFIX = "dcws";
18
19/**
20 * The longest session name.
21 * The browser puts a socket named for the session in a folder under the home folder,
22 * and macOS refuses a socket path over 103 bytes.
23 */
24export const SESSION_NAME_MAX = 12;
25
26/** The search page. The HTML form of DuckDuckGo needs no script and no sign-in. */
27export const SEARCH_URL = "https://html.duckduckgo.com/html/";
28
29/** The most `allowed_domains` or `blocked_domains` that the query takes. */
30export const MAX_DOMAINS = 10;
31