/** * pal cli knowledge — query and manage the knowledge store. * * Thin presentation layer over src/tools/knowledge/{lib,graph}.ts. Owns * formatting + argv parsing only; all entity logic lives in the tools. * * Subcommands: * search Substring search across title/tags/body * graph [--hops N] BFS traversal from a slug (default 2 hops) * stats Counts, hubs, isolated nodes * hubs Top 10 most-connected entities * find Entities tagged with * show Print one entity (frontmatter + body) * add Create entity (interactive unless flags given) * --tags ai,research Comma-separated tags * --related slug:type Repeatable; type ∈ RELATION_TYPES * --quality 0-10 * --status seedling|budding|evergreen * --type Free-form sub-type (default by domain) * ls [domain] List entities, optionally by one domain */ import { readFileSync } from "node:fs"; import { parseArgs } from "node:util"; import * as clack from "@clack/prompts"; import { toPath } from "../hooks/lib/paths"; import { buildGraph, resolveSlug, stats, traverse } from "../tools/knowledge/graph"; import { type CompanyInput, ingestEntities, type PersonInput, } from "../tools/knowledge/ingest"; import { DOMAINS, type Domain, type Entity, getOrCreate, list, load, RELATION_TYPES, type Related, type RelationType, STATUSES, type Status, } from "../tools/knowledge/lib"; // ── Dispatcher ───────────────────────────────────────────────────── export async function runKnowledge(args: string[]): Promise { const [sub, ...rest] = args; switch (sub) { case "search": return cmdSearch(rest); case "graph": return cmdGraph(rest); case "stats": return cmdStats(); case "hubs": return cmdHubs(); case "find": return cmdFind(rest); case "show": return cmdShow(rest); case "add": return cmdAdd(rest); case "ls": return cmdLs(rest); case "ingest": return cmdIngest(rest); case undefined: case "help": case "--help": case "-h": showHelp(); return 0; default: console.error(`Unknown subcommand: ${sub}\n`); showHelp(); return 1; } } // ── Helpers ──────────────────────────────────────────────────────── function showHelp(): void { console.log(` Usage: pal cli knowledge [args] Subcommands: search Substring search across title, tags, body graph [--hops N] BFS traversal from a slug (default 2 hops) stats Counts, hubs, isolated nodes hubs Top 10 most-connected entities find Entities tagged with show Print one entity (frontmatter + body) add Create entity (interactive unless flags given) --tags ai,research Comma-separated tags --related slug:type Typed relation (repeatable) --quality 0-10 Default 5 --status seedling|budding|evergreen Default seedling --type Free-form sub-type ls [domain] List entities (optionally by one domain) ingest [--file F] Upsert JSON from stdin (or --file) --source Provenance tag (default "manual") Domains: People, Companies, Ideas, Research Relation types: ${RELATION_TYPES.join(", ")} `); } function isDomain(s: string): s is Domain { return (DOMAINS as readonly string[]).includes(s); } function isStatus(s: string): s is Status { return (STATUSES as readonly string[]).includes(s); } function isRelationType(s: string): s is RelationType { return (RELATION_TYPES as readonly string[]).includes(s); } function shortLine(entity: Entity, extra?: string): string { const tags = entity.frontmatter.tags.length ? ` [${entity.frontmatter.tags.join(", ")}]` : ""; const tail = extra ? ` ${extra}` : ""; return ` ${entity.domain}/${entity.slug} — ${entity.frontmatter.title}${tags}${tail}`; } // ── search ───────────────────────────────────────────────────────── interface SearchHit { entity: Entity; score: number; } /** * Strip PAL-emitted source markup lines from a body before search scoring. * Keeps the markers on disk (for provenance + idempotency) but excludes * them from the search corpus — otherwise source IDs leak into results * and every entity from a batch matches substrings of the source ID. * ISC-22. */ const PAL_HEADING_RE = /^### \d{4}-\d{2}-\d{2} — /; function bodyForSearch(body: string): string { return body .split("\n") .filter((line) => !PAL_HEADING_RE.test(line) && !line.startsWith("