add familiar commands: capture, process, graph, health, briefing

This commit is contained in:
artale 2026-06-13 10:50:12 +02:00
parent a8a88a4553
commit efba0f026c
3 changed files with 192 additions and 0 deletions

1
.gitignore vendored
View File

@ -5,5 +5,6 @@ dist/
*.log
LOGS/
TEMP/
.familiar-test
.DS_Store
*.tsbuildinfo

View File

@ -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 <note>")
.description("Quick capture — drop a note into inbox")
.option("-s, --source <src>", "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

View File

@ -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[];
}