@tekmidian/pai 0.9.15 → 0.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -1 +0,0 @@
1
- {"version":3,"file":"helpers-OCVFgprQ.mjs","names":["createHash"],"sources":["../src/utils/hash.ts","../src/memory/chunker.ts","../src/memory/indexer/helpers.ts"],"sourcesContent":["/**\n * Shared hashing utilities. Centralises all SHA-256 usage so every module\n * obtains digests through the same function rather than inlining createHash.\n */\n\nimport { createHash } from \"node:crypto\";\n\n/**\n * Compute a SHA-256 hex digest of the given string.\n * Aliased as sha256File for compatibility with existing call-sites that use\n * that name to hash file contents.\n */\nexport function sha256(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/** Alias kept for backwards compatibility with memory/indexer call-sites. */\nexport const sha256File = sha256;\n","/**\n * Markdown text chunker for the PAI memory engine.\n *\n * Splits markdown files into overlapping text segments suitable for BM25\n * full-text indexing. Respects heading boundaries where possible, falling\n * back to paragraph and sentence splitting when sections are large.\n */\n\nimport { sha256 } from \"../utils/hash.js\";\n\nexport interface Chunk {\n text: string;\n startLine: number; // 1-indexed\n endLine: number; // 1-indexed, inclusive\n hash: string; // SHA-256 of text\n}\n\nexport interface ChunkOptions {\n /** Approximate maximum tokens per chunk. Default 400. */\n maxTokens?: number;\n /** Overlap in tokens from the previous chunk. Default 80. */\n overlap?: number;\n}\n\nconst DEFAULT_MAX_TOKENS = 400;\nconst DEFAULT_OVERLAP = 80;\n\n/**\n * Approximate token count using a words * 1.3 heuristic.\n * Matches the OpenClaw estimate approach.\n */\nexport function estimateTokens(text: string): number {\n const wordCount = text.split(/\\s+/).filter(Boolean).length;\n return Math.ceil(wordCount * 1.3);\n}\n\n// sha256 imported from utils/hash.ts\n\n// ---------------------------------------------------------------------------\n// Internal section / paragraph / sentence splitters\n// ---------------------------------------------------------------------------\n\n/**\n * A contiguous block of lines associated with an approximate token count.\n */\ninterface LineBlock {\n lines: Array<{ text: string; lineNo: number }>;\n tokens: number;\n}\n\n/**\n * Split content into sections delimited by ## or ### headings.\n * Each section starts at its heading line (or at line 1 for a preamble).\n */\nfunction splitBySections(\n lines: Array<{ text: string; lineNo: number }>,\n): LineBlock[] {\n const sections: LineBlock[] = [];\n let current: Array<{ text: string; lineNo: number }> = [];\n\n for (const line of lines) {\n const isHeading = /^#{1,3}\\s/.test(line.text);\n if (isHeading && current.length > 0) {\n const text = current.map((l) => l.text).join(\"\\n\");\n sections.push({ lines: current, tokens: estimateTokens(text) });\n current = [];\n }\n current.push(line);\n }\n\n if (current.length > 0) {\n const text = current.map((l) => l.text).join(\"\\n\");\n sections.push({ lines: current, tokens: estimateTokens(text) });\n }\n\n return sections;\n}\n\n/**\n * Split a LineBlock by double-newline paragraph boundaries.\n */\nfunction splitByParagraphs(block: LineBlock): LineBlock[] {\n const paragraphs: LineBlock[] = [];\n let current: Array<{ text: string; lineNo: number }> = [];\n\n for (const line of block.lines) {\n if (line.text.trim() === \"\" && current.length > 0) {\n // Empty line — potential paragraph boundary\n const text = current.map((l) => l.text).join(\"\\n\");\n paragraphs.push({ lines: [...current], tokens: estimateTokens(text) });\n current = [];\n } else {\n current.push(line);\n }\n }\n\n if (current.length > 0) {\n const text = current.map((l) => l.text).join(\"\\n\");\n paragraphs.push({ lines: current, tokens: estimateTokens(text) });\n }\n\n return paragraphs.length > 0 ? paragraphs : [block];\n}\n\n/**\n * Split a LineBlock by sentence boundaries (. ! ?) when even paragraphs are\n * too large. Works character-by-character within joined lines.\n */\nfunction splitBySentences(block: LineBlock, maxTokens: number): LineBlock[] {\n const fullText = block.lines.map((l) => l.text).join(\" \");\n // Very rough sentence split — split on '. ', '! ', '? ' followed by uppercase\n const sentenceRe = /(?<=[.!?])\\s+(?=[A-Z\"'])/g;\n const sentences = fullText.split(sentenceRe);\n\n const result: LineBlock[] = [];\n let accText = \"\";\n // We can't recover exact line numbers inside a single oversized paragraph,\n // so we approximate using the block's start/end lines distributed evenly.\n const startLine = block.lines[0]?.lineNo ?? 1;\n const endLine = block.lines[block.lines.length - 1]?.lineNo ?? startLine;\n const totalLines = endLine - startLine + 1;\n const linesPerSentence = Math.max(1, Math.floor(totalLines / Math.max(1, sentences.length)));\n\n let sentenceIdx = 0;\n let approxLine = startLine;\n\n const flush = () => {\n if (!accText.trim()) return;\n const endApprox = Math.min(approxLine + linesPerSentence - 1, endLine);\n result.push({\n lines: [{ text: accText.trim(), lineNo: approxLine }],\n tokens: estimateTokens(accText),\n });\n approxLine = endApprox + 1;\n accText = \"\";\n };\n\n for (const sentence of sentences) {\n sentenceIdx++;\n const candidateText = accText ? accText + \" \" + sentence : sentence;\n if (estimateTokens(candidateText) > maxTokens && accText) {\n flush();\n accText = sentence;\n } else {\n accText = candidateText;\n }\n }\n void sentenceIdx; // used only for iteration count\n flush();\n\n return result.length > 0 ? result : [block];\n}\n\n// ---------------------------------------------------------------------------\n// Overlap helper\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the last `overlapTokens` worth of text from a list of previously\n * emitted chunks to prepend to the next chunk.\n */\nfunction buildOverlapPrefix(\n chunks: Chunk[],\n overlapTokens: number,\n): Array<{ text: string; lineNo: number }> {\n if (overlapTokens <= 0 || chunks.length === 0) return [];\n\n const lastChunk = chunks[chunks.length - 1];\n if (!lastChunk) return [];\n\n const lines = lastChunk.text.split(\"\\n\");\n const kept: string[] = [];\n let acc = 0;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const lineTokens = estimateTokens(lines[i] ?? \"\");\n acc += lineTokens;\n kept.unshift(lines[i] ?? \"\");\n if (acc >= overlapTokens) break;\n }\n\n // Distribute overlap lines across the lastChunk's line range\n const startLine = lastChunk.endLine - kept.length + 1;\n return kept.map((text, idx) => ({ text, lineNo: Math.max(lastChunk.startLine, startLine + idx) }));\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Chunk a markdown file into overlapping segments for BM25 indexing.\n *\n * Strategy:\n * 1. Split by headings (##, ###) as natural boundaries.\n * 2. If a section exceeds maxTokens, split by paragraphs.\n * 3. If a paragraph still exceeds maxTokens, split by sentences.\n * 4. Apply overlap: each chunk includes the last `overlap` tokens from the\n * previous chunk.\n */\n/**\n * Strip `<private>...</private>` blocks from content before indexing.\n * Content within these tags is excluded from memory — never stored or searched.\n */\nexport function stripPrivateTags(content: string): string {\n return content.replace(/<private>[\\s\\S]*?<\\/private>/gi, \"\");\n}\n\nexport function chunkMarkdown(content: string, opts?: ChunkOptions): Chunk[] {\n const maxTokens = opts?.maxTokens ?? DEFAULT_MAX_TOKENS;\n const overlapTokens = opts?.overlap ?? DEFAULT_OVERLAP;\n\n // Strip private content before indexing\n content = stripPrivateTags(content);\n\n if (!content.trim()) return [];\n\n const rawLines = content.split(\"\\n\");\n const lines: Array<{ text: string; lineNo: number }> = rawLines.map((text, idx) => ({\n text,\n lineNo: idx + 1, // 1-indexed\n }));\n\n // Step 1: section split\n const sections = splitBySections(lines);\n\n // Step 2 & 3: further split oversized sections\n const finalBlocks: LineBlock[] = [];\n for (const section of sections) {\n if (section.tokens <= maxTokens) {\n finalBlocks.push(section);\n continue;\n }\n // Too big — split by paragraphs\n const paras = splitByParagraphs(section);\n for (const para of paras) {\n if (para.tokens <= maxTokens) {\n finalBlocks.push(para);\n continue;\n }\n // Still too big — split by sentences\n const sentences = splitBySentences(para, maxTokens);\n finalBlocks.push(...sentences);\n }\n }\n\n // Step 4: build final chunks with overlap\n const chunks: Chunk[] = [];\n\n for (const block of finalBlocks) {\n if (block.lines.length === 0) continue;\n\n // Build overlap prefix from previous chunks\n const overlapLines = buildOverlapPrefix(chunks, overlapTokens);\n\n // Combine overlap + block lines\n const allLines = [...overlapLines, ...block.lines];\n const text = allLines.map((l) => l.text).join(\"\\n\").trim();\n\n if (!text) continue;\n\n const startLine = block.lines[0]?.lineNo ?? 1;\n const endLine = block.lines[block.lines.length - 1]?.lineNo ?? startLine;\n\n chunks.push({\n text,\n startLine,\n endLine,\n hash: sha256(text),\n });\n }\n\n return chunks;\n}\n","/**\n * Shared helpers for the PAI memory indexers.\n *\n * Contains utilities used by both the sync (SQLite) and async (StorageBackend)\n * indexer paths: hashing, chunk ID generation, directory walking, and path guards.\n */\n\nimport { readdirSync, existsSync } from \"node:fs\";\nimport { sha256File } from \"../../utils/hash.js\";\nimport { join, normalize } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { basename } from \"node:path\";\n\n// ---------------------------------------------------------------------------\n// Tier detection\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a relative file path into one of the four memory tiers.\n *\n * Rules (in priority order):\n * - MEMORY.md anywhere in memory/ → 'evergreen'\n * - YYYY-MM-DD.md in memory/ → 'daily'\n * - anything else in memory/ → 'topic'\n * - anything in Notes/ → 'session'\n */\nexport function detectTier(\n relativePath: string,\n): \"evergreen\" | \"daily\" | \"topic\" | \"session\" {\n // Normalise to forward slashes and strip leading ./\n const p = relativePath.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n\n // Notes directory → session tier\n if (p.startsWith(\"Notes/\") || p === \"Notes\") {\n return \"session\";\n }\n\n const fileName = basename(p);\n\n // MEMORY.md (case-sensitive match) → evergreen\n if (fileName === \"MEMORY.md\") {\n return \"evergreen\";\n }\n\n // YYYY-MM-DD.md → daily\n if (/^\\d{4}-\\d{2}-\\d{2}\\.md$/.test(fileName)) {\n return \"daily\";\n }\n\n // Default for memory/ files\n return \"topic\";\n}\n\n// ---------------------------------------------------------------------------\n// Hashing and chunk ID generation\n// ---------------------------------------------------------------------------\n\n// sha256File imported from ../../utils/hash.js\nexport { sha256File } from \"../../utils/hash.js\";\n\n/**\n * Generate a deterministic chunk ID from its coordinates.\n * Format: sha256(\"projectId:path:chunkIndex:startLine:endLine\")\n *\n * The chunkIndex (0-based position within the file) is included so that\n * chunks with approximated line numbers (e.g. from splitBySentences) never\n * produce colliding IDs even when multiple chunks share the same startLine/endLine.\n */\nexport function chunkId(\n projectId: number,\n path: string,\n chunkIndex: number,\n startLine: number,\n endLine: number,\n): string {\n return createHash(\"sha256\")\n .update(`${projectId}:${path}:${chunkIndex}:${startLine}:${endLine}`)\n .digest(\"hex\");\n}\n\n// ---------------------------------------------------------------------------\n// Event loop yield\n// ---------------------------------------------------------------------------\n\n/**\n * Yield to the Node.js event loop so that IPC server can process requests\n * during long index runs.\n *\n * Uses setTimeout(10ms) rather than setImmediate — the 10ms pause gives the\n * event loop enough time to accept and process incoming IPC connections\n * (socket data, new connections, etc.). Without this, synchronous ONNX\n * inference blocks IPC for the full duration of each embedding (~50-100ms\n * per chunk).\n */\nexport function yieldToEventLoop(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, 10));\n}\n\n// ---------------------------------------------------------------------------\n// Directory skip sets\n// ---------------------------------------------------------------------------\n\n/**\n * Directories to ALWAYS skip, at any depth, during any directory walk.\n * These are build artifacts, dependency trees, and VCS internals that\n * should never be indexed regardless of where they appear in the tree.\n */\nexport const ALWAYS_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\", // CocoaPods (iOS/macOS)\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\", // Xcode\n \".next\", // Next.js\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n // Backup snapshots (Carbon Copy Cloner, Time Machine, etc.)\n \"snaps\",\n \".Trashes\",\n]);\n\n/**\n * Directories to skip when doing a root-level content scan.\n * These are either already handled by dedicated scans or should never be indexed.\n */\nexport const ROOT_SCAN_SKIP_DIRS = new Set([\n \"memory\",\n \"Notes\",\n \".claude\",\n \".DS_Store\",\n // Everything in ALWAYS_SKIP_DIRS is also excluded at root level\n ...ALWAYS_SKIP_DIRS,\n]);\n\n/**\n * Additional directories to skip at the content-scan level (first level below root).\n * These are common macOS/Linux home-directory or repo noise directories that are\n * never meaningful as project content.\n */\nexport const CONTENT_SCAN_SKIP_DIRS = new Set([\n // macOS home directory standard folders\n \"Library\",\n \"Applications\",\n \"Music\",\n \"Movies\",\n \"Pictures\",\n \"Desktop\",\n \"Downloads\",\n \"Public\",\n // Common dev noise\n \"coverage\",\n // Everything in ALWAYS_SKIP_DIRS is also excluded at this level\n ...ALWAYS_SKIP_DIRS,\n]);\n\n// ---------------------------------------------------------------------------\n// Directory walkers\n// ---------------------------------------------------------------------------\n\n/**\n * Safety cap: maximum number of .md files collected per project scan.\n * Prevents runaway scans on huge root paths (e.g. home directory).\n * Projects with more files than this are scanned up to the cap only.\n */\nconst MAX_FILES_PER_PROJECT = 5_000;\n\n/**\n * Maximum recursion depth for directory walks.\n * Prevents deep traversal of large directory trees (e.g. development repos).\n * Depth 0 = the given directory itself (no recursion).\n * Value 6 allows: root → subdirs → sub-subdirs → ... up to 6 levels.\n * Sufficient for memory/, Notes/, and typical docs structures.\n */\nconst MAX_WALK_DEPTH = 6;\n\n/**\n * Recursively collect all .md files under a directory.\n * Returns absolute paths. Stops early if the accumulated count hits the cap\n * or if the recursion depth exceeds MAX_WALK_DEPTH.\n *\n * @param dir Directory to scan.\n * @param acc Shared accumulator array (mutated in place for early exit).\n * @param cap Maximum number of files to collect (across all recursive calls).\n * @param depth Current recursion depth (0 = the initial call).\n */\nexport function walkMdFiles(\n dir: string,\n acc?: string[],\n cap = MAX_FILES_PER_PROJECT,\n depth = 0,\n): string[] {\n const results = acc ?? [];\n if (!existsSync(dir)) return results;\n if (results.length >= cap) return results;\n if (depth > MAX_WALK_DEPTH) return results;\n\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (results.length >= cap) break;\n if (entry.isSymbolicLink()) continue;\n // Skip known junk directories at every recursion depth\n if (ALWAYS_SKIP_DIRS.has(entry.name)) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walkMdFiles(full, results, cap, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(full);\n }\n }\n } catch {\n // Unreadable directory — skip\n }\n return results;\n}\n\n/**\n * Recursively collect all .md files under rootPath, excluding directories\n * that are already covered by dedicated scans (memory/, Notes/) and\n * common noise directories (.git, node_modules, etc.).\n *\n * Returns absolute paths for files NOT already handled by the specific scanners.\n * Stops collecting once MAX_FILES_PER_PROJECT is reached.\n */\nexport function walkContentFiles(rootPath: string): string[] {\n if (!existsSync(rootPath)) return [];\n\n const results: string[] = [];\n try {\n for (const entry of readdirSync(rootPath, { withFileTypes: true })) {\n if (results.length >= MAX_FILES_PER_PROJECT) break;\n if (entry.isSymbolicLink()) continue;\n if (ROOT_SCAN_SKIP_DIRS.has(entry.name)) continue;\n if (CONTENT_SCAN_SKIP_DIRS.has(entry.name)) continue;\n\n const full = join(rootPath, entry.name);\n if (entry.isDirectory()) {\n walkMdFiles(full, results, MAX_FILES_PER_PROJECT);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n // Skip root-level MEMORY.md — handled by the dedicated evergreen scan\n if (entry.name !== \"MEMORY.md\") {\n results.push(full);\n }\n }\n }\n } catch {\n // Unreadable directory — skip\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Path safety guard\n// ---------------------------------------------------------------------------\n\n/** Paths that must never be indexed — system/temp dirs that can contain backup snapshots. */\nconst BLOCKED_ROOTS = new Set([\"/tmp\", \"/private/tmp\", \"/var\", \"/private/var\"]);\n\n/**\n * Returns true if rootPath should skip the recursive content scan.\n *\n * Skips content scanning for:\n * - The home directory itself or any ancestor (too broad — millions of files)\n * - Git repositories (code repos — index memory/ and Notes/ only, not all .md files)\n *\n * The content scan is still useful for Obsidian vaults, Notes folders, and\n * other doc-centric project trees where ALL markdown files are meaningful.\n *\n * The memory/, Notes/, and claude_notes_dir scans always run regardless.\n */\nexport function isPathTooBroadForContentScan(rootPath: string): boolean {\n const normalized = normalize(rootPath);\n\n // Block system/temp directories outright (CCC snapshots live here)\n if (BLOCKED_ROOTS.has(normalized)) return true;\n for (const blocked of BLOCKED_ROOTS) {\n if (normalized.startsWith(blocked + \"/\")) return true;\n }\n\n const home = homedir();\n\n // Skip the home directory itself or any ancestor of home\n if (home.startsWith(normalized) || normalized === \"/\") {\n return true;\n }\n\n // Skip home directory itself (depth 0)\n if (normalized.startsWith(home)) {\n const rel = normalized.slice(home.length).replace(/^\\//, \"\");\n const depth = rel ? rel.split(\"/\").length : 0;\n if (depth === 0) return true;\n }\n\n // Skip git repositories — content scan is only for doc-centric projects\n // (Obsidian vaults, knowledge bases). Code repos use memory/ and Notes/ only.\n if (existsSync(join(normalized, \".git\"))) {\n return true;\n }\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Session title parser\n// ---------------------------------------------------------------------------\n\nconst SESSION_TITLE_RE = /^(\\d{4})\\s*-\\s*(\\d{4}-\\d{2}-\\d{2})\\s*-\\s*(.+)\\.md$/;\n\n/**\n * Parse a session title from a Notes filename.\n * Format: \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n * Returns a synthetic chunk text like \"Session #0086 2026-02-23: Pai Daemon Background Service\"\n * or null if the filename doesn't match the expected pattern.\n */\nexport function parseSessionTitleChunk(fileName: string): string | null {\n const m = SESSION_TITLE_RE.exec(fileName);\n if (!m) return null;\n const [, num, date, title] = m;\n return `Session #${num} ${date}: ${title}`;\n}\n\n/** Number of files to process before yielding to the event loop inside indexProject. */\nexport const INDEX_YIELD_EVERY = 10;\n"],"mappings":";;;;;;;;;;;;;;;AAYA,SAAgB,OAAO,SAAyB;AAC9C,QAAOA,aAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM;;;AAI3D,MAAa,aAAa;;;;;;;;;;;ACO1B,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;;;;;AAMxB,SAAgB,eAAe,MAAsB;CACnD,MAAM,YAAY,KAAK,MAAM,MAAM,CAAC,OAAO,QAAQ,CAAC;AACpD,QAAO,KAAK,KAAK,YAAY,IAAI;;;;;;AAqBnC,SAAS,gBACP,OACa;CACb,MAAM,WAAwB,EAAE;CAChC,IAAI,UAAmD,EAAE;AAEzD,MAAK,MAAM,QAAQ,OAAO;AAExB,MADkB,YAAY,KAAK,KAAK,KAAK,IAC5B,QAAQ,SAAS,GAAG;GACnC,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AAClD,YAAS,KAAK;IAAE,OAAO;IAAS,QAAQ,eAAe,KAAK;IAAE,CAAC;AAC/D,aAAU,EAAE;;AAEd,UAAQ,KAAK,KAAK;;AAGpB,KAAI,QAAQ,SAAS,GAAG;EACtB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AAClD,WAAS,KAAK;GAAE,OAAO;GAAS,QAAQ,eAAe,KAAK;GAAE,CAAC;;AAGjE,QAAO;;;;;AAMT,SAAS,kBAAkB,OAA+B;CACxD,MAAM,aAA0B,EAAE;CAClC,IAAI,UAAmD,EAAE;AAEzD,MAAK,MAAM,QAAQ,MAAM,MACvB,KAAI,KAAK,KAAK,MAAM,KAAK,MAAM,QAAQ,SAAS,GAAG;EAEjD,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AAClD,aAAW,KAAK;GAAE,OAAO,CAAC,GAAG,QAAQ;GAAE,QAAQ,eAAe,KAAK;GAAE,CAAC;AACtE,YAAU,EAAE;OAEZ,SAAQ,KAAK,KAAK;AAItB,KAAI,QAAQ,SAAS,GAAG;EACtB,MAAM,OAAO,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AAClD,aAAW,KAAK;GAAE,OAAO;GAAS,QAAQ,eAAe,KAAK;GAAE,CAAC;;AAGnE,QAAO,WAAW,SAAS,IAAI,aAAa,CAAC,MAAM;;;;;;AAOrD,SAAS,iBAAiB,OAAkB,WAAgC;CAI1E,MAAM,YAHW,MAAM,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,IAAI,CAG9B,MADR,4BACyB;CAE5C,MAAM,SAAsB,EAAE;CAC9B,IAAI,UAAU;CAGd,MAAM,YAAY,MAAM,MAAM,IAAI,UAAU;CAC5C,MAAM,UAAU,MAAM,MAAM,MAAM,MAAM,SAAS,IAAI,UAAU;CAC/D,MAAM,aAAa,UAAU,YAAY;CACzC,MAAM,mBAAmB,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,KAAK,IAAI,GAAG,UAAU,OAAO,CAAC,CAAC;CAE5F,IAAI,cAAc;CAClB,IAAI,aAAa;CAEjB,MAAM,cAAc;AAClB,MAAI,CAAC,QAAQ,MAAM,CAAE;EACrB,MAAM,YAAY,KAAK,IAAI,aAAa,mBAAmB,GAAG,QAAQ;AACtE,SAAO,KAAK;GACV,OAAO,CAAC;IAAE,MAAM,QAAQ,MAAM;IAAE,QAAQ;IAAY,CAAC;GACrD,QAAQ,eAAe,QAAQ;GAChC,CAAC;AACF,eAAa,YAAY;AACzB,YAAU;;AAGZ,MAAK,MAAM,YAAY,WAAW;AAChC;EACA,MAAM,gBAAgB,UAAU,UAAU,MAAM,WAAW;AAC3D,MAAI,eAAe,cAAc,GAAG,aAAa,SAAS;AACxD,UAAO;AACP,aAAU;QAEV,WAAU;;AAId,QAAO;AAEP,QAAO,OAAO,SAAS,IAAI,SAAS,CAAC,MAAM;;;;;;AAW7C,SAAS,mBACP,QACA,eACyC;AACzC,KAAI,iBAAiB,KAAK,OAAO,WAAW,EAAG,QAAO,EAAE;CAExD,MAAM,YAAY,OAAO,OAAO,SAAS;AACzC,KAAI,CAAC,UAAW,QAAO,EAAE;CAEzB,MAAM,QAAQ,UAAU,KAAK,MAAM,KAAK;CACxC,MAAM,OAAiB,EAAE;CACzB,IAAI,MAAM;AAEV,MAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,aAAa,eAAe,MAAM,MAAM,GAAG;AACjD,SAAO;AACP,OAAK,QAAQ,MAAM,MAAM,GAAG;AAC5B,MAAI,OAAO,cAAe;;CAI5B,MAAM,YAAY,UAAU,UAAU,KAAK,SAAS;AACpD,QAAO,KAAK,KAAK,MAAM,SAAS;EAAE;EAAM,QAAQ,KAAK,IAAI,UAAU,WAAW,YAAY,IAAI;EAAE,EAAE;;;;;;;;;;;;;;;;AAqBpG,SAAgB,iBAAiB,SAAyB;AACxD,QAAO,QAAQ,QAAQ,kCAAkC,GAAG;;AAG9D,SAAgB,cAAc,SAAiB,MAA8B;CAC3E,MAAM,YAAY,MAAM,aAAa;CACrC,MAAM,gBAAgB,MAAM,WAAW;AAGvC,WAAU,iBAAiB,QAAQ;AAEnC,KAAI,CAAC,QAAQ,MAAM,CAAE,QAAO,EAAE;CAS9B,MAAM,WAAW,gBAPA,QAAQ,MAAM,KAAK,CAC4B,KAAK,MAAM,SAAS;EAClF;EACA,QAAQ,MAAM;EACf,EAAE,CAGoC;CAGvC,MAAM,cAA2B,EAAE;AACnC,MAAK,MAAM,WAAW,UAAU;AAC9B,MAAI,QAAQ,UAAU,WAAW;AAC/B,eAAY,KAAK,QAAQ;AACzB;;EAGF,MAAM,QAAQ,kBAAkB,QAAQ;AACxC,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,KAAK,UAAU,WAAW;AAC5B,gBAAY,KAAK,KAAK;AACtB;;GAGF,MAAM,YAAY,iBAAiB,MAAM,UAAU;AACnD,eAAY,KAAK,GAAG,UAAU;;;CAKlC,MAAM,SAAkB,EAAE;AAE1B,MAAK,MAAM,SAAS,aAAa;AAC/B,MAAI,MAAM,MAAM,WAAW,EAAG;EAO9B,MAAM,OADW,CAAC,GAHG,mBAAmB,QAAQ,cAAc,EAG3B,GAAG,MAAM,MAAM,CAC5B,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,MAAM;AAE1D,MAAI,CAAC,KAAM;EAEX,MAAM,YAAY,MAAM,MAAM,IAAI,UAAU;EAC5C,MAAM,UAAU,MAAM,MAAM,MAAM,MAAM,SAAS,IAAI,UAAU;AAE/D,SAAO,KAAK;GACV;GACA;GACA;GACA,MAAM,OAAO,KAAK;GACnB,CAAC;;AAGJ,QAAO;;;;;;;;;;;;;;;;;;;;ACtPT,SAAgB,WACd,cAC6C;CAE7C,MAAM,IAAI,aAAa,QAAQ,OAAO,IAAI,CAAC,QAAQ,SAAS,GAAG;AAG/D,KAAI,EAAE,WAAW,SAAS,IAAI,MAAM,QAClC,QAAO;CAGT,MAAM,WAAW,SAAS,EAAE;AAG5B,KAAI,aAAa,YACf,QAAO;AAIT,KAAI,0BAA0B,KAAK,SAAS,CAC1C,QAAO;AAIT,QAAO;;;;;;;;;;AAkBT,SAAgB,QACd,WACA,MACA,YACA,WACA,SACQ;AACR,QAAO,WAAW,SAAS,CACxB,OAAO,GAAG,UAAU,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CACpE,OAAO,MAAM;;;;;;;;;;;;AAiBlB,SAAgB,mBAAkC;AAChD,QAAO,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;;;;;;;AAY1D,MAAa,mBAAmB,IAAI,IAAI;CAEtC;CAEA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACD,CAAC;;;;;AAMF,MAAa,sBAAsB,IAAI,IAAI;CACzC;CACA;CACA;CACA;CAEA,GAAG;CACJ,CAAC;;;;;;AAOF,MAAa,yBAAyB,IAAI,IAAI;CAE5C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA,GAAG;CACJ,CAAC;;;;;;AAWF,MAAM,wBAAwB;;;;;;;;AAS9B,MAAM,iBAAiB;;;;;;;;;;;AAYvB,SAAgB,YACd,KACA,KACA,MAAM,uBACN,QAAQ,GACE;CACV,MAAM,UAAU,OAAO,EAAE;AACzB,KAAI,CAAC,WAAW,IAAI,CAAE,QAAO;AAC7B,KAAI,QAAQ,UAAU,IAAK,QAAO;AAClC,KAAI,QAAQ,eAAgB,QAAO;AAEnC,KAAI;AACF,OAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,EAAE;AAC7D,OAAI,QAAQ,UAAU,IAAK;AAC3B,OAAI,MAAM,gBAAgB,CAAE;AAE5B,OAAI,iBAAiB,IAAI,MAAM,KAAK,CAAE;GACtC,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAClC,OAAI,MAAM,aAAa,CACrB,aAAY,MAAM,SAAS,KAAK,QAAQ,EAAE;YACjC,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,CACrD,SAAQ,KAAK,KAAK;;SAGhB;AAGR,QAAO;;;;;;;;;;AAWT,SAAgB,iBAAiB,UAA4B;AAC3D,KAAI,CAAC,WAAW,SAAS,CAAE,QAAO,EAAE;CAEpC,MAAM,UAAoB,EAAE;AAC5B,KAAI;AACF,OAAK,MAAM,SAAS,YAAY,UAAU,EAAE,eAAe,MAAM,CAAC,EAAE;AAClE,OAAI,QAAQ,UAAU,sBAAuB;AAC7C,OAAI,MAAM,gBAAgB,CAAE;AAC5B,OAAI,oBAAoB,IAAI,MAAM,KAAK,CAAE;AACzC,OAAI,uBAAuB,IAAI,MAAM,KAAK,CAAE;GAE5C,MAAM,OAAO,KAAK,UAAU,MAAM,KAAK;AACvC,OAAI,MAAM,aAAa,CACrB,aAAY,MAAM,SAAS,sBAAsB;YACxC,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,MAAM,EAErD;QAAI,MAAM,SAAS,YACjB,SAAQ,KAAK,KAAK;;;SAIlB;AAGR,QAAO;;;AAQT,MAAM,gBAAgB,IAAI,IAAI;CAAC;CAAQ;CAAgB;CAAQ;CAAe,CAAC;;;;;;;;;;;;;AAc/E,SAAgB,6BAA6B,UAA2B;CACtE,MAAM,aAAa,UAAU,SAAS;AAGtC,KAAI,cAAc,IAAI,WAAW,CAAE,QAAO;AAC1C,MAAK,MAAM,WAAW,cACpB,KAAI,WAAW,WAAW,UAAU,IAAI,CAAE,QAAO;CAGnD,MAAM,OAAO,SAAS;AAGtB,KAAI,KAAK,WAAW,WAAW,IAAI,eAAe,IAChD,QAAO;AAIT,KAAI,WAAW,WAAW,KAAK,EAAE;EAC/B,MAAM,MAAM,WAAW,MAAM,KAAK,OAAO,CAAC,QAAQ,OAAO,GAAG;AAE5D,OADc,MAAM,IAAI,MAAM,IAAI,CAAC,SAAS,OAC9B,EAAG,QAAO;;AAK1B,KAAI,WAAW,KAAK,YAAY,OAAO,CAAC,CACtC,QAAO;AAGT,QAAO;;AAOT,MAAM,mBAAmB;;;;;;;AAQzB,SAAgB,uBAAuB,UAAiC;CACtE,MAAM,IAAI,iBAAiB,KAAK,SAAS;AACzC,KAAI,CAAC,EAAG,QAAO;CACf,MAAM,GAAG,KAAK,MAAM,SAAS;AAC7B,QAAO,YAAY,IAAI,GAAG,KAAK,IAAI;;;AAIrC,MAAa,oBAAoB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"kg-extraction-r5KkTmWY.mjs","names":[],"sources":["../src/cli/commands/registry/utils.ts","../src/cli/commands/registry/scan.ts","../src/daemon/templates/triple-extraction-prompt.ts","../src/memory/kg-extraction.ts"],"sourcesContent":["/** Shared database helpers for registry command operations. */\n\nimport type { Database } from \"better-sqlite3\";\nimport { now } from \"../../utils.js\";\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n * 1. root_path — most reliable; handles slug collisions\n * 2. encoded_dir — Claude project dirs are canonical\n * 3. Insert with suffix-deduplication on slug collision\n */\nexport function upsertProject(\n db: Database,\n slug: string,\n rootPath: string,\n encodedDir: string\n): { id: number; isNew: boolean } {\n const ts = now();\n\n const byPath = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (byPath) {\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (!encodedOwner || encodedOwner.id === byPath.id) {\n db.prepare(\n \"UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(encodedDir, ts, byPath.id);\n }\n return { id: byPath.id, isNew: false };\n }\n\n const byEncoded = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (byEncoded) {\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (!pathOwner || pathOwner.id === byEncoded.id) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(rootPath, ts, byEncoded.id);\n }\n return { id: byEncoded.id, isNew: false };\n }\n\n // Insert — deduplicate slug with numeric suffix if needed.\n let finalSlug = slug;\n let attempt = 0;\n while (true) {\n const conflict = db\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(finalSlug) as { id: number } | undefined;\n if (!conflict) break;\n attempt++;\n finalSlug = `${slug}-${attempt}`;\n }\n\n const result = db\n .prepare(\n `INSERT OR IGNORE INTO projects\n (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n )\n .run(finalSlug, finalSlug, rootPath, encodedDir, ts, ts);\n\n if (result.changes === 0) {\n const fallback =\n (db.prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\").get(encodedDir) as { id: number } | undefined) ??\n (db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(rootPath) as { id: number } | undefined);\n\n if (fallback) {\n return { id: fallback.id, isNew: false };\n }\n\n throw new Error(\n `upsertProject: INSERT OR IGNORE was suppressed but no matching row found ` +\n `for root_path=${rootPath} encoded_dir=${encodedDir}`\n );\n }\n\n return { id: result.lastInsertRowid as number, isNew: true };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport function upsertSession(\n db: Database,\n projectId: number,\n number: number,\n date: string,\n slug: string,\n title: string,\n filename: string\n): boolean {\n const existing = db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(projectId, number);\n\n if (existing) return false;\n\n const ts = now();\n db.prepare(\n `INSERT INTO sessions\n (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`\n ).run(projectId, number, date, slug, title, filename, ts);\n\n return true;\n}\n","/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */\n\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { ok, warn, err, dim, bold } from \"../../utils.js\";\nimport { encodeDir } from \"../../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../../registry/pai-marker.js\";\nimport { upsertProject, upsertSession } from \"./utils.js\";\nimport type { Database } from \"better-sqlite3\";\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\nconst PAI_CONFIG_DIR = join(homedir(), \".pai\");\nconst PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, \"config.json\");\n\ninterface PaiConfig {\n scan_dirs: string[];\n}\n\nexport function loadScanConfig(): PaiConfig {\n if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };\n try {\n return JSON.parse(readFileSync(PAI_CONFIG_FILE, \"utf8\")) as PaiConfig;\n } catch {\n return { scan_dirs: [] };\n }\n}\n\nexport function saveScanConfig(config: PaiConfig): void {\n mkdirSync(PAI_CONFIG_DIR, { recursive: true });\n writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\nexport function resolveHome(p: string): string {\n if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// File discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories.\n * Returns filenames (basename only).\n */\nexport function findNoteFiles(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(entry.name);\n } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n const yearDir = join(dir, entry.name);\n for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n const monthDir = join(yearDir, monthEntry.name);\n for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n results.push(noteEntry.name);\n }\n }\n }\n }\n }\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Scan result type\n// ---------------------------------------------------------------------------\n\nexport interface ScanResult {\n projectsScanned: number;\n projectsNew: number;\n projectsUpdated: number;\n sessionsScanned: number;\n sessionsNew: number;\n skipped: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Core scan logic\n// ---------------------------------------------------------------------------\n\nexport function performScan(db: Database): ScanResult {\n const result: ScanResult = {\n projectsScanned: 0,\n projectsNew: 0,\n projectsUpdated: 0,\n sessionsScanned: 0,\n sessionsNew: 0,\n skipped: [],\n };\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);\n }\n\n const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n const full = join(CLAUDE_PROJECTS_DIR, name);\n return statSync(full).isDirectory();\n });\n\n const lookupMap = buildEncodedDirMap();\n\n for (const encodedDir of entries) {\n const rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n if (!existsSync(rootPath)) {\n result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);\n result.projectsScanned++;\n continue;\n }\n\n const slug = slugify(basename(rootPath) || encodedDir);\n const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);\n\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try {\n ensurePaiMarker(rootPath, slug);\n } catch {\n // Non-fatal\n }\n\n const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n if (existsSync(claudeNotesDir)) {\n const rootNotesDir = join(rootPath, \"Notes\");\n if (claudeNotesDir !== rootNotesDir) {\n db.prepare(\n \"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(claudeNotesDir, Date.now(), id);\n }\n }\n\n if (!existsSync(claudeNotesDir)) continue;\n\n const noteFiles = findNoteFiles(claudeNotesDir);\n\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n\n // Phase 2: Scan project-root Notes/ for all registered active projects\n {\n const activeProjects = db\n .prepare(\"SELECT id, slug, root_path FROM projects WHERE status = 'active'\")\n .all() as { id: number; slug: string; root_path: string }[];\n\n for (const project of activeProjects) {\n const notesDir = join(project.root_path, \"Notes\");\n if (!existsSync(notesDir)) continue;\n\n let files: string[];\n try {\n files = findNoteFiles(notesDir);\n } catch {\n continue;\n }\n\n for (const filename of files) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(db, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n }\n\n // Phase 3: Scan extra directories from config\n const config = loadScanConfig();\n if (config.scan_dirs.length) {\n for (const rawDir of config.scan_dirs) {\n const scanDir = resolveHome(rawDir);\n if (!existsSync(scanDir)) {\n result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n continue;\n }\n\n const children = readdirSync(scanDir).filter((name) => {\n if (name.startsWith(\".\")) return false;\n const full = join(scanDir, name);\n try { return statSync(full).isDirectory(); } catch { return false; }\n });\n\n for (const child of children) {\n const childPath = join(scanDir, child);\n const childSlug = slugify(child);\n const childEncoded = encodeDir(childPath);\n\n const existing = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(childPath) as { id: number } | undefined;\n\n if (existing) {\n result.projectsScanned++;\n result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n continue;\n }\n\n const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n }\n }\n }\n\n // Phase 4: Discover PAI.md markers in scan_dirs\n if (config.scan_dirs.length) {\n const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n const markers = discoverPaiMarkers(resolvedScanDirs);\n\n for (const marker of markers) {\n const registeredRow = db\n .prepare(\"SELECT id, root_path, slug FROM projects WHERE slug = ?\")\n .get(marker.slug) as { id: number; root_path: string; slug: string } | undefined;\n\n if (!registeredRow) continue;\n\n if (registeredRow.root_path !== marker.projectRoot) {\n const newEncoded = encodeDir(marker.projectRoot);\n const now4 = Date.now();\n\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(newEncoded) as { id: number } | undefined;\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(marker.projectRoot) as { id: number } | undefined;\n\n const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n if (encodedSafe && pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(marker.projectRoot, newEncoded, now4, registeredRow.id);\n } else if (pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(marker.projectRoot, now4, registeredRow.id);\n }\n }\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// cmdScan\n// ---------------------------------------------------------------------------\n\n/**\n * Run the registry scan CLI command.\n *\n * @param opts.quick When true, skips verbose output (same scan, less noise).\n * The underlying scan is always incremental via upsert —\n * this flag exists for hook/daemon-triggered invocations that\n * want minimal log output.\n */\nexport function cmdScan(db: Database, opts: { quick?: boolean } = {}): void {\n const config = loadScanConfig();\n if (!opts.quick) {\n console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n if (config.scan_dirs.length) {\n console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n }\n console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n }\n\n let result: ScanResult;\n try {\n result = performScan(db);\n } catch (e) {\n console.error(err(String(e)));\n process.exit(1);\n }\n\n if (!opts.quick) {\n console.log(\n ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`)\n );\n console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));\n console.log(dim(` Sessions: ${result.sessionsNew} new`));\n\n if (result.skipped.length) {\n console.log();\n console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));\n for (const s of result.skipped.slice(0, 10)) {\n console.log(dim(` ${s}`));\n }\n if (result.skipped.length > 10) {\n console.log(dim(` ... and ${result.skipped.length - 10} more`));\n }\n }\n } else {\n // Quick mode: single compact line (stderr so it doesn't pollute pipe output)\n process.stderr.write(\n `[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions ` +\n `(${result.projectsNew}+${result.sessionsNew} new).\\n`\n );\n }\n}\n","/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n sessionContent: string;\n projectSlug: string;\n gitLog: string;\n}): string {\n return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"Matthias\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n \"entities\": [\n {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n {\"name\": \"Matthias\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n ],\n \"relations\": [\n {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n {\"source\": \"Matthias\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nJSON object (entities + relations):`;\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n * - findClaudeBinary() — locate the claude CLI\n * - spawnClaude() — generic prompt -> response runner (strips ANTHROPIC_API_KEY)\n * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { kgAdd, kgQuery, kgInvalidate } from \"./kg.js\";\nimport { upsertKgEntity } from \"./kg-entity.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n return \"claude\";\n}\n\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n haiku: 60_000,\n sonnet: 120_000,\n opus: 300_000,\n};\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so\n * the CLI uses the user's Max plan (free) instead of billing the API key.\n */\nexport async function spawnClaude(\n prompt: string,\n model: \"haiku\" | \"sonnet\" | \"opus\" = \"sonnet\"\n): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;\n const child = spawn(\n claudeBin,\n [\"--model\", model, \"-p\", \"--no-session-persistence\"],\n { env: envWithoutApiKey, stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n );\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n timer = setTimeout(() => {\n process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\nexport interface ExtractTriplesParams {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog?: string;\n model?: \"haiku\" | \"sonnet\" | \"opus\";\n /** Optional federation SQLite db — when provided, entities are upserted into kg_entities (QW1) */\n federationDb?: Database;\n /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n extracted: number;\n added: number;\n superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n pool: Pool,\n params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n const prompt = buildTripleExtractionPrompt({\n sessionContent: params.summaryText,\n projectSlug: params.projectSlug,\n gitLog: params.gitLog ?? \"\",\n });\n\n const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n if (!jsonOutput) return stats;\n\n // Strip markdown code fences if Claude wrapped the JSON\n const cleaned = jsonOutput\n .replace(/^```json\\s*/m, \"\")\n .replace(/^```\\s*/m, \"\")\n .replace(/\\s*```$/m, \"\")\n .trim();\n\n // Support both legacy array format and new structured format\n type LegacyTriple = { subject: string; predicate: string; object: string };\n type NewRelation = { source: string; relation: string; target: string };\n type NewEntity = { name: string; type: string; description: string };\n type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n let triples: Array<LegacyTriple>;\n try {\n const parsed = JSON.parse(cleaned);\n\n if (Array.isArray(parsed)) {\n // Legacy format: [{subject, predicate, object}]\n triples = parsed;\n } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n // New structured format: {entities: [...], relations: [...]}\n const newFmt = parsed as NewFormat;\n\n // QW1: Upsert entities into federation SQLite kg_entities table when db is available\n if (params.federationDb && Array.isArray(newFmt.entities)) {\n const tenantId = params.tenantId ?? \"default\";\n for (const entity of newFmt.entities) {\n if (!entity.name) continue;\n try {\n upsertKgEntity(params.federationDb, {\n name: entity.name,\n type: entity.type ?? \"unknown\",\n description: entity.description,\n tenantId,\n });\n } catch (entityErr) {\n process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n }\n }\n }\n\n triples = newFmt.relations.map((r: NewRelation) => ({\n subject: r.source,\n predicate: r.relation,\n object: r.target,\n }));\n } else {\n process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n return stats;\n }\n } catch (e) {\n process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\\n`);\n return stats;\n }\n\n if (!Array.isArray(triples)) return stats;\n stats.extracted = triples.length;\n\n for (const t of triples) {\n if (!t.subject || !t.predicate || !t.object) continue;\n\n try {\n const existing = await kgQuery(pool, {\n subject: t.subject,\n predicate: t.predicate,\n project_id: params.projectId ?? undefined,\n });\n\n // If an identical (subject, predicate, object) is already valid, skip — idempotent\n const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n if (alreadyValid) continue;\n\n // Invalidate any superseded triple (same subject+predicate, different object)\n const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n if (supersedes) {\n await kgInvalidate(pool, supersedes.id);\n stats.superseded++;\n }\n\n await kgAdd(pool, {\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n project_id: params.projectId ?? undefined,\n source_session: params.sessionId,\n confidence: \"EXTRACTED\",\n });\n stats.added++;\n } catch (tripleErr) {\n process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n }\n }\n\n return stats;\n}\n"],"mappings":"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"}