linkedin-studio/scripts/brain/src/cli.ts
Kjell Tore Guttormsen b65eb25328 feat(linkedin-studio): SB-S1 brain CLI — ingest + published list (init preserved) [skip-docs]
Add flag-routing infra (parseFlags) + two subcommands to the brain CLI, keeping
the existing `init` branch intact:
- `ingest --file <path> [--source] [--date]` / `ingest --scan-inbox` → capture
  published posts into ingest/published/ (Wrote / Duplicate / Collision report).
- `published list [--json]` → inspect the gold corpus (id · provenance · date · first line).
6 subprocess tests incl. an explicit `init` regression guard. brain 57→63 tests, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 14:56:45 +02:00

123 lines
4 KiB
JavaScript

#!/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 <path> [--source <s>] [--date <YYYY-MM-DD>]
* node --import tsx src/cli.ts ingest --scan-inbox [--source <s>]
* 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<string, string> {
const out: Record<string, string> = {};
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 <path> [--source <s>] [--date <YYYY-MM-DD>]\n" +
" ingest --scan-inbox [--source <s>]\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<string, string>): 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 <path> 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<string, string>): 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();