SPB Git

spb/search-box Public

Agentic web research engine — hypotheses, verbatim evidence, contradictions, sourced answers streamed live. Claude Opus 5 + Firecrawl + PostgreSQL.

TypeScript 76.9% CSS 18.7% SQL 2.1% JavaScript 1.8% Shell 0.5%
3.5 KB · 123 lines typescript
Raw Blame History
1/**2 * Search-box.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: packages/agent/src/cli.ts6 * Description: CLI test harness — runs one research session and prints live events.7 */89import { readFileSync } from "node:fs";10import { dirname, join } from "node:path";11import { fileURLToPath } from "node:url";12import { migrate, events } from "@search-box/db";13import { createSession, runSession } from "./run.js";1415// Minimal .env loader (repo root), no dependency.16function loadEnv(): void {17  const here = dirname(fileURLToPath(import.meta.url));18  const envPath = join(here, "..", "..", "..", ".env");19  try {20    for (const line of readFileSync(envPath, "utf8").split("\n")) {21      const m = line.match(/^([A-Z0-9_]+)=(.*)$/);22      if (m && m[1] && process.env[m[1]] === undefined) process.env[m[1]] = m[2] ?? "";23    }24  } catch {25    // .env optional if vars already exported26  }27}2829async function main(): Promise<void> {30  loadEnv();31  const question = process.argv.slice(2).join(" ").trim();32  if (!question) {33    console.error('usage: pnpm research "your research question"');34    process.exit(1);35  }3637  await migrate();38  const sessionId = await createSession(question);39  console.log(`session: ${sessionId}\n`);4041  // Tail events live while the session runs.42  let lastSeq = 0;43  let done = false;44  const tail = (async () => {45    while (!done) {46      const batch = await events.listAfter(sessionId, lastSeq);47      for (const ev of batch) {48        lastSeq = ev.seq;49        printEvent(ev.payload);50      }51      await new Promise((r) => setTimeout(r, 400));52    }53    const batch = await events.listAfter(sessionId, lastSeq);54    for (const ev of batch) printEvent(ev.payload);55  })();5657  try {58    await runSession(sessionId);59  } finally {60    done = true;61    await tail;62  }63  process.exit(0);64}6566function printEvent(p: { type: string } & Record<string, unknown>): void {67  switch (p.type) {68    case "plan.updated":69      console.log(`\n◆ PLAN: ${(p.objectives as string[]).join(" | ")}`);70      break;71    case "thought":72      console.log(`\n… ${p.publicReason}`);73      break;74    case "action.started": {75      const kind = p.kind as string;76      console.log(`→ ${kind}: ${p.label}`);77      break;78    }79    case "action.completed":80      console.log(`  ${p.ok ? "✓" : "✗"} ${p.summary} (${p.latencyMs}ms)`);81      break;82    case "claim.added": {83      const claim = p.claim as { id: string; text: string };84      console.log(`\n★ CLAIM ${claim.id}: ${claim.text}`);85      break;86    }87    case "claim.updated": {88      const claim = p.claim as { id: string; status: string; confidence: number };89      console.log(`  ↺ ${claim.id} → ${claim.status} (${Math.round(claim.confidence * 100)}%)`);90      break;91    }92    case "evidence.added": {93      const ev = p.evidence as { stance: string; quote: string };94      console.log(`  ❝ [${ev.stance}] ${ev.quote.slice(0, 120)}…`);95      break;96    }97    case "contradiction.added": {98      const c = p.contradiction as { description: string };99      console.log(`\n⚡ CONTRADICTION: ${c.description}`);100      break;101    }102    case "answer.delta":103      process.stdout.write(p.delta as string);104      break;105    case "synthesis.started":106      console.log(`\n\n========== ANSWER ==========\n`);107      break;108    case "session.completed":109      console.log(`\n\n========== DONE ==========`);110      break;111    case "session.failed":112      console.log(`\nFAILED: ${p.error}`);113      break;114    default:115      break;116  }117}118119main().catch((err) => {120  console.error(err);121  process.exit(1);122});123