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…

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.
dotclaude is a marketplace with one core plugin and seven add-on plugins. Each add-on works without the core plugin.
| Plugin | What it gives |
|---|---|
dotclaude | The 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-codegraph | The code graph of a project, from the CLI codegraph. |
dotclaude-browser | The browse skill, and a headless browser search for each WebSearch call. |
dotclaude-docs | The docs skill, with current library docs from the CLI ctx7. |
dotclaude-sessions | The start-cost and usage skills, which measure the session start and the usage. |
dotclaude-lab | Tools for plugin authors: the sandbox, capture, schema, and eval compare. |
dcc-plugin-agents-md | Loads AGENTS.md files where Claude Code loads CLAUDE.md, in place of the built-in cc-plugin-agents-md. |
dcc-plugin-you-should-know | Opt-in. Shows one thing to know before Claude changes something, in place of the built-in cc-plugin-you-should-know. |
/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:
2.1.296 or later. dotclaude is tested only on 2.1.296.git.1.4.3 or later on PATH, for the status line, the setup command, and the second_opinion tool.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.
| Part | Effect |
|---|---|
| Compaction | Off. autoCompactEnabled is false, and DISABLE_COMPACT is 1, which also turns off /compact. |
| Context window | CLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW are 300000. A session that reaches the limit stops, and you run /clear. |
| Memory | autoMemoryEnabled is false. The spec store holds the state instead. |
| Output style | outputStyle is dotclaude:Concise. Your own outputStyle setting can change it. |
| Permissions | Normal work passes. Work like sudo asks. Destructive actions are denied by autoMode.hard_deny. Reads of secret files are denied. |
| Sandbox | Bash runs in the sandbox. Its network reaches GitHub and the package registries only. |
| Guard | Asks before the first call of a session to a GitHub repository of another owner that has an AI policy file. |
| Asks | Asks only when a change weakens a check. It never asks for a read. |
| Status line | The 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%. |
| Handoff | With the default manual value of mainHandover, a hint at 60% of the window tells you to run /dotclaude:handoff. |
| Checks | The 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.
| Agent | Model | Effort |
|---|---|---|
planner, reviewer | claude-opus-5-5 | high |
debugger | claude-sonnet-5-5 | high |
implementer, researcher | claude-sonnet-5-5 | medium |
investigator | claude-haiku-5-5 | medium |
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.
Each model call reads the whole context again, so the size of the context sets the cost of each turn.
/dotclaude:specify <id>, or run dotclaude-spec specify <id>./clear, run dotclaude-spec status <id>. In the fresh session, run dotclaude-spec next <id>, which prints the brief of the next package.See Context and handoff and Spec store.
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.
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.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
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.
MIT.
hooks/register.ts 13 lines1// 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};
13slices/search/adapters/mod.ts 186 lines1// 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};
186kernel/slice.ts 114 lines1// 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}
114slices/search/domain/search.ts 189 lines1// 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.";
189slices/search/limits.ts 31 lines1// 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