@tekmidian/pai 0.11.0 → 0.12.0
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/README.md +84 -10
- package/dist/{auto-route-D54Kv7yI.mjs → auto-route-CLYcToZ5.mjs} +4 -4
- package/dist/{auto-route-D54Kv7yI.mjs.map → auto-route-CLYcToZ5.mjs.map} +1 -1
- package/dist/cli/index.mjs +20 -10275
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts +11 -0
- package/dist/cli/program.d.mts.map +1 -0
- package/dist/cli/program.mjs +249 -0
- package/dist/cli/program.mjs.map +1 -0
- package/dist/{clusters-BwmdKB-V.mjs → clusters-BYPw7vfW.mjs} +2 -2
- package/dist/{clusters-BwmdKB-V.mjs.map → clusters-BYPw7vfW.mjs.map} +1 -1
- package/dist/{config-DqBY3aT0.mjs → config-BuhHWyOK.mjs} +1 -1
- package/dist/{config-DqBY3aT0.mjs.map → config-BuhHWyOK.mjs.map} +1 -1
- package/dist/daemon/index.mjs +16 -18
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-CSKj8Xm9.mjs → daemon-DDplhnm9.mjs} +185 -31
- package/dist/daemon-DDplhnm9.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/{db-CYmBWcjh.mjs → db-CmYbAVCD.mjs} +1 -1
- package/dist/{db-CYmBWcjh.mjs.map → db-CmYbAVCD.mjs.map} +1 -1
- package/dist/{detect-CdaA48EI.mjs → detect-KjycLtXM.mjs} +1 -1
- package/dist/{detect-CdaA48EI.mjs.map → detect-KjycLtXM.mjs.map} +1 -1
- package/dist/{detector-Q_lVXtj-.mjs → detector-ZiOhHszd.mjs} +2 -2
- package/dist/{detector-Q_lVXtj-.mjs.map → detector-ZiOhHszd.mjs.map} +1 -1
- package/dist/{embeddings-DGRAPAYb.mjs → embeddings-BJPOcbik.mjs} +1 -1
- package/dist/{embeddings-DGRAPAYb.mjs.map → embeddings-BJPOcbik.mjs.map} +1 -1
- package/dist/{factory-BufouUQ1.mjs → factory-CayQCMoN.mjs} +5 -8
- package/dist/{factory-BufouUQ1.mjs.map → factory-CayQCMoN.mjs.map} +1 -1
- package/dist/{helpers-crDEr6S2.mjs → helpers-IjZkXBhj.mjs} +1 -1
- package/dist/{helpers-crDEr6S2.mjs.map → helpers-IjZkXBhj.mjs.map} +1 -1
- package/dist/hooks/observe.mjs +21 -0
- package/dist/hooks/observe.mjs.map +2 -2
- package/dist/index.mjs +9 -10
- package/dist/{indexer-backend-DcYThufd.mjs → indexer-backend-Cm5RS7y0.mjs} +3 -3
- package/dist/{indexer-backend-DcYThufd.mjs.map → indexer-backend-Cm5RS7y0.mjs.map} +1 -1
- package/dist/{ipc-client-C3pjwy2m.mjs → ipc-client-CoyUHPod.mjs} +1 -1
- package/dist/{ipc-client-C3pjwy2m.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/{kg-entity-DVzTsy6G.mjs → kg-entity-LXblD7LZ.mjs} +1 -1
- package/dist/{kg-entity-DVzTsy6G.mjs.map → kg-entity-LXblD7LZ.mjs.map} +1 -1
- package/dist/{kg-extraction-iR1BKKWK.mjs → kg-extraction-C8DEUHTS.mjs} +17 -6
- package/dist/kg-extraction-C8DEUHTS.mjs.map +1 -0
- package/dist/{latent-ideas-dWVuI_dg.mjs → latent-ideas-RyC6eyqI.mjs} +4 -4
- package/dist/{latent-ideas-dWVuI_dg.mjs.map → latent-ideas-RyC6eyqI.mjs.map} +1 -1
- package/dist/main-resolver-uFxNiDy7.mjs +1339 -0
- package/dist/main-resolver-uFxNiDy7.mjs.map +1 -0
- package/dist/{migrate-fLD6rAdO.mjs → migrate-B02wDDgS.mjs} +2 -2
- package/dist/{migrate-fLD6rAdO.mjs.map → migrate-B02wDDgS.mjs.map} +1 -1
- package/dist/{neighborhood-DixmpfPT.mjs → neighborhood-CklGIB8r.mjs} +2 -2
- package/dist/{neighborhood-DixmpfPT.mjs.map → neighborhood-CklGIB8r.mjs.map} +1 -1
- package/dist/{note-context-BY7rfGdl.mjs → note-context-qvZlVXVO.mjs} +1 -1
- package/dist/{note-context-BY7rfGdl.mjs.map → note-context-qvZlVXVO.mjs.map} +1 -1
- package/dist/{pai-marker-CXQPX2P6.mjs → pai-marker-HVBwwBW-.mjs} +1 -1
- package/dist/{pai-marker-CXQPX2P6.mjs.map → pai-marker-HVBwwBW-.mjs.map} +1 -1
- package/dist/pick-Dt21ojcW.mjs +8250 -0
- package/dist/pick-Dt21ojcW.mjs.map +1 -0
- package/dist/{postgres-Cp25xZes.mjs → postgres-B451Hnkm.mjs} +2 -2
- package/dist/{postgres-Cp25xZes.mjs.map → postgres-B451Hnkm.mjs.map} +1 -1
- package/dist/{query-feedback-C0hKUHVz.mjs → query-feedback-fapbqRgh.mjs} +1 -1
- package/dist/{query-feedback-C0hKUHVz.mjs.map → query-feedback-fapbqRgh.mjs.map} +1 -1
- package/dist/{reranker-CMNZcfVx.mjs → reranker-C08R99zn.mjs} +1 -1
- package/dist/{reranker-CMNZcfVx.mjs.map → reranker-C08R99zn.mjs.map} +1 -1
- package/dist/{search-i2nlQ-JM.mjs → search-HcdKtMla.mjs} +3 -3
- package/dist/{search-i2nlQ-JM.mjs.map → search-HcdKtMla.mjs.map} +1 -1
- package/dist/{sqlite-B3JHfJc2.mjs → sqlite-DQpY1Esi.mjs} +3 -3
- package/dist/{sqlite-B3JHfJc2.mjs.map → sqlite-DQpY1Esi.mjs.map} +1 -1
- package/dist/{state-BHZAhLou.mjs → state-BIlxNRUn.mjs} +1 -1
- package/dist/{state-BHZAhLou.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs → stop-words-BwplsQ3z.mjs} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs.map → stop-words-BwplsQ3z.mjs.map} +1 -1
- package/dist/{sync-CmBKOL3K.mjs → sync-uPR4g438.mjs} +205 -6
- package/dist/sync-uPR4g438.mjs.map +1 -0
- package/dist/{themes-DO8mUVwD.mjs → themes-Cg8oG9Ra.mjs} +3 -3
- package/dist/{themes-DO8mUVwD.mjs.map → themes-Cg8oG9Ra.mjs.map} +1 -1
- package/dist/{tools-C1JNgya7.mjs → tools-Op3C2Nm6.mjs} +26 -26
- package/dist/{tools-C1JNgya7.mjs.map → tools-Op3C2Nm6.mjs.map} +1 -1
- package/dist/{trace-TYCLMCkY.mjs → trace-CLK-NPkb.mjs} +1 -1
- package/dist/{trace-TYCLMCkY.mjs.map → trace-CLK-NPkb.mjs.map} +1 -1
- package/dist/{utils-BAxjW3j8.mjs → utils-CqqgB0dH.mjs} +1 -1
- package/dist/{utils-BAxjW3j8.mjs.map → utils-CqqgB0dH.mjs.map} +1 -1
- package/dist/{vault-indexer-vNnL3Azb.mjs → vault-indexer-Dn_BccVK.mjs} +2 -2
- package/dist/{vault-indexer-vNnL3Azb.mjs.map → vault-indexer-Dn_BccVK.mjs.map} +1 -1
- package/dist/{work-queue-worker-BxsBtz6j.mjs → work-queue-worker-DHOIVVQ7.mjs} +5 -5
- package/dist/{work-queue-worker-BxsBtz6j.mjs.map → work-queue-worker-DHOIVVQ7.mjs.map} +1 -1
- package/dist/{zettelkasten-B-94vghH.mjs → zettelkasten-NwBD1NsT.mjs} +4 -4
- package/dist/{zettelkasten-B-94vghH.mjs.map → zettelkasten-NwBD1NsT.mjs.map} +1 -1
- package/docs/commands/README.md +151 -0
- package/docs/commands/_examples/kg.md +13 -0
- package/docs/commands/_examples/memory.md +13 -0
- package/docs/commands/_examples/observation.md +13 -0
- package/docs/commands/_examples/projects.md +16 -0
- package/docs/commands/_examples/skill.md +13 -0
- package/docs/commands/_examples/zettel.md +13 -0
- package/docs/commands/backup.md +24 -0
- package/docs/commands/clear-names.md +27 -0
- package/docs/commands/daemon.md +78 -0
- package/docs/commands/db.md +74 -0
- package/docs/commands/end.md +24 -0
- package/docs/commands/help.md +30 -0
- package/docs/commands/kg.md +89 -0
- package/docs/commands/mcp.md +35 -0
- package/docs/commands/memory.md +125 -0
- package/docs/commands/notify.md +83 -0
- package/docs/commands/observation.md +80 -0
- package/docs/commands/obsidian.md +60 -0
- package/docs/commands/pause.md +35 -0
- package/docs/commands/project.md +345 -0
- package/docs/commands/projects.md +364 -0
- package/docs/commands/registry.md +68 -0
- package/docs/commands/restore.md +31 -0
- package/docs/commands/sessions.md +25 -0
- package/docs/commands/setup.md +20 -0
- package/docs/commands/shell-init.md +18 -0
- package/docs/commands/skill.md +54 -0
- package/docs/commands/topic.md +46 -0
- package/docs/commands/update.md +18 -0
- package/docs/commands/zettel.md +151 -0
- package/docs/mcp-skill-guide.md +292 -0
- package/package.json +4 -2
- package/scripts/build-docs.mjs +371 -0
- package/src/hooks/ts/post-tool-use/observe.ts +43 -0
- package/dist/daemon-CSKj8Xm9.mjs.map +0 -1
- package/dist/db-BtuN768f.mjs +0 -206
- package/dist/db-BtuN768f.mjs.map +0 -1
- package/dist/indexer-D7MvSQPY.mjs +0 -1
- package/dist/kg-extraction-iR1BKKWK.mjs.map +0 -1
- package/dist/sync-CmBKOL3K.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-
|
|
1
|
+
{"version":3,"file":"db-CmYbAVCD.mjs","names":[],"sources":["../src/memory/schema.ts","../src/memory/db.ts"],"sourcesContent":["/**\n * SQLite DDL for the PAI federation database (federation.db).\n *\n * The federation DB is the cross-project search index — a single SQLite file\n * at ~/.pai/federation.db that holds chunked text from every registered\n * project's memory/ and Notes/ directories.\n *\n * Tables:\n * - memory_files — file-level metadata (hash, mtime, size) for change detection\n * - memory_chunks — chunked text with line numbers, tier classification, and optional embedding\n * - memory_fts — FTS5 virtual table backed by memory_chunks text\n *\n * Vault tables (vault_files, vault_aliases, vault_links, vault_name_index, vault_health)\n * have been migrated to Postgres (docker/init.sql) and are no longer created here.\n *\n * Schema version history:\n * v1 — initial schema (BM25 search only)\n * v2 — added embedding BLOB column to memory_chunks (Phase 2.5, vector search)\n * v3 — added vault tables (now removed — vault tables live in Postgres)\n */\n\nimport type { Database } from \"better-sqlite3\";\n\n/** Current schema version. Bump when adding new columns or tables. */\nexport const SCHEMA_VERSION = 5;\n\nexport const FEDERATION_SCHEMA_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n last_accessed_at INTEGER,\n relevance_score REAL DEFAULT 0.5,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n\nCREATE TABLE IF NOT EXISTS kg_entities (\n entity_id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL DEFAULT 'default',\n name TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'unknown',\n description TEXT,\n first_seen INTEGER,\n last_seen INTEGER,\n mention_count INTEGER NOT NULL DEFAULT 1,\n feedback_weight REAL NOT NULL DEFAULT 0.5,\n UNIQUE(tenant_id, entity_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_kge_tenant ON kg_entities(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_kge_name ON kg_entities(tenant_id, name);\nCREATE INDEX IF NOT EXISTS idx_kge_type ON kg_entities(tenant_id, type);\n`;\n\n/**\n * Apply the full federation schema to an open database.\n *\n * Idempotent — all statements use IF NOT EXISTS so calling this on an\n * already-initialised database is safe.\n *\n * Also runs any necessary migrations for existing databases (e.g. adding the\n * embedding column to an older schema that was created without it).\n */\nexport function initializeFederationSchema(db: Database): void {\n db.exec(FEDERATION_SCHEMA_SQL);\n runMigrations(db);\n}\n\n// ---------------------------------------------------------------------------\n// Migrations\n// ---------------------------------------------------------------------------\n\n/**\n * Apply incremental migrations to an existing database.\n *\n * Each migration is idempotent — safe to call on a database that has already\n * been migrated.\n */\nfunction runMigrations(db: Database): void {\n const columns = db.prepare(\"PRAGMA table_info(memory_chunks)\").all() as Array<{\n name: string;\n }>;\n\n // Migration v1→v2: add embedding BLOB column (schema v2, Phase 2.5)\n const hasEmbedding = columns.some((c) => c.name === \"embedding\");\n if (!hasEmbedding) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN embedding BLOB\");\n }\n\n // Create the partial index for embedded chunks (safe now that the column exists)\n db.exec(\n \"CREATE INDEX IF NOT EXISTS idx_mc_embedding ON memory_chunks(id) WHERE embedding IS NOT NULL\",\n );\n\n // Migration v4→v5: add last_accessed_at and relevance_score columns (QW2 + MR2)\n const hasLastAccessedAt = columns.some((c) => c.name === \"last_accessed_at\");\n if (!hasLastAccessedAt) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN last_accessed_at INTEGER\");\n }\n\n const hasRelevanceScore = columns.some((c) => c.name === \"relevance_score\");\n if (!hasRelevanceScore) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN relevance_score REAL DEFAULT 0.5\");\n }\n}\n","/**\n * Database connection helper for the PAI federation DB.\n *\n * Uses better-sqlite3 (synchronous API) to open or create federation.db.\n * On first open it runs the full DDL via initializeFederationSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeFederationSchema } from \"./schema.js\";\n\nexport type { Database };\n\n/** Default federation DB path inside the ~/.pai/ directory. */\nconst DEFAULT_FEDERATION_PATH = join(homedir(), \".pai\", \"federation.db\");\n\n/**\n * Open (or create) the PAI federation database.\n *\n * @param path Absolute path to federation.db. Defaults to ~/.pai/federation.db.\n * @returns An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n * - Creates the parent directory if it does not exist.\n * - Enables WAL journal mode.\n * - Runs initializeFederationSchema() to ensure tables exist.\n */\nexport function openFederation(path: string = DEFAULT_FEDERATION_PATH): Database {\n // Ensure the directory exists before SQLite tries to create the file\n mkdirSync(dirname(path), { recursive: true });\n\n const db = new BetterSqlite3(path);\n\n // WAL gives better concurrent read performance and crash safety\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n\n // Apply schema (idempotent — all statements use IF NOT EXISTS)\n initializeFederationSchema(db);\n\n return db;\n}\n"],"mappings":";;;;;;;AA0BA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0ErC,SAAgB,2BAA2B,IAAoB;AAC7D,IAAG,KAAK,sBAAsB;AAC9B,eAAc,GAAG;;;;;;;;AAanB,SAAS,cAAc,IAAoB;CACzC,MAAM,UAAU,GAAG,QAAQ,mCAAmC,CAAC,KAAK;AAMpE,KAAI,CADiB,QAAQ,MAAM,MAAM,EAAE,SAAS,YAAY,CAE9D,IAAG,KAAK,sDAAsD;AAIhE,IAAG,KACD,+FACD;AAID,KAAI,CADsB,QAAQ,MAAM,MAAM,EAAE,SAAS,mBAAmB,CAE1E,IAAG,KAAK,gEAAgE;AAI1E,KAAI,CADsB,QAAQ,MAAM,MAAM,EAAE,SAAS,kBAAkB,CAEzE,IAAG,KAAK,wEAAwE;;;;;;;;;;;;;AC1HpF,MAAM,0BAA0B,KAAK,SAAS,EAAE,QAAQ,gBAAgB;;;;;;;;;;;;AAaxE,SAAgB,eAAe,OAAe,yBAAmC;AAE/E,WAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;CAE7C,MAAM,KAAK,IAAI,cAAc,KAAK;AAGlC,IAAG,OAAO,qBAAqB;AAC/B,IAAG,OAAO,oBAAoB;AAG9B,4BAA2B,GAAG;AAE9B,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-
|
|
1
|
+
{"version":3,"file":"detect-KjycLtXM.mjs","names":[],"sources":["../src/cli/commands/detect.ts"],"sourcesContent":["/**\n * Project detection logic for PAI.\n *\n * detectProject(cwd) — given a filesystem path, returns the best matching\n * project from the registry:\n * 1. Exact path match\n * 2. Longest parent match (project whose root_path is an ancestor of cwd)\n *\n * Exported for use by the CLI `pai project detect` command and the MCP\n * `project_detect` tool.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { resolve } from \"node:path\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DetectedProject {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n type: string;\n status: string;\n session_count: number;\n last_session_date: string | null;\n match_type: \"exact\" | \"parent\";\n /** Only set when match_type is 'parent' — the portion of cwd below root_path */\n relative_path: string | null;\n}\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n type: string;\n status: string;\n}\n\n// ---------------------------------------------------------------------------\n// Core detection function\n// ---------------------------------------------------------------------------\n\n/**\n * Detect which registered project a filesystem path belongs to.\n *\n * @param db Open registry database\n * @param cwd Absolute path to detect (defaults to process.cwd())\n * @returns The best matching project, or null if no match\n */\nexport function detectProject(\n db: Database,\n cwd?: string\n): DetectedProject | null {\n const target = resolve(cwd ?? process.cwd());\n\n // Load all active projects ordered by root_path length descending\n // so the longest (most specific) match wins in a linear scan.\n const projects = db\n .prepare(\n `SELECT id, slug, display_name, root_path, encoded_dir, type, status\n FROM projects\n WHERE status != 'archived'\n ORDER BY LENGTH(root_path) DESC`\n )\n .all() as ProjectRow[];\n\n let matched: ProjectRow | null = null;\n let matchType: \"exact\" | \"parent\" = \"exact\";\n\n for (const p of projects) {\n const root = resolve(p.root_path);\n if (target === root) {\n matched = p;\n matchType = \"exact\";\n break;\n }\n if (!matched && target.startsWith(root + \"/\")) {\n matched = p;\n matchType = \"parent\";\n // Keep scanning — a longer root_path match might exist (but shouldn't\n // since we sorted by length desc). Safety break anyway once found.\n break;\n }\n }\n\n if (!matched) return null;\n\n // Enrich with session stats\n const sessionStats = db\n .prepare(\n `SELECT COUNT(*) AS cnt, MAX(date) AS last_date\n FROM sessions WHERE project_id = ?`\n )\n .get(matched.id) as { cnt: number; last_date: string | null };\n\n const relative =\n matchType === \"parent\"\n ? target.slice(resolve(matched.root_path).length + 1)\n : null;\n\n return {\n id: matched.id,\n slug: matched.slug,\n display_name: matched.display_name,\n root_path: matched.root_path,\n encoded_dir: matched.encoded_dir,\n type: matched.type,\n status: matched.status,\n session_count: sessionStats.cnt,\n last_session_date: sessionStats.last_date,\n match_type: matchType,\n relative_path: relative,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format a DetectedProject for human-readable CLI output.\n */\nexport function formatDetection(d: DetectedProject): string {\n const lines: string[] = [\n `slug: ${d.slug}`,\n `display_name: ${d.display_name}`,\n `root_path: ${d.root_path}`,\n `type: ${d.type}`,\n `status: ${d.status}`,\n `match: ${d.match_type}${d.relative_path ? ` (+${d.relative_path})` : \"\"}`,\n `sessions: ${d.session_count}`,\n ];\n if (d.last_session_date) {\n lines.push(`last_session: ${d.last_session_date}`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Format a DetectedProject as JSON for machine consumption.\n */\nexport function formatDetectionJson(d: DetectedProject): string {\n return JSON.stringify(\n {\n slug: d.slug,\n display_name: d.display_name,\n root_path: d.root_path,\n encoded_dir: d.encoded_dir,\n type: d.type,\n status: d.status,\n match_type: d.match_type,\n relative_path: d.relative_path,\n session_count: d.session_count,\n last_session_date: d.last_session_date,\n },\n null,\n 2\n );\n}\n"],"mappings":";;;;;;;;;;AAuDA,SAAgB,cACd,IACA,KACwB;CACxB,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAI5C,MAAM,WAAW,GACd,QACC;;;wCAID,CACA,KAAK;CAER,IAAI,UAA6B;CACjC,IAAI,YAAgC;AAEpC,MAAK,MAAM,KAAK,UAAU;EACxB,MAAM,OAAO,QAAQ,EAAE,UAAU;AACjC,MAAI,WAAW,MAAM;AACnB,aAAU;AACV,eAAY;AACZ;;AAEF,MAAI,CAAC,WAAW,OAAO,WAAW,OAAO,IAAI,EAAE;AAC7C,aAAU;AACV,eAAY;AAGZ;;;AAIJ,KAAI,CAAC,QAAS,QAAO;CAGrB,MAAM,eAAe,GAClB,QACC;2CAED,CACA,IAAI,QAAQ,GAAG;CAElB,MAAM,WACJ,cAAc,WACV,OAAO,MAAM,QAAQ,QAAQ,UAAU,CAAC,SAAS,EAAE,GACnD;AAEN,QAAO;EACL,IAAI,QAAQ;EACZ,MAAM,QAAQ;EACd,cAAc,QAAQ;EACtB,WAAW,QAAQ;EACnB,aAAa,QAAQ;EACrB,MAAM,QAAQ;EACd,QAAQ,QAAQ;EAChB,eAAe,aAAa;EAC5B,mBAAmB,aAAa;EAChC,YAAY;EACZ,eAAe;EAChB;;;;;AAUH,SAAgB,gBAAgB,GAA4B;CAC1D,MAAM,QAAkB;EACtB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE,aAAa,EAAE,gBAAgB,MAAM,EAAE,cAAc,KAAK;EAC7E,iBAAiB,EAAE;EACpB;AACD,KAAI,EAAE,kBACJ,OAAM,KAAK,iBAAiB,EAAE,oBAAoB;AAEpD,QAAO,MAAM,KAAK,KAAK;;;;;AAMzB,SAAgB,oBAAoB,GAA4B;AAC9D,QAAO,KAAK,UACV;EACE,MAAM,EAAE;EACR,cAAc,EAAE;EAChB,WAAW,EAAE;EACb,aAAa,EAAE;EACf,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,YAAY,EAAE;EACd,eAAe,EAAE;EACjB,eAAe,EAAE;EACjB,mBAAmB,EAAE;EACtB,EACD,MACA,EACD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
-
import { n as populateSlugs, r as searchMemory } from "./search-
|
|
2
|
+
import { n as populateSlugs, r as searchMemory } from "./search-HcdKtMla.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/topics/detector.ts
|
|
5
5
|
var detector_exports = /* @__PURE__ */ __exportAll({ detectTopicShift: () => detectTopicShift });
|
|
@@ -71,4 +71,4 @@ async function detectTopicShift(registryDb, federation, params) {
|
|
|
71
71
|
|
|
72
72
|
//#endregion
|
|
73
73
|
export { detector_exports as n, detectTopicShift as t };
|
|
74
|
-
//# sourceMappingURL=detector-
|
|
74
|
+
//# sourceMappingURL=detector-ZiOhHszd.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detector-
|
|
1
|
+
{"version":3,"file":"detector-ZiOhHszd.mjs","names":[],"sources":["../src/topics/detector.ts"],"sourcesContent":["/**\n * Topic shift detection engine.\n *\n * Accepts a context summary (recent conversation text) and determines whether\n * the conversation has drifted away from the currently-routed project.\n *\n * Algorithm:\n * 1. Run keyword memory_search against the context text (no project filter)\n * 2. Score results by project — sum of BM25 scores per project\n * 3. Compare the top-scoring project against the current project\n * 4. If a different project dominates by more than the confidence threshold,\n * report a topic shift.\n *\n * Design decisions:\n * - Keyword search only (no semantic) — fast, no embedding requirement\n * - Works with or without an active daemon (direct DB access path)\n * - Stateless: callers supply currentProject; detector has no session memory\n * - Minimal: returns a plain result object, not MCP content arrays\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { searchMemory, populateSlugs } from \"../memory/search.js\";\nimport type { SearchResult } from \"../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicCheckParams {\n /** Recent conversation context (a few sentences or tool call summaries) */\n context: string;\n /** The project slug the session is currently routed to. May be null/empty. */\n currentProject?: string;\n /**\n * Minimum confidence [0,1] to declare a shift. Default: 0.6.\n * Higher = less sensitive, fewer false positives.\n */\n threshold?: number;\n /**\n * Maximum results to draw from memory search (candidates). Default: 20.\n * More candidates = more accurate scoring, slightly slower.\n */\n candidates?: number;\n}\n\nexport interface TopicCheckResult {\n /** Whether a significant topic shift was detected. */\n shifted: boolean;\n /** The project slug the session is currently routed to (echoed from input). */\n currentProject: string | null;\n /** The project slug that best matches the context, or null if no clear match. */\n suggestedProject: string | null;\n /**\n * Confidence score for the suggested project [0,1].\n * Represents the fraction of total score mass held by the top project.\n * 1.0 = all matching chunks belong to one project.\n * 0.5 = two projects are equally matched.\n */\n confidence: number;\n /** Number of memory chunks that contributed to scoring. */\n chunkCount: number;\n /** Top-3 scoring projects with their normalised scores (for debugging). */\n topProjects: Array<{ slug: string; score: number }>;\n}\n\n// ---------------------------------------------------------------------------\n// Core algorithm\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the provided context text best matches a different project\n * than the session's current routing.\n *\n * Works with either a raw SQLite Database or a StorageBackend.\n * For the StorageBackend path, keyword search is used.\n * For the raw Database path (legacy/direct), searchMemory() is called.\n */\nexport async function detectTopicShift(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: TopicCheckParams\n): Promise<TopicCheckResult> {\n const threshold = params.threshold ?? 0.6;\n const candidates = params.candidates ?? 20;\n const currentProject = params.currentProject?.trim() || null;\n\n if (!params.context || params.context.trim().length === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: 0,\n topProjects: [],\n };\n }\n\n // -------------------------------------------------------------------------\n // Run memory search across ALL projects (no project filter)\n // -------------------------------------------------------------------------\n\n let results: SearchResult[];\n\n const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n \"backendType\" in x;\n\n if (isBackend(federation)) {\n results = await federation.searchKeyword(params.context, {\n maxResults: candidates,\n });\n } else {\n results = searchMemory(federation, params.context, {\n maxResults: candidates,\n });\n }\n\n if (results.length === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: 0,\n topProjects: [],\n };\n }\n\n // Populate project slugs from the registry\n const withSlugs = populateSlugs(results, registryDb);\n\n // -------------------------------------------------------------------------\n // Score projects by summing BM25 scores of matching chunks\n // -------------------------------------------------------------------------\n\n const projectScores = new Map<string, number>();\n\n for (const r of withSlugs) {\n const slug = r.projectSlug;\n if (!slug) continue;\n projectScores.set(slug, (projectScores.get(slug) ?? 0) + r.score);\n }\n\n if (projectScores.size === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: withSlugs.length,\n topProjects: [],\n };\n }\n\n // Sort by total score descending\n const ranked = Array.from(projectScores.entries())\n .sort((a, b) => b[1] - a[1]);\n\n const totalScore = ranked.reduce((sum, [, s]) => sum + s, 0);\n\n // Top-3 for reporting (normalised to [0,1] fraction of total mass)\n const topProjects = ranked.slice(0, 3).map(([slug, score]) => ({\n slug,\n score: totalScore > 0 ? score / totalScore : 0,\n }));\n\n const topSlug = ranked[0][0];\n const topRawScore = ranked[0][1];\n const confidence = totalScore > 0 ? topRawScore / totalScore : 0;\n\n // -------------------------------------------------------------------------\n // Determine if a shift occurred\n // -------------------------------------------------------------------------\n\n // A shift is detected when:\n // 1. confidence >= threshold (the top project dominates)\n // 2. The top project is different from currentProject\n // 3. There is a currentProject to compare against\n // (if no current project, we still return the best match but no \"shift\")\n\n const isDifferent =\n currentProject !== null &&\n topSlug !== currentProject;\n\n const shifted = isDifferent && confidence >= threshold;\n\n return {\n shifted,\n currentProject,\n suggestedProject: topSlug,\n confidence,\n chunkCount: withSlugs.length,\n topProjects,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA8EA,eAAsB,iBACpB,YACA,YACA,QAC2B;CAC3B,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,iBAAiB,OAAO,gBAAgB,MAAM,IAAI;AAExD,KAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,MAAM,CAAC,WAAW,EACtD,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY;EACZ,aAAa,EAAE;EAChB;CAOH,IAAI;CAEJ,MAAM,aAAa,MACjB,iBAAiB;AAEnB,KAAI,UAAU,WAAW,CACvB,WAAU,MAAM,WAAW,cAAc,OAAO,SAAS,EACvD,YAAY,YACb,CAAC;KAEF,WAAU,aAAa,YAAY,OAAO,SAAS,EACjD,YAAY,YACb,CAAC;AAGJ,KAAI,QAAQ,WAAW,EACrB,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY;EACZ,aAAa,EAAE;EAChB;CAIH,MAAM,YAAY,cAAc,SAAS,WAAW;CAMpD,MAAM,gCAAgB,IAAI,KAAqB;AAE/C,MAAK,MAAM,KAAK,WAAW;EACzB,MAAM,OAAO,EAAE;AACf,MAAI,CAAC,KAAM;AACX,gBAAc,IAAI,OAAO,cAAc,IAAI,KAAK,IAAI,KAAK,EAAE,MAAM;;AAGnE,KAAI,cAAc,SAAS,EACzB,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY,UAAU;EACtB,aAAa,EAAE;EAChB;CAIH,MAAM,SAAS,MAAM,KAAK,cAAc,SAAS,CAAC,CAC/C,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG;CAE9B,MAAM,aAAa,OAAO,QAAQ,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE;CAG5D,MAAM,cAAc,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,YAAY;EAC7D;EACA,OAAO,aAAa,IAAI,QAAQ,aAAa;EAC9C,EAAE;CAEH,MAAM,UAAU,OAAO,GAAG;CAC1B,MAAM,cAAc,OAAO,GAAG;CAC9B,MAAM,aAAa,aAAa,IAAI,cAAc,aAAa;AAkB/D,QAAO;EACL,SANA,mBAAmB,QACnB,YAAY,kBAEiB,cAAc;EAI3C;EACA,kBAAkB;EAClB;EACA,YAAY,UAAU;EACtB;EACD"}
|
|
@@ -88,4 +88,4 @@ function cosineSimilarity(a, b) {
|
|
|
88
88
|
|
|
89
89
|
//#endregion
|
|
90
90
|
export { generateEmbedding as a, embeddings_exports as i, cosineSimilarity as n, deserializeEmbedding as r, configureEmbeddingModel as t };
|
|
91
|
-
//# sourceMappingURL=embeddings-
|
|
91
|
+
//# sourceMappingURL=embeddings-BJPOcbik.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embeddings-
|
|
1
|
+
{"version":3,"file":"embeddings-BJPOcbik.mjs","names":[],"sources":["../src/memory/embeddings.ts"],"sourcesContent":["/**\n * Embedding generation for the PAI federation memory engine (Phase 2.5).\n *\n * Uses @huggingface/transformers with the Snowflake/snowflake-arctic-embed-m-v1.5 model\n * (768 dims, q8 quantization, MTEB strong retrieval quality).\n *\n * The model uses CLS pooling (first token) — NOT mean pooling.\n * For retrieval, queries require a prefix: \"Represent this sentence for searching relevant passages: \"\n * Documents should be embedded WITHOUT a prefix.\n *\n * The pipeline is a lazy singleton — loaded on first call, reused thereafter.\n * This avoids loading the heavy ML model on every CLI invocation.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const EMBEDDING_DIM = 768;\nconst DEFAULT_EMBEDDING_MODEL = \"Snowflake/snowflake-arctic-embed-m-v1.5\";\n\n/** Query prefix required by Snowflake Arctic Embed for retrieval tasks. */\nconst QUERY_PREFIX = \"Represent this sentence for searching relevant passages: \";\n\n// ---------------------------------------------------------------------------\n// Lazy pipeline singleton\n// ---------------------------------------------------------------------------\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _embeddingPipeline: any = null;\nlet _currentModel: string | null = null;\n\n/**\n * Configure the embedding model to use.\n * Must be called before the first generateEmbedding() call.\n * If the pipeline is already loaded with a different model, it will be reloaded.\n *\n * @param model HuggingFace model ID (e.g. \"Snowflake/snowflake-arctic-embed-m-v1.5\").\n * Pass undefined or empty string to use the default model.\n */\nexport function configureEmbeddingModel(model?: string): void {\n const resolved = model?.trim() || DEFAULT_EMBEDDING_MODEL;\n if (_currentModel !== null && _currentModel !== resolved) {\n // Model changed — force reload on next call\n _embeddingPipeline = null;\n }\n _currentModel = resolved;\n}\n\nasync function getEmbedder() {\n const model = _currentModel ?? DEFAULT_EMBEDDING_MODEL;\n if (!_embeddingPipeline) {\n // Dynamic import to avoid loading the ML runtime on startup\n const { pipeline } = await import(\"@huggingface/transformers\");\n _embeddingPipeline = await pipeline(\n \"feature-extraction\",\n model,\n { dtype: \"q8\" },\n );\n }\n return _embeddingPipeline;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a normalized 768-dim embedding for the given text.\n *\n * Uses CLS pooling (first token) and L2 normalization (cosine similarity ready).\n *\n * @param text The text to embed.\n * @param isQuery If true, prepend the Snowflake query prefix. Use for search queries.\n * Documents should be embedded without the prefix (default: false).\n */\nexport async function generateEmbedding(text: string, isQuery: boolean = false): Promise<Float32Array> {\n const prefix = isQuery ? QUERY_PREFIX : \"\";\n const input = prefix + text;\n const extractor = await getEmbedder();\n // Snowflake Arctic Embed uses CLS pooling (first token), not mean pooling\n const output = await extractor(input, { pooling: \"cls\", normalize: true });\n return new Float32Array(output.data);\n}\n\n// ---------------------------------------------------------------------------\n// Serialization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a Float32Array to a Buffer for storage in a SQLite BLOB column.\n */\nexport function serializeEmbedding(vec: Float32Array): Buffer {\n return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);\n}\n\n/**\n * Deserialize a Buffer (from a SQLite BLOB column) back into a Float32Array.\n */\nexport function deserializeEmbedding(blob: Buffer): Float32Array {\n return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);\n}\n\n// ---------------------------------------------------------------------------\n// Similarity computation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute cosine similarity between two normalized embedding vectors.\n *\n * Since both vectors are already L2-normalized by the embedding model,\n * cosine similarity reduces to a dot product — but we compute the full\n * formula for correctness when embeddings may not be pre-normalized.\n *\n * Returns a value in [-1, 1] where 1 = identical.\n */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n const denom = Math.sqrt(normA) * Math.sqrt(normB);\n if (denom === 0) return 0;\n return dot / denom;\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,0BAA0B;;AAGhC,MAAM,eAAe;AAOrB,IAAI,qBAA0B;AAC9B,IAAI,gBAA+B;;;;;;;;;AAUnC,SAAgB,wBAAwB,OAAsB;CAC5D,MAAM,WAAW,OAAO,MAAM,IAAI;AAClC,KAAI,kBAAkB,QAAQ,kBAAkB,SAE9C,sBAAqB;AAEvB,iBAAgB;;AAGlB,eAAe,cAAc;CAC3B,MAAM,QAAQ,iBAAiB;AAC/B,KAAI,CAAC,oBAAoB;EAEvB,MAAM,EAAE,aAAa,MAAM,OAAO;AAClC,uBAAqB,MAAM,SACzB,sBACA,OACA,EAAE,OAAO,MAAM,CAChB;;AAEH,QAAO;;;;;;;;;;;AAgBT,eAAsB,kBAAkB,MAAc,UAAmB,OAA8B;CAErG,MAAM,SADS,UAAU,eAAe,MACjB;CAGvB,MAAM,SAAS,OAFG,MAAM,aAAa,EAEN,OAAO;EAAE,SAAS;EAAO,WAAW;EAAM,CAAC;AAC1E,QAAO,IAAI,aAAa,OAAO,KAAK;;;;;AAUtC,SAAgB,mBAAmB,KAA2B;AAC5D,QAAO,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,WAAW;;;;;AAMhE,SAAgB,qBAAqB,MAA4B;AAC/D,QAAO,IAAI,aAAa,KAAK,QAAQ,KAAK,YAAY,KAAK,aAAa,EAAE;;;;;;;;;;;AAgB5E,SAAgB,iBAAiB,GAAiB,GAAyB;CACzE,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAO,EAAE,KAAK,EAAE;AAChB,WAAS,EAAE,KAAK,EAAE;AAClB,WAAS,EAAE,KAAK,EAAE;;CAEpB,MAAM,QAAQ,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK,MAAM;AACjD,KAAI,UAAU,EAAG,QAAO;AACxB,QAAO,MAAM"}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
-
|
|
3
1
|
//#region src/storage/factory.ts
|
|
4
|
-
var factory_exports = /* @__PURE__ */ __exportAll({ createStorageBackend: () => createStorageBackend });
|
|
5
2
|
/**
|
|
6
3
|
* Create and return the configured StorageBackend.
|
|
7
4
|
*
|
|
@@ -15,7 +12,7 @@ async function createStorageBackend(config) {
|
|
|
15
12
|
}
|
|
16
13
|
async function tryPostgres(config) {
|
|
17
14
|
try {
|
|
18
|
-
const { PostgresBackend } = await import("./postgres-
|
|
15
|
+
const { PostgresBackend } = await import("./postgres-B451Hnkm.mjs");
|
|
19
16
|
const pgConfig = config.postgres ?? {};
|
|
20
17
|
await PostgresBackend.ensureDatabase(pgConfig);
|
|
21
18
|
const backend = new PostgresBackend(pgConfig);
|
|
@@ -34,11 +31,11 @@ async function tryPostgres(config) {
|
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
33
|
async function createSQLiteBackend() {
|
|
37
|
-
const { openFederation } = await import("./db-
|
|
38
|
-
const { SQLiteBackend } = await import("./sqlite-
|
|
34
|
+
const { openFederation } = await import("./db-CmYbAVCD.mjs").then((n) => n.t);
|
|
35
|
+
const { SQLiteBackend } = await import("./sqlite-DQpY1Esi.mjs");
|
|
39
36
|
return new SQLiteBackend(openFederation());
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
//#endregion
|
|
43
|
-
export {
|
|
44
|
-
//# sourceMappingURL=factory-
|
|
40
|
+
export { createStorageBackend as t };
|
|
41
|
+
//# sourceMappingURL=factory-CayQCMoN.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-
|
|
1
|
+
{"version":3,"file":"factory-CayQCMoN.mjs","names":[],"sources":["../src/storage/factory.ts"],"sourcesContent":["/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n * If Postgres is configured but unavailable, falls back to SQLite with\n * a warning log — the daemon never crashes due to a missing Postgres.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-fallback behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend if reachable, else SQLiteBackend\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await tryPostgres(config);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\nasync function tryPostgres(config: PaiDaemonConfig): Promise<StorageBackend> {\n try {\n const { PostgresBackend } = await import(\"./postgres.js\");\n const pgConfig = config.postgres ?? {};\n\n // Ensure the per-user database exists and has the schema applied\n await PostgresBackend.ensureDatabase(pgConfig);\n\n const backend = new PostgresBackend(pgConfig);\n\n const err = await backend.testConnection();\n if (err) {\n process.stderr.write(\n `[pai-daemon] Postgres unavailable (${err}). Falling back to SQLite.\\n`\n );\n await backend.close();\n return createSQLiteBackend();\n }\n\n process.stderr.write(\"[pai-daemon] Connected to PostgreSQL backend.\\n\");\n return backend;\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Postgres init error (${msg}). Falling back to SQLite.\\n`\n );\n return createSQLiteBackend();\n }\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const { openFederation } = await import(\"../memory/db.js\");\n const { SQLiteBackend } = await import(\"./sqlite.js\");\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n"],"mappings":";;;;;;;;AAkBA,eAAsB,qBACpB,QACyB;AACzB,KAAI,OAAO,mBAAmB,WAC5B,QAAO,MAAM,YAAY,OAAO;AAIlC,QAAO,qBAAqB;;AAG9B,eAAe,YAAY,QAAkD;AAC3E,KAAI;EACF,MAAM,EAAE,oBAAoB,MAAM,OAAO;EACzC,MAAM,WAAW,OAAO,YAAY,EAAE;AAGtC,QAAM,gBAAgB,eAAe,SAAS;EAE9C,MAAM,UAAU,IAAI,gBAAgB,SAAS;EAE7C,MAAM,MAAM,MAAM,QAAQ,gBAAgB;AAC1C,MAAI,KAAK;AACP,WAAQ,OAAO,MACb,sCAAsC,IAAI,8BAC3C;AACD,SAAM,QAAQ,OAAO;AACrB,UAAO,qBAAqB;;AAG9B,UAAQ,OAAO,MAAM,kDAAkD;AACvE,SAAO;UACA,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MACb,qCAAqC,IAAI,8BAC1C;AACD,SAAO,qBAAqB;;;AAIhC,eAAe,sBAA+C;CAC5D,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cADA,gBAAgB,CACC"}
|
|
@@ -425,4 +425,4 @@ const INDEX_YIELD_EVERY = 10;
|
|
|
425
425
|
|
|
426
426
|
//#endregion
|
|
427
427
|
export { parseSessionTitleChunk as a, yieldToEventLoop as c, sha256 as d, sha256File as f, isPathTooBroadForContentScan as i, chunkMarkdown as l, chunkId as n, walkContentFiles as o, detectTier as r, walkMdFiles as s, INDEX_YIELD_EVERY as t, estimateTokens as u };
|
|
428
|
-
//# sourceMappingURL=helpers-
|
|
428
|
+
//# sourceMappingURL=helpers-IjZkXBhj.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers-crDEr6S2.mjs","names":[],"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 { createHash } from \"node:crypto\";\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":"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"}
|
|
1
|
+
{"version":3,"file":"helpers-IjZkXBhj.mjs","names":[],"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 { createHash } from \"node:crypto\";\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":"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"}
|
package/dist/hooks/observe.mjs
CHANGED
|
@@ -318,6 +318,26 @@ function classify(toolName, toolInput) {
|
|
|
318
318
|
concepts
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
|
+
function extractSkillInvocation(toolName, toolInput) {
|
|
322
|
+
if (toolName === "Skill") {
|
|
323
|
+
return str(toolInput.skill) || null;
|
|
324
|
+
}
|
|
325
|
+
if (toolName === "Bash") {
|
|
326
|
+
const cmd = str(toolInput.command);
|
|
327
|
+
const m = cmd.match(/skill[-]?workflow[-]?notification\s+(\S+)\s+(\S+)/i);
|
|
328
|
+
if (m) return m[2];
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
async function maybeRecordSkill(hookData) {
|
|
333
|
+
const skill = extractSkillInvocation(hookData.tool_name, hookData.tool_input);
|
|
334
|
+
if (!skill) return;
|
|
335
|
+
await sendToDaemon("skill_telemetry_record", {
|
|
336
|
+
skill_name: skill,
|
|
337
|
+
source: "local",
|
|
338
|
+
cwd: hookData.cwd ?? ""
|
|
339
|
+
});
|
|
340
|
+
}
|
|
321
341
|
async function main() {
|
|
322
342
|
try {
|
|
323
343
|
const chunks = [];
|
|
@@ -328,6 +348,7 @@ async function main() {
|
|
|
328
348
|
if (!raw) process.exit(0);
|
|
329
349
|
const hookData = JSON.parse(raw);
|
|
330
350
|
if (isProbeSession(hookData.cwd)) process.exit(0);
|
|
351
|
+
await maybeRecordSkill(hookData);
|
|
331
352
|
if (SKIP_TOOLS.has(hookData.tool_name)) process.exit(0);
|
|
332
353
|
const obs = classify(hookData.tool_name, hookData.tool_input);
|
|
333
354
|
if (!obs) process.exit(0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/ts/post-tool-use/observe.ts", "../../src/utils/hash.ts", "../../src/hooks/ts/lib/project-utils/paths.ts", "../../src/hooks/ts/lib/pai-paths.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n/**\n * PostToolUse Hook - Observation Capture\n *\n * Classifies each tool call as a structured observation and sends it to the\n * PAI daemon via IPC. Fire-and-forget with a 5-second timeout. Never blocks\n * Claude Code (always exits 0).\n */\n\nimport { connect } from 'net';\nimport { sha256 } from '../../../utils/hash.js';\nimport { basename } from 'path';\nimport { isProbeSession } from '../lib/project-utils.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HookData {\n session_id: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n tool_response?: unknown;\n cwd?: string;\n}\n\ntype ObservationType = 'change' | 'discovery' | 'decision' | 'feature';\n\ninterface Observation {\n type: ObservationType;\n title: string;\n narrative: string;\n tool_name: string;\n tool_input_summary: string;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Tools to skip entirely\n// ---------------------------------------------------------------------------\n\nconst SKIP_TOOLS = new Set([\n 'ToolSearch',\n 'AskUserQuestion',\n 'EnterPlanMode',\n 'ExitPlanMode',\n 'Skill',\n]);\n\n// ---------------------------------------------------------------------------\n// Inline IPC sender \u2014 avoids importing src/daemon/ipc-client.ts at build time\n// ---------------------------------------------------------------------------\n\nfunction sendToDaemon(method: string, params: Record<string, unknown>): Promise<void> {\n return new Promise((resolve) => {\n const timeout = setTimeout(() => { resolve(); }, 5000);\n try {\n const socket = connect('/tmp/pai.sock', () => {\n const req = JSON.stringify({ id: Date.now().toString(), method, params }) + '\\n';\n socket.write(req);\n socket.on('data', () => { clearTimeout(timeout); socket.destroy(); resolve(); });\n socket.on('error', () => { clearTimeout(timeout); resolve(); });\n });\n socket.on('error', () => { clearTimeout(timeout); resolve(); });\n } catch {\n clearTimeout(timeout);\n resolve();\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Concept extraction from file paths\n// ---------------------------------------------------------------------------\n\nconst SKIP_SEGMENTS = new Set([\n 'src', 'dist', 'lib', 'bin', 'test', 'tests', 'spec', 'specs',\n 'node_modules', 'Users', 'home', 'usr', 'var', 'tmp', 'etc',\n 'hooks', 'scripts', 'config', 'configs', 'assets', 'static',\n 'public', 'private', 'build', 'out', 'output', 'generated',\n 'ts', 'js', 'mjs', 'cjs',\n]);\n\nfunction extractConcepts(paths: string[]): string[] {\n const concepts = new Set<string>();\n for (const p of paths) {\n const segments = p.split('/').filter(Boolean);\n for (const seg of segments) {\n // Drop extensions\n const clean = seg.replace(/\\.[^.]+$/, '');\n if (clean.length > 2 && !SKIP_SEGMENTS.has(clean) && !/^\\d+$/.test(clean)) {\n concepts.add(clean);\n }\n }\n }\n return Array.from(concepts).slice(0, 10);\n}\n\n// ---------------------------------------------------------------------------\n// Inline classifier\n// ---------------------------------------------------------------------------\n\nfunction str(v: unknown): string {\n if (typeof v === 'string') return v;\n return '';\n}\n\nfunction truncate(s: string, len: number): string {\n return s.length > len ? s.slice(0, len) : s;\n}\n\nfunction classify(toolName: string, toolInput: Record<string, unknown>): Observation | null {\n if (SKIP_TOOLS.has(toolName)) return null;\n\n let type: ObservationType = 'discovery';\n let title = '';\n let narrative = '';\n let tool_input_summary = '';\n const files_read: string[] = [];\n const files_modified: string[] = [];\n\n switch (toolName) {\n case 'Edit':\n case 'MultiEdit': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'change';\n title = `Modified ${name}`;\n narrative = fp ? `Edited ${fp}` : 'Edited a file';\n tool_input_summary = fp;\n files_modified.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Write':\n case 'NotebookEdit': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'change';\n title = `Created ${name}`;\n narrative = fp ? `Wrote ${fp}` : 'Wrote a file';\n tool_input_summary = fp;\n files_modified.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Read': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'discovery';\n title = `Read ${name}`;\n narrative = fp ? `Read ${fp}` : 'Read a file';\n tool_input_summary = fp;\n files_read.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Grep': {\n const pattern = str(toolInput.pattern);\n type = 'discovery';\n title = `Searched for '${truncate(pattern, 40)}'`;\n narrative = `Grep search: ${pattern}`;\n tool_input_summary = pattern;\n const gPath = str(toolInput.path || toolInput.file_path);\n if (gPath) files_read.push(gPath);\n break;\n }\n\n case 'Glob': {\n const pattern = str(toolInput.pattern);\n type = 'discovery';\n title = `Found files: ${truncate(pattern, 40)}`;\n narrative = `Glob pattern: ${pattern}`;\n tool_input_summary = pattern;\n break;\n }\n\n case 'Bash': {\n const cmd = str(toolInput.command);\n const cmdLower = cmd.toLowerCase();\n\n if (/git\\s+commit/.test(cmdLower)) {\n type = 'decision';\n // Try to extract the commit message after -m \"...\"\n const mMatch = cmd.match(/-m\\s+[\"']([^\"']+)/);\n const msg = mMatch ? mMatch[1] : cmd;\n title = `Committed: ${truncate(msg, 60)}`;\n narrative = `Git commit: ${msg}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/git\\s+push/.test(cmdLower)) {\n type = 'decision';\n title = 'Pushed to remote';\n narrative = `Git push: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/\\b(jest|vitest|pytest|bun\\s+test|npm\\s+test|yarn\\s+test|pnpm\\s+test|node\\s+--test)\\b/.test(cmdLower)) {\n type = 'feature';\n title = 'Ran tests';\n narrative = `Test run: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/\\b(build|compile|bun\\s+run\\s+build|tsc|esbuild|webpack|vite\\s+build)\\b/.test(cmdLower)) {\n type = 'feature';\n title = 'Built project';\n narrative = `Build: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else {\n type = 'discovery';\n title = `Ran: ${truncate(cmd, 60)}`;\n narrative = `Bash: ${truncate(cmd, 120)}`;\n tool_input_summary = truncate(cmd, 120);\n }\n break;\n }\n\n case 'Task': {\n const prompt = str(toolInput.prompt || toolInput.description);\n type = 'discovery';\n title = `Delegated: ${truncate(prompt, 60)}`;\n narrative = `Spawned agent: ${truncate(prompt, 200)}`;\n tool_input_summary = truncate(prompt, 200);\n break;\n }\n\n case 'WebFetch': {\n const url = str(toolInput.url);\n type = 'discovery';\n title = `Fetched: ${truncate(url, 60)}`;\n narrative = `Web fetch: ${url}`;\n tool_input_summary = url;\n break;\n }\n\n case 'WebSearch': {\n const query = str(toolInput.query);\n type = 'discovery';\n title = `Searched web: ${truncate(query, 60)}`;\n narrative = `Web search: ${query}`;\n tool_input_summary = query;\n break;\n }\n\n default: {\n // mcp__* tools and anything else\n if (toolName.startsWith('mcp__')) {\n type = 'discovery';\n title = `MCP: ${toolName}`;\n narrative = `Called MCP tool ${toolName}`;\n tool_input_summary = toolName;\n } else {\n // Generic fallback \u2014 still capture rather than skip\n type = 'discovery';\n title = `Tool: ${toolName}`;\n narrative = `Called ${toolName}`;\n tool_input_summary = JSON.stringify(toolInput).slice(0, 120);\n }\n break;\n }\n }\n\n const allPaths = [...files_read, ...files_modified];\n const concepts = extractConcepts(allPaths);\n\n return {\n type,\n title,\n narrative,\n tool_name: toolName,\n tool_input_summary,\n files_read,\n files_modified,\n concepts,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n try {\n // Read stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n const raw = Buffer.concat(chunks).toString('utf-8').trim();\n if (!raw) process.exit(0);\n\n const hookData: HookData = JSON.parse(raw);\n\n // Skip probe/health-check sessions\n if (isProbeSession(hookData.cwd)) process.exit(0);\n\n // Skip uninteresting tools\n if (SKIP_TOOLS.has(hookData.tool_name)) process.exit(0);\n\n // Classify\n const obs = classify(hookData.tool_name, hookData.tool_input);\n if (!obs) process.exit(0);\n\n // Content-hash dedup key\n const hash = sha256(hookData.session_id + hookData.tool_name + obs.title).slice(0, 16);\n\n // Fire-and-forget to daemon\n await sendToDaemon('observation_store', {\n session_id: hookData.session_id,\n type: obs.type,\n title: obs.title,\n narrative: obs.narrative,\n tool_name: obs.tool_name,\n tool_input_summary: obs.tool_input_summary,\n files_read: obs.files_read,\n files_modified: obs.files_modified,\n concepts: obs.concepts,\n content_hash: hash,\n cwd: hookData.cwd ?? '',\n });\n\n process.exit(0);\n } catch {\n // Never block Claude Code\n process.exit(0);\n }\n}\n\nmain();\n", "/**\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 * Path utilities \u2014 encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory.\n * Returns the number of files moved.\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n try {\n renameSync(sourcePath, destPath);\n if (!silent) console.error(`Moved ${file} \u2192 sessions/`);\n movedCount++;\n } catch (error) {\n if (!silent) console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n\n return movedCount;\n}\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n/**\n * PostToolUse Hook - Observation Capture\n *\n * Classifies each tool call as a structured observation and sends it to the\n * PAI daemon via IPC. Fire-and-forget with a 5-second timeout. Never blocks\n * Claude Code (always exits 0).\n */\n\nimport { connect } from 'net';\nimport { sha256 } from '../../../utils/hash.js';\nimport { basename } from 'path';\nimport { isProbeSession } from '../lib/project-utils.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HookData {\n session_id: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n tool_response?: unknown;\n cwd?: string;\n}\n\ntype ObservationType = 'change' | 'discovery' | 'decision' | 'feature';\n\ninterface Observation {\n type: ObservationType;\n title: string;\n narrative: string;\n tool_name: string;\n tool_input_summary: string;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Tools to skip entirely\n// ---------------------------------------------------------------------------\n\nconst SKIP_TOOLS = new Set([\n 'ToolSearch',\n 'AskUserQuestion',\n 'EnterPlanMode',\n 'ExitPlanMode',\n 'Skill',\n]);\n\n// ---------------------------------------------------------------------------\n// Inline IPC sender \u2014 avoids importing src/daemon/ipc-client.ts at build time\n// ---------------------------------------------------------------------------\n\nfunction sendToDaemon(method: string, params: Record<string, unknown>): Promise<void> {\n return new Promise((resolve) => {\n const timeout = setTimeout(() => { resolve(); }, 5000);\n try {\n const socket = connect('/tmp/pai.sock', () => {\n const req = JSON.stringify({ id: Date.now().toString(), method, params }) + '\\n';\n socket.write(req);\n socket.on('data', () => { clearTimeout(timeout); socket.destroy(); resolve(); });\n socket.on('error', () => { clearTimeout(timeout); resolve(); });\n });\n socket.on('error', () => { clearTimeout(timeout); resolve(); });\n } catch {\n clearTimeout(timeout);\n resolve();\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Concept extraction from file paths\n// ---------------------------------------------------------------------------\n\nconst SKIP_SEGMENTS = new Set([\n 'src', 'dist', 'lib', 'bin', 'test', 'tests', 'spec', 'specs',\n 'node_modules', 'Users', 'home', 'usr', 'var', 'tmp', 'etc',\n 'hooks', 'scripts', 'config', 'configs', 'assets', 'static',\n 'public', 'private', 'build', 'out', 'output', 'generated',\n 'ts', 'js', 'mjs', 'cjs',\n]);\n\nfunction extractConcepts(paths: string[]): string[] {\n const concepts = new Set<string>();\n for (const p of paths) {\n const segments = p.split('/').filter(Boolean);\n for (const seg of segments) {\n // Drop extensions\n const clean = seg.replace(/\\.[^.]+$/, '');\n if (clean.length > 2 && !SKIP_SEGMENTS.has(clean) && !/^\\d+$/.test(clean)) {\n concepts.add(clean);\n }\n }\n }\n return Array.from(concepts).slice(0, 10);\n}\n\n// ---------------------------------------------------------------------------\n// Inline classifier\n// ---------------------------------------------------------------------------\n\nfunction str(v: unknown): string {\n if (typeof v === 'string') return v;\n return '';\n}\n\nfunction truncate(s: string, len: number): string {\n return s.length > len ? s.slice(0, len) : s;\n}\n\nfunction classify(toolName: string, toolInput: Record<string, unknown>): Observation | null {\n if (SKIP_TOOLS.has(toolName)) return null;\n\n let type: ObservationType = 'discovery';\n let title = '';\n let narrative = '';\n let tool_input_summary = '';\n const files_read: string[] = [];\n const files_modified: string[] = [];\n\n switch (toolName) {\n case 'Edit':\n case 'MultiEdit': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'change';\n title = `Modified ${name}`;\n narrative = fp ? `Edited ${fp}` : 'Edited a file';\n tool_input_summary = fp;\n files_modified.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Write':\n case 'NotebookEdit': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'change';\n title = `Created ${name}`;\n narrative = fp ? `Wrote ${fp}` : 'Wrote a file';\n tool_input_summary = fp;\n files_modified.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Read': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'discovery';\n title = `Read ${name}`;\n narrative = fp ? `Read ${fp}` : 'Read a file';\n tool_input_summary = fp;\n files_read.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Grep': {\n const pattern = str(toolInput.pattern);\n type = 'discovery';\n title = `Searched for '${truncate(pattern, 40)}'`;\n narrative = `Grep search: ${pattern}`;\n tool_input_summary = pattern;\n const gPath = str(toolInput.path || toolInput.file_path);\n if (gPath) files_read.push(gPath);\n break;\n }\n\n case 'Glob': {\n const pattern = str(toolInput.pattern);\n type = 'discovery';\n title = `Found files: ${truncate(pattern, 40)}`;\n narrative = `Glob pattern: ${pattern}`;\n tool_input_summary = pattern;\n break;\n }\n\n case 'Bash': {\n const cmd = str(toolInput.command);\n const cmdLower = cmd.toLowerCase();\n\n if (/git\\s+commit/.test(cmdLower)) {\n type = 'decision';\n // Try to extract the commit message after -m \"...\"\n const mMatch = cmd.match(/-m\\s+[\"']([^\"']+)/);\n const msg = mMatch ? mMatch[1] : cmd;\n title = `Committed: ${truncate(msg, 60)}`;\n narrative = `Git commit: ${msg}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/git\\s+push/.test(cmdLower)) {\n type = 'decision';\n title = 'Pushed to remote';\n narrative = `Git push: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/\\b(jest|vitest|pytest|bun\\s+test|npm\\s+test|yarn\\s+test|pnpm\\s+test|node\\s+--test)\\b/.test(cmdLower)) {\n type = 'feature';\n title = 'Ran tests';\n narrative = `Test run: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/\\b(build|compile|bun\\s+run\\s+build|tsc|esbuild|webpack|vite\\s+build)\\b/.test(cmdLower)) {\n type = 'feature';\n title = 'Built project';\n narrative = `Build: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else {\n type = 'discovery';\n title = `Ran: ${truncate(cmd, 60)}`;\n narrative = `Bash: ${truncate(cmd, 120)}`;\n tool_input_summary = truncate(cmd, 120);\n }\n break;\n }\n\n case 'Task': {\n const prompt = str(toolInput.prompt || toolInput.description);\n type = 'discovery';\n title = `Delegated: ${truncate(prompt, 60)}`;\n narrative = `Spawned agent: ${truncate(prompt, 200)}`;\n tool_input_summary = truncate(prompt, 200);\n break;\n }\n\n case 'WebFetch': {\n const url = str(toolInput.url);\n type = 'discovery';\n title = `Fetched: ${truncate(url, 60)}`;\n narrative = `Web fetch: ${url}`;\n tool_input_summary = url;\n break;\n }\n\n case 'WebSearch': {\n const query = str(toolInput.query);\n type = 'discovery';\n title = `Searched web: ${truncate(query, 60)}`;\n narrative = `Web search: ${query}`;\n tool_input_summary = query;\n break;\n }\n\n default: {\n // mcp__* tools and anything else\n if (toolName.startsWith('mcp__')) {\n type = 'discovery';\n title = `MCP: ${toolName}`;\n narrative = `Called MCP tool ${toolName}`;\n tool_input_summary = toolName;\n } else {\n // Generic fallback \u2014 still capture rather than skip\n type = 'discovery';\n title = `Tool: ${toolName}`;\n narrative = `Called ${toolName}`;\n tool_input_summary = JSON.stringify(toolInput).slice(0, 120);\n }\n break;\n }\n }\n\n const allPaths = [...files_read, ...files_modified];\n const concepts = extractConcepts(allPaths);\n\n return {\n type,\n title,\n narrative,\n tool_name: toolName,\n tool_input_summary,\n files_read,\n files_modified,\n concepts,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Skill telemetry capture (self-educating skill system, Phase 1)\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the invoked skill name from a tool call, or null if this call is not\n * a skill invocation. Two signals:\n * - the `Skill` tool (tool_input.skill)\n * - Bash `skill-workflow-notification WORKFLOW SKILL` (2nd arg is the skill)\n */\nfunction extractSkillInvocation(\n toolName: string,\n toolInput: Record<string, unknown>\n): string | null {\n if (toolName === 'Skill') {\n return str(toolInput.skill) || null;\n }\n if (toolName === 'Bash') {\n // Matches both the hyphenated doc form and the real camelCase tool path:\n // ~/.claude/Tools/SkillWorkflowNotification GIT CORE\n // skill-workflow-notification GIT CORE\n // Capture order is WORKFLOW then SKILL \u2014 we want the skill (2nd arg).\n const cmd = str(toolInput.command);\n const m = cmd.match(/skill[-]?workflow[-]?notification\\s+(\\S+)\\s+(\\S+)/i);\n if (m) return m[2];\n }\n return null;\n}\n\nasync function maybeRecordSkill(hookData: HookData): Promise<void> {\n const skill = extractSkillInvocation(hookData.tool_name, hookData.tool_input);\n if (!skill) return;\n await sendToDaemon('skill_telemetry_record', {\n skill_name: skill,\n source: 'local',\n cwd: hookData.cwd ?? '',\n });\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n try {\n // Read stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n const raw = Buffer.concat(chunks).toString('utf-8').trim();\n if (!raw) process.exit(0);\n\n const hookData: HookData = JSON.parse(raw);\n\n // Skip probe/health-check sessions\n if (isProbeSession(hookData.cwd)) process.exit(0);\n\n // Record skill telemetry BEFORE the SKIP gate (the Skill tool is skipped\n // for observations but is exactly the signal we want for telemetry).\n await maybeRecordSkill(hookData);\n\n // Skip uninteresting tools\n if (SKIP_TOOLS.has(hookData.tool_name)) process.exit(0);\n\n // Classify\n const obs = classify(hookData.tool_name, hookData.tool_input);\n if (!obs) process.exit(0);\n\n // Content-hash dedup key\n const hash = sha256(hookData.session_id + hookData.tool_name + obs.title).slice(0, 16);\n\n // Fire-and-forget to daemon\n await sendToDaemon('observation_store', {\n session_id: hookData.session_id,\n type: obs.type,\n title: obs.title,\n narrative: obs.narrative,\n tool_name: obs.tool_name,\n tool_input_summary: obs.tool_input_summary,\n files_read: obs.files_read,\n files_modified: obs.files_modified,\n concepts: obs.concepts,\n content_hash: hash,\n cwd: hookData.cwd ?? '',\n });\n\n process.exit(0);\n } catch {\n // Never block Claude Code\n process.exit(0);\n }\n}\n\nmain();\n", "/**\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 * Path utilities \u2014 encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory.\n * Returns the number of files moved.\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n try {\n renameSync(sourcePath, destPath);\n if (!silent) console.error(`Moved ${file} \u2192 sessions/`);\n movedCount++;\n } catch (error) {\n if (!silent) console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n\n return movedCount;\n}\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["basename", "join", "join", "resolve", "basename"]
|
|
7
7
|
}
|