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%
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