#!/usr/bin/env node /** * CLI for the second-brain foundation + ingest (SB-S0 / SB-S1). * * node --import tsx src/cli.ts init * node --import tsx src/cli.ts ingest --file [--source ] [--date ] * node --import tsx src/cli.ts ingest --scan-inbox [--source ] * node --import tsx src/cli.ts published list [--json] * * `init` scaffolds the brain/ + ingest/ tree (idempotent). `ingest` captures the * user's published posts into ingest/published/ tagged provenance=published — the * gold signal the voice/profile-learning surface learns from (never ai-draft). * * Exit code: 0 on success, 2 on usage error. */ import { readFileSync } from "node:fs"; import { dataRoot } from "./dataRoot.js"; import { ingestText, listPublished, scanInbox } from "./ingest.js"; import { initBrain } from "./scaffold.js"; /** Parse `--key value` / boolean `--flag` args (the specifics-bank CLI idiom). */ function parseFlags(args: string[]): Record { const out: Record = {}; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a.startsWith("--")) { const key = a.slice(2); const next = args[i + 1]; if (next === undefined || next.startsWith("--")) { out[key] = "true"; } else { out[key] = next; i++; } } } return out; } function today(): string { return new Date().toISOString().slice(0, 10); } function usage(msg: string): never { console.error(`error: ${msg}`); console.error( "usage:\n" + " init\n" + " ingest --file [--source ] [--date ]\n" + " ingest --scan-inbox [--source ]\n" + " published list [--json]", ); process.exit(2); } function runInit(): void { const { created, skipped } = initBrain(); console.log(`Brain root: ${dataRoot("brain")}`); if (created.length > 0) { console.log(`Created (${created.length}):`); for (const c of created) console.log(` + ${c}`); } if (skipped.length > 0) { console.log(`Skipped — already present (${skipped.length}):`); for (const s of skipped) console.log(` · ${s}`); } if (created.length === 0) console.log("Already initialised — nothing to do."); } function runIngest(flags: Record): void { if (flags["scan-inbox"] === "true") { const res = scanInbox({ captured_at: today(), source: flags.source }); console.log( `Scanned inbox → published: ${res.processed.length} new, ${res.skipped.length} already published`, ); return; } const file = flags.file; if (!file || file === "true") usage("ingest needs --file or --scan-inbox"); const body = readFileSync(file, "utf8"); const res = ingestText({ body, captured_at: today(), source: flags.source, published_date: flags.date === "true" ? undefined : flags.date, }); if (res.written && res.collision) { console.log(`Collision (different body, same id) → wrote ${res.path}`); } else if (res.written) { console.log(`Wrote ingest/published/${res.record.id}.md`); } else { console.log(`Duplicate — already published: ${res.record.id}`); } } function runPublished(rest: string[], flags: Record): void { if (rest[0] !== "list") usage("published needs: list"); const { records, skipped } = listPublished(); if (flags.json === "true") { console.log(JSON.stringify({ records, skipped }, null, 2)); return; } const tail = skipped > 0 ? ` (${skipped} unreadable skipped)` : ""; console.log(`Published gold corpus: ${records.length} record(s)${tail}`); for (const r of records) { console.log(` · ${r.id} · ${r.provenance} · ${r.published_date} · ${r.firstLine}`); } } function main(): void { const [command, ...rest] = process.argv.slice(2); const flags = parseFlags(rest); if (command === "init") return runInit(); if (command === "ingest") return runIngest(flags); if (command === "published") return runPublished(rest, flags); usage(command ? `unknown command: ${command}` : "no command given"); } main();