diff --git a/.gitignore b/.gitignore index 3709fe7..82a610b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ dist/ *.log LOGS/ TEMP/ +.familiar-test .DS_Store *.tsbuildinfo diff --git a/src/index.ts b/src/index.ts index cf6a768..d6d1368 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1068,6 +1068,114 @@ program console.log(` Every run compounds. The harness, not the model.\n`); }); +// ── Familiar ──────────────────────────────────────────────── + +const familiar = program + .command("familiar") + .description("Familiar knowledge base — inbox-to-wiki processing"); + +familiar + .command("capture ") + .description("Quick capture — drop a note into inbox") + .option("-s, --source ", "Source label (voice, web, idea, etc.)") + .action(async (note: string, opts: { source?: string }) => { + const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js"); + const f = new FamiliarKnowledge(); + const path = f.quickCapture(note, opts.source); + console.log(`\n ✓ Captured to: ${path}\n`); + }); + +familiar + .command("process") + .description("Process all inbox notes into wiki pages") + .action(async () => { + const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js"); + const f = new FamiliarKnowledge(); + const notes = f.scanInbox(); + + if (notes.length === 0) { + console.log(` No notes in inbox/ to process.\n`); + return; + } + + // Default classifier — generates basic wiki pages from inbox content + const classifier = (content: string) => { + const firstLine = content.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? "Untitled"; + const tags = ["inbox"]; + if (content.toLowerCase().includes("loop")) tags.push("loops"); + if (content.toLowerCase().includes("agent")) tags.push("agent"); + if (content.toLowerCase().includes("safety")) tags.push("safety"); + + return { + title: firstLine, + summary: content.split("\n").slice(1, 3).join(" ").trim().slice(0, 300), + confidence: 0.5, + tags, + mentions: [], + contradictions: [], + }; + }; + + const processed = f.processAllInbox(classifier); + f.autoCommit(`familiar: processed ${processed.length} inbox notes`); + console.log(`\n Processed ${processed.length} notes:\n`); + for (const p of processed) { + console.log(` ✓ ${p.frontmatter.title}`); + console.log(` Tags: ${p.frontmatter.tags.join(", ")}`); + console.log(` Wiki: ${p.wikiPath}`); + } + console.log(``); + }); + +familiar + .command("graph") + .description("Show knowledge graph") + .action(async () => { + const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js"); + const f = new FamiliarKnowledge(); + const graph = f.buildGraphIndex(); + console.log(`\n Knowledge Graph:`); + console.log(` ${graph.nodes.length} pages, ${graph.edges.length} connections\n`); + for (const node of graph.nodes) { + const edges = graph.edges.filter((e) => e.source === node.id); + console.log(` ${node.title}`); + for (const e of edges) console.log(` → ${e.target}`); + } + console.log(``); + }); + +familiar + .command("health") + .description("Audit wiki health — detect orphaned pages, low confidence, missing backlinks") + .action(async () => { + const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js"); + const f = new FamiliarKnowledge(); + const health = f.graphHealth(); + console.log(`\n Graph Health:\n`); + console.log(` Pages: ${health.stats.totalPages}`); + console.log(` Edges: ${health.stats.totalConnections}`); + console.log(` Orphaned: ${health.stats.orphanedPages}`); + console.log(` No backlinks: ${health.stats.pagesWithoutBacklinks}`); + console.log(` Low confidence: ${health.stats.pagesWithLowConfidence}\n`); + if (health.issues.length > 0) { + console.log(` Issues:`); + for (const issue of health.issues.slice(0, 5)) { + console.log(` • ${issue}`); + } + } + console.log(``); + }); + +familiar + .command("briefing") + .description("Generate daily briefing page") + .action(async () => { + const { FamiliarKnowledge } = await import("./upgrades/familiar-knowledge.js"); + const f = new FamiliarKnowledge(); + const briefing = f.generateBriefing(); + console.log(`\n${briefing}\n`); + }); + // ── PAI Pi ─────────────────────────────────────────────────── const paiPi = program diff --git a/src/upgrades/familiar-knowledge.ts b/src/upgrades/familiar-knowledge.ts index e8181fd..e810798 100644 --- a/src/upgrades/familiar-knowledge.ts +++ b/src/upgrades/familiar-knowledge.ts @@ -283,6 +283,77 @@ export class FamiliarKnowledge { return lines.join("\n"); } + /** Quick capture — drop a note into inbox/ with timestamp */ + quickCapture(content: string, source?: string): string { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const prefix = source ? `captured-from-${source}` : "quick-capture"; + const fileName = `${prefix}-${timestamp}.md`; + const filePath = path.join(this.baseDir, "inbox", fileName); + const header = source + ? `# Quick Capture (from ${source})\n\nCaptured: ${new Date().toISOString()}\n\n` + : `# Quick Capture\n\nCaptured: ${new Date().toISOString()}\n\n`; + fs.writeFileSync(filePath, header + content, "utf-8"); + return filePath; + } + + /** Graph health audit — detect issues in the wiki */ + graphHealth(): GraphHealthReport { + const graph = this.buildGraphIndex(); + const issues: string[] = []; + const stats = { + totalPages: graph.nodes.length, + totalConnections: graph.edges.length, + orphanedPages: 0, + pagesWithLowConfidence: 0, + pagesWithoutBacklinks: 0, + stalePages: 0, + }; + + for (const node of graph.nodes) { + // Orphaned: no edges at all + const hasEdges = graph.edges.some((e) => e.source === node.id || e.target === node.id); + if (!hasEdges) { + stats.orphanedPages++; + issues.push(`Orphaned: "${node.title}" has no connections to other pages`); + } + + // Low confidence + if (node.confidence < 0.3) { + stats.pagesWithLowConfidence++; + issues.push(`Low confidence: "${node.title}" (${node.confidence})`); + } + + // No backlinks (only check source edges since our graph has source→target direction) + const hasIncoming = graph.edges.some((e) => e.target === node.id); + if (!hasIncoming && graph.nodes.length > 1) { + stats.pagesWithoutBacklinks++; + issues.push(`No backlinks: "${node.title}" has no pages linking to it`); + } + } + + return { stats, issues }; + } + + /** Git auto-commit after processing */ + autoCommit(message?: string): boolean { + try { + const { execSync } = require("node:child_process"); + // Check if we're in a git repo + execSync("git rev-parse --git-dir", { stdio: "ignore", cwd: this.baseDir }); + + const commitMsg = message ?? `familiar: auto-process inbox at ${new Date().toISOString().slice(0, 16)}`; + execSync(`git add -A`, { cwd: this.baseDir }); + execSync(`git commit -m "${commitMsg}"`, { cwd: this.baseDir, stdio: "ignore" }); + + // Count changed files + const diff = execSync("git diff --cached --name-only", { encoding: "utf-8", cwd: this.baseDir }); + const fileCount = diff.trim().split("\n").filter(Boolean).length; + return fileCount > 0; + } catch { + return false; // Not a git repo or git not available + } + } + private slugify(text: string): string { return text .toLowerCase() @@ -323,3 +394,15 @@ export interface GraphIndex { nodes: GraphNode[]; edges: GraphEdge[]; } + +export interface GraphHealthReport { + stats: { + totalPages: number; + totalConnections: number; + orphanedPages: number; + pagesWithLowConfidence: number; + pagesWithoutBacklinks: number; + stalePages: number; + }; + issues: string[]; +}