web.ts

  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}