1import { readFile } from "node:fs/promises";
2import { basename } from "node:path";
3import { applyConfigOverrides, loadConfig } from "../../config/loader.js";
4import { createWorkspace } from "../../workspace/manager.js";
5import { writeWorkspaceFile } from "../../workspace/content.js";
6import { createGrepTool } from "../../agent/tools/grep.js";
7import { createReadTool } from "../../agent/tools/read.js";
8import { createLsTool } from "../../agent/tools/ls.js";
9import { createFindTool } from "../../agent/tools/find.js";
10import { createWebFetchTool } from "../../agent/tools/web-fetch.js";
11import { createWebSearchTool } from "../../agent/tools/web-search.js";
12import { runAgent } from "../../agent/runner.js";
13import { WEB_SYSTEM_PROMPT } from "../../agent/prompts/web.js";
14import { createEventLogger, printUsageSummary } from "../output.js";
15import { FetchError, ToolInputError } from "../../util/errors.js";
16
17const INJECT_THRESHOLD = 50 * 1024;
18
19export interface WebCommandOptions {
20 query: string;
21 url?: string;
22 model?: string;
23 verbose: boolean;
24 cleanup: boolean;
25}
26
27export async function runWebCommand(options: WebCommandOptions): Promise<void> {
28 const { config } = await loadConfig();
29 const overrides = applyConfigOverrides(config, {
30 defaults: { model: options.model ?? config.defaults.model, cleanup: options.cleanup },
31 web: { model: options.model ?? config.web.model },
32 });
33
34 const workspace = await createWorkspace({ cleanup: overrides.defaults.cleanup });
35 const logger = createEventLogger({ verbose: options.verbose });
36
37 const kagiSession =
38 overrides.web.kagi_session_token ?? overrides.defaults.kagi_session_token ?? process.env["KAGI_SESSION_TOKEN"];
39 const tabstackKey =
40 overrides.web.tabstack_api_key ??
41 overrides.defaults.tabstack_api_key ??
42 process.env["TABSTACK_API_KEY"];
43
44 if (!kagiSession) {
45 throw new ToolInputError("Missing Kagi session token (set KAGI_SESSION_TOKEN or config)");
46 }
47 if (!tabstackKey) {
48 throw new ToolInputError("Missing Tabstack API key (set TABSTACK_API_KEY or config)");
49 }
50
51 let systemPrompt = WEB_SYSTEM_PROMPT;
52 const promptPath = overrides.web.system_prompt_path;
53 if (promptPath) {
54 const home = process.env["HOME"] ?? "";
55 systemPrompt = await readFile(promptPath.replace(/^~\//, `${home}/`), "utf8");
56 }
57
58 const tools = [
59 createWebSearchTool(kagiSession),
60 createWebFetchTool(tabstackKey),
61 createReadTool(workspace.path),
62 createGrepTool(workspace.path),
63 createLsTool(workspace.path),
64 createFindTool(workspace.path),
65 ];
66
67 let seededContext = "";
68 if (options.url) {
69 const fetchTool = createWebFetchTool(tabstackKey);
70 try {
71 const result = await fetchTool.execute("prefetch", { url: options.url, nocache: false });
72 const text = result.content
73 .map((block) => (block.type === "text" ? block.text ?? "" : ""))
74 .join("");
75 if (text.length <= INJECT_THRESHOLD) {
76 seededContext = text;
77 } else {
78 const filename = `web/${basename(new URL(options.url).pathname) || "index"}.md`;
79 await writeWorkspaceFile(workspace.path, filename, text);
80 seededContext = `Fetched content stored at ${filename}`;
81 }
82 } catch (error: any) {
83 await workspace.cleanup();
84 throw new FetchError(options.url, error?.message ?? String(error));
85 }
86 }
87
88 const query = seededContext ? `${options.query}\n\n${seededContext}` : options.query;
89
90 try {
91 const result = await runAgent(query, {
92 model: overrides.web.model ?? overrides.defaults.model,
93 systemPrompt,
94 tools,
95 onEvent: logger,
96 config: overrides,
97 });
98
99 process.stdout.write(result.message + "\n");
100 printUsageSummary(result.usage as any);
101 } finally {
102 await workspace.cleanup();
103 }
104}