1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5import { readFile } from "node:fs/promises";
6import { basename } from "node:path";
7import { expandHomePath } from "../../util/path.js";
8import { applyConfigOverrides, loadConfig } from "../../config/loader.js";
9import { createWorkspace } from "../../workspace/manager.js";
10import { writeWorkspaceFile } from "../../workspace/content.js";
11import { createGrepTool } from "../../agent/tools/grep.js";
12import { createReadTool } from "../../agent/tools/read.js";
13import { createLsTool } from "../../agent/tools/ls.js";
14import { createFindTool } from "../../agent/tools/find.js";
15import { createWebFetchTool } from "../../agent/tools/web-fetch.js";
16import { createWebSearchTool } from "../../agent/tools/web-search.js";
17import { runAgent } from "../../agent/runner.js";
18import { buildWebPrompt } from "../../agent/prompts/web.js";
19import { createEventLogger, printUsageSummary } from "../output.js";
20import { ConfigError, FetchError } from "../../util/errors.js";
21
22const INJECT_THRESHOLD = 50 * 1024;
23
24export interface WebCommandOptions {
25 query: string;
26 url?: string;
27 model?: string;
28 verbose: boolean;
29 cleanup: boolean;
30}
31
32export async function runWebCommand(options: WebCommandOptions): Promise<void> {
33 const { config } = await loadConfig();
34 const overrides = applyConfigOverrides(config, {
35 defaults: { model: options.model ?? config.defaults.model, cleanup: options.cleanup },
36 web: { model: options.model ?? config.web.model },
37 });
38
39 const workspace = await createWorkspace({ cleanup: overrides.defaults.cleanup });
40
41 try {
42 const logger = createEventLogger({ verbose: options.verbose });
43
44 const kagiSession =
45 overrides.web.kagi_session_token ?? overrides.defaults.kagi_session_token ?? process.env["KAGI_SESSION_TOKEN"];
46 const tabstackKey =
47 overrides.web.tabstack_api_key ??
48 overrides.defaults.tabstack_api_key ??
49 process.env["TABSTACK_API_KEY"];
50
51 if (!kagiSession) {
52 throw new ConfigError("Web search requires KAGI_SESSION_TOKEN (set via environment or config)");
53 }
54 if (!tabstackKey) {
55 throw new ConfigError("Web fetch requires TABSTACK_API_KEY (set via environment or config)");
56 }
57
58 let systemPrompt = buildWebPrompt({
59 currentTime: new Date().toISOString(),
60 });
61 const promptPath = overrides.web.system_prompt_path;
62 if (promptPath) {
63 systemPrompt = await readFile(expandHomePath(promptPath), "utf8");
64 }
65
66 const tools = [
67 createWebSearchTool(kagiSession),
68 createWebFetchTool(tabstackKey),
69 createReadTool(workspace.path),
70 createGrepTool(workspace.path),
71 createLsTool(workspace.path),
72 createFindTool(workspace.path),
73 ];
74
75 let seededContext = "";
76 if (options.url) {
77 const fetchTool = createWebFetchTool(tabstackKey);
78 try {
79 const result = await fetchTool.execute("prefetch", { url: options.url, nocache: false });
80 const text = result.content
81 .map((block) => (block.type === "text" ? block.text ?? "" : ""))
82 .join("");
83 if (text.length <= INJECT_THRESHOLD) {
84 seededContext = text;
85 } else {
86 const filename = `web/${basename(new URL(options.url).pathname) || "index"}.md`;
87 await writeWorkspaceFile(workspace.path, filename, text);
88 seededContext = `Content from ${options.url} was too large to include directly. It has been saved to ${filename} in your workspace.`;
89 }
90 } catch (error: any) {
91 throw new FetchError(options.url, error?.message ?? String(error));
92 }
93 }
94
95 const query = seededContext
96 ? `${options.query}\n\n<attached_content url="${options.url}">\n${seededContext}\n</attached_content>`
97 : options.query;
98
99 const result = await runAgent(query, {
100 model: overrides.web.model ?? overrides.defaults.model,
101 systemPrompt,
102 tools,
103 onEvent: logger,
104 config: overrides,
105 });
106
107 process.stdout.write(result.message + "\n");
108 printUsageSummary(result.usage as any, result.requestCount);
109 } finally {
110 await workspace.cleanup();
111 }
112}