web.ts

  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}