@tekmidian/pai 0.5.6 → 0.6.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/ARCHITECTURE.md +72 -1
- package/README.md +107 -3
- package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
- package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
- package/dist/cli/index.mjs +1897 -1569
- package/dist/cli/index.mjs.map +1 -1
- package/dist/clusters-JIDQW65f.mjs +201 -0
- package/dist/clusters-JIDQW65f.mjs.map +1 -0
- package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
- package/dist/config-BuhHWyOK.mjs.map +1 -0
- package/dist/daemon/index.mjs +12 -9
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
- package/dist/daemon-D3hYb5_C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +4597 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
- package/dist/db-BtuN768f.mjs.map +1 -0
- package/dist/db-DdUperSl.mjs +110 -0
- package/dist/db-DdUperSl.mjs.map +1 -0
- package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
- package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
- package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
- package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
- package/dist/helpers-BEST-4Gx.mjs +420 -0
- package/dist/helpers-BEST-4Gx.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +19 -4
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/capture-session-summary.mjs +38 -0
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +6 -12
- package/dist/hooks/cleanup-session-files.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +105 -111
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +26 -17
- package/dist/hooks/initialize-session.mjs.map +4 -4
- package/dist/hooks/inject-observations.mjs +220 -0
- package/dist/hooks/inject-observations.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +18 -2
- package/dist/hooks/load-core-context.mjs.map +4 -4
- package/dist/hooks/load-project-context.mjs +102 -97
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/observe.mjs +354 -0
- package/dist/hooks/observe.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +174 -90
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/sync-todo-to-md.mjs +31 -33
- package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
- package/dist/index.d.mts +32 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -9
- package/dist/indexer-D53l5d1U.mjs +1 -0
- package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
- package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
- package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
- package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
- package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
- package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
- package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
- package/dist/note-context-BK24bX8Y.mjs +126 -0
- package/dist/note-context-BK24bX8Y.mjs.map +1 -0
- package/dist/postgres-CKf-EDtS.mjs +846 -0
- package/dist/postgres-CKf-EDtS.mjs.map +1 -0
- package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
- package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
- package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
- package/dist/search-DC1qhkKn.mjs.map +1 -0
- package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
- package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
- package/dist/state-C6_vqz7w.mjs +102 -0
- package/dist/state-C6_vqz7w.mjs.map +1 -0
- package/dist/stop-words-BaMEGVeY.mjs +326 -0
- package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
- package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
- package/dist/sync-BOsnEj2-.mjs.map +1 -0
- package/dist/themes-BvYF0W8T.mjs +148 -0
- package/dist/themes-BvYF0W8T.mjs.map +1 -0
- package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
- package/dist/tools-DcaJlYDN.mjs.map +1 -0
- package/dist/trace-CRx9lPuc.mjs +137 -0
- package/dist/trace-CRx9lPuc.mjs.map +1 -0
- package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
- package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
- package/dist/zettelkasten-cdajbnPr.mjs +708 -0
- package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
- package/package.json +1 -2
- package/src/hooks/ts/capture-all-events.ts +6 -0
- package/src/hooks/ts/lib/project-utils/index.ts +50 -0
- package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
- package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
- package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
- package/src/hooks/ts/lib/project-utils.ts +40 -999
- package/src/hooks/ts/post-tool-use/observe.ts +327 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
- package/src/hooks/ts/session-start/initialize-session.ts +7 -1
- package/src/hooks/ts/session-start/inject-observations.ts +254 -0
- package/src/hooks/ts/session-start/load-core-context.ts +7 -0
- package/src/hooks/ts/session-start/load-project-context.ts +8 -1
- package/src/hooks/ts/stop/stop-hook.ts +28 -0
- package/templates/claude-md.template.md +7 -74
- package/templates/skills/user/.gitkeep +0 -0
- package/dist/chunker-CbnBe0s0.mjs +0 -191
- package/dist/chunker-CbnBe0s0.mjs.map +0 -1
- package/dist/config-Cf92lGX_.mjs.map +0 -1
- package/dist/daemon-D9evGlgR.mjs.map +0 -1
- package/dist/db-4lSqLFb8.mjs.map +0 -1
- package/dist/db-Dp8VXIMR.mjs +0 -212
- package/dist/db-Dp8VXIMR.mjs.map +0 -1
- package/dist/indexer-CMPOiY1r.mjs.map +0 -1
- package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
- package/dist/mcp/index.d.mts +0 -1
- package/dist/mcp/index.mjs +0 -500
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/postgres-FXrHDPcE.mjs +0 -358
- package/dist/postgres-FXrHDPcE.mjs.map +0 -1
- package/dist/schemas-BFIgGntb.mjs +0 -3405
- package/dist/schemas-BFIgGntb.mjs.map +0 -1
- package/dist/search-_oHfguA5.mjs.map +0 -1
- package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
- package/dist/tools-DV_lsiCc.mjs.map +0 -1
- package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
- package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
- package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
- package/templates/README.md +0 -181
- package/templates/skills/createskill-skill.template.md +0 -78
- package/templates/skills/history-system.template.md +0 -371
- package/templates/skills/hook-system.template.md +0 -913
- package/templates/skills/sessions-skill.template.md +0 -102
- package/templates/skills/skill-system.template.md +0 -214
- package/templates/skills/terminal-tabs.template.md +0 -120
- package/templates/templates.md +0 -20
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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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",
|
|
6
|
+
"names": ["basename", "join", "join", "resolve", "basename"]
|
|
7
|
+
}
|
package/dist/hooks/stop-hook.mjs
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/hooks/ts/stop/stop-hook.ts
|
|
10
|
-
import { readFileSync as
|
|
11
|
-
import { basename as
|
|
4
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5
|
+
import { basename as basename3, dirname } from "path";
|
|
12
6
|
|
|
13
|
-
// src/hooks/ts/lib/project-utils.ts
|
|
14
|
-
import { existsSync as existsSync2, mkdirSync, readdirSync,
|
|
7
|
+
// src/hooks/ts/lib/project-utils/paths.ts
|
|
8
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, renameSync } from "fs";
|
|
15
9
|
import { join as join2, basename } from "path";
|
|
16
10
|
|
|
17
11
|
// src/hooks/ts/lib/pai-paths.ts
|
|
@@ -72,8 +66,16 @@ function validatePAIStructure() {
|
|
|
72
66
|
}
|
|
73
67
|
validatePAIStructure();
|
|
74
68
|
|
|
75
|
-
// src/hooks/ts/lib/project-utils.ts
|
|
69
|
+
// src/hooks/ts/lib/project-utils/paths.ts
|
|
76
70
|
var PROJECTS_DIR = join2(PAI_DIR, "projects");
|
|
71
|
+
var PROBE_CWD_PATTERNS = [
|
|
72
|
+
"/CodexBar/ClaudeProbe",
|
|
73
|
+
"/ClaudeProbe"
|
|
74
|
+
];
|
|
75
|
+
function isProbeSession(cwd) {
|
|
76
|
+
const dir = cwd || process.cwd();
|
|
77
|
+
return PROBE_CWD_PATTERNS.some((pattern) => dir.includes(pattern));
|
|
78
|
+
}
|
|
77
79
|
function encodePath(path) {
|
|
78
80
|
return path.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
79
81
|
}
|
|
@@ -104,14 +106,58 @@ function findNotesDir(cwd) {
|
|
|
104
106
|
function getSessionsDirFromProjectDir(projectDir) {
|
|
105
107
|
return join2(projectDir, "sessions");
|
|
106
108
|
}
|
|
109
|
+
function ensureSessionsDirFromProjectDir(projectDir) {
|
|
110
|
+
const sessionsDir = getSessionsDirFromProjectDir(projectDir);
|
|
111
|
+
if (!existsSync2(sessionsDir)) {
|
|
112
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
113
|
+
console.error(`Created sessions directory: ${sessionsDir}`);
|
|
114
|
+
}
|
|
115
|
+
return sessionsDir;
|
|
116
|
+
}
|
|
117
|
+
function moveSessionFilesToSessionsDir(projectDir, excludeFile, silent = false) {
|
|
118
|
+
const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);
|
|
119
|
+
if (!existsSync2(projectDir)) return 0;
|
|
120
|
+
const files = readdirSync(projectDir);
|
|
121
|
+
let movedCount = 0;
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
if (file.endsWith(".jsonl") && file !== excludeFile) {
|
|
124
|
+
const sourcePath = join2(projectDir, file);
|
|
125
|
+
const destPath = join2(sessionsDir, file);
|
|
126
|
+
try {
|
|
127
|
+
renameSync(sourcePath, destPath);
|
|
128
|
+
if (!silent) console.error(`Moved ${file} \u2192 sessions/`);
|
|
129
|
+
movedCount++;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!silent) console.error(`Could not move ${file}: ${error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return movedCount;
|
|
136
|
+
}
|
|
137
|
+
function findTodoPath(cwd) {
|
|
138
|
+
const localPaths = [
|
|
139
|
+
join2(cwd, "TODO.md"),
|
|
140
|
+
join2(cwd, "notes", "TODO.md"),
|
|
141
|
+
join2(cwd, "Notes", "TODO.md"),
|
|
142
|
+
join2(cwd, ".claude", "TODO.md")
|
|
143
|
+
];
|
|
144
|
+
for (const path of localPaths) {
|
|
145
|
+
if (existsSync2(path)) return path;
|
|
146
|
+
}
|
|
147
|
+
return join2(getNotesDir(cwd), "TODO.md");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/hooks/ts/lib/project-utils/notify.ts
|
|
151
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
152
|
+
import { join as join3 } from "path";
|
|
153
|
+
import { homedir as homedir2 } from "os";
|
|
107
154
|
function isWhatsAppEnabled() {
|
|
108
155
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (!existsSync2(settingsPath)) return false;
|
|
156
|
+
const settingsPath = join3(homedir2(), ".claude", "settings.json");
|
|
157
|
+
if (!existsSync3(settingsPath)) return false;
|
|
112
158
|
const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
113
159
|
const enabled = settings.enabledMcpjsonServers || [];
|
|
114
|
-
return enabled.includes("whazaa");
|
|
160
|
+
return enabled.includes("aibroker") || enabled.includes("whazaa") || enabled.includes("telex");
|
|
115
161
|
} catch {
|
|
116
162
|
return false;
|
|
117
163
|
}
|
|
@@ -152,81 +198,47 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
152
198
|
console.error("ntfy.sh notification failed after all retries");
|
|
153
199
|
return false;
|
|
154
200
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.error(`Created sessions directory: ${sessionsDir}`);
|
|
160
|
-
}
|
|
161
|
-
return sessionsDir;
|
|
162
|
-
}
|
|
163
|
-
function moveSessionFilesToSessionsDir(projectDir, excludeFile, silent = false) {
|
|
164
|
-
const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);
|
|
165
|
-
if (!existsSync2(projectDir)) {
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
const files = readdirSync(projectDir);
|
|
169
|
-
let movedCount = 0;
|
|
170
|
-
for (const file of files) {
|
|
171
|
-
if (file.endsWith(".jsonl") && file !== excludeFile) {
|
|
172
|
-
const sourcePath = join2(projectDir, file);
|
|
173
|
-
const destPath = join2(sessionsDir, file);
|
|
174
|
-
try {
|
|
175
|
-
renameSync(sourcePath, destPath);
|
|
176
|
-
if (!silent) {
|
|
177
|
-
console.error(`Moved ${file} \u2192 sessions/`);
|
|
178
|
-
}
|
|
179
|
-
movedCount++;
|
|
180
|
-
} catch (error) {
|
|
181
|
-
if (!silent) {
|
|
182
|
-
console.error(`Could not move ${file}: ${error}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return movedCount;
|
|
188
|
-
}
|
|
201
|
+
|
|
202
|
+
// src/hooks/ts/lib/project-utils/session-notes.ts
|
|
203
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync, renameSync as renameSync2 } from "fs";
|
|
204
|
+
import { join as join4, basename as basename2 } from "path";
|
|
189
205
|
function getCurrentNotePath(notesDir) {
|
|
190
|
-
if (!
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
206
|
+
if (!existsSync4(notesDir)) return null;
|
|
193
207
|
const findLatestIn = (dir) => {
|
|
194
|
-
if (!
|
|
195
|
-
const files =
|
|
208
|
+
if (!existsSync4(dir)) return null;
|
|
209
|
+
const files = readdirSync2(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
|
|
196
210
|
const numA = parseInt(a.match(/^(\d+)/)?.[1] || "0", 10);
|
|
197
211
|
const numB = parseInt(b.match(/^(\d+)/)?.[1] || "0", 10);
|
|
198
212
|
return numA - numB;
|
|
199
213
|
});
|
|
200
214
|
if (files.length === 0) return null;
|
|
201
|
-
return
|
|
215
|
+
return join4(dir, files[files.length - 1]);
|
|
202
216
|
};
|
|
203
217
|
const now = /* @__PURE__ */ new Date();
|
|
204
218
|
const year = String(now.getFullYear());
|
|
205
219
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
206
|
-
const currentMonthDir =
|
|
220
|
+
const currentMonthDir = join4(notesDir, year, month);
|
|
207
221
|
const found = findLatestIn(currentMonthDir);
|
|
208
222
|
if (found) return found;
|
|
209
223
|
const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
210
224
|
const prevYear = String(prevDate.getFullYear());
|
|
211
225
|
const prevMonth = String(prevDate.getMonth() + 1).padStart(2, "0");
|
|
212
|
-
const prevMonthDir =
|
|
226
|
+
const prevMonthDir = join4(notesDir, prevYear, prevMonth);
|
|
213
227
|
const prevFound = findLatestIn(prevMonthDir);
|
|
214
228
|
if (prevFound) return prevFound;
|
|
215
229
|
return findLatestIn(notesDir);
|
|
216
230
|
}
|
|
217
231
|
function addWorkToSessionNote(notePath, workItems, sectionTitle) {
|
|
218
|
-
if (!
|
|
232
|
+
if (!existsSync4(notePath)) {
|
|
219
233
|
console.error(`Note file not found: ${notePath}`);
|
|
220
234
|
return;
|
|
221
235
|
}
|
|
222
|
-
let content =
|
|
236
|
+
let content = readFileSync3(notePath, "utf-8");
|
|
223
237
|
let workText = "";
|
|
224
|
-
if (sectionTitle)
|
|
225
|
-
workText += `
|
|
238
|
+
if (sectionTitle) workText += `
|
|
226
239
|
### ${sectionTitle}
|
|
227
240
|
|
|
228
241
|
`;
|
|
229
|
-
}
|
|
230
242
|
for (const item of workItems) {
|
|
231
243
|
const checkbox = item.completed !== false ? "[x]" : "[ ]";
|
|
232
244
|
workText += `- ${checkbox} **${item.title}**
|
|
@@ -249,7 +261,7 @@ function addWorkToSessionNote(notePath, workItems, sectionTitle) {
|
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
writeFileSync(notePath, content);
|
|
252
|
-
console.error(`Added ${workItems.length} work item(s) to: ${
|
|
264
|
+
console.error(`Added ${workItems.length} work item(s) to: ${basename2(notePath)}`);
|
|
253
265
|
}
|
|
254
266
|
function sanitizeForFilename(str) {
|
|
255
267
|
return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 50);
|
|
@@ -273,40 +285,30 @@ function extractMeaningfulName(noteContent, summary) {
|
|
|
273
285
|
}
|
|
274
286
|
}
|
|
275
287
|
const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
|
|
276
|
-
if (numberedItems)
|
|
277
|
-
return sanitizeForFilename(numberedItems[1]);
|
|
278
|
-
}
|
|
288
|
+
if (numberedItems) return sanitizeForFilename(numberedItems[1]);
|
|
279
289
|
}
|
|
280
290
|
if (summary && summary.length > 5 && summary !== "Session completed.") {
|
|
281
291
|
const cleanSummary = summary.replace(/[^\w\s-]/g, " ").trim().split(/\s+/).slice(0, 5).join(" ");
|
|
282
|
-
if (cleanSummary.length > 3)
|
|
283
|
-
return sanitizeForFilename(cleanSummary);
|
|
284
|
-
}
|
|
292
|
+
if (cleanSummary.length > 3) return sanitizeForFilename(cleanSummary);
|
|
285
293
|
}
|
|
286
294
|
return "";
|
|
287
295
|
}
|
|
288
296
|
function renameSessionNote(notePath, meaningfulName) {
|
|
289
|
-
if (!meaningfulName || !
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const dir = join2(notePath, "..");
|
|
293
|
-
const oldFilename = basename(notePath);
|
|
297
|
+
if (!meaningfulName || !existsSync4(notePath)) return notePath;
|
|
298
|
+
const dir = join4(notePath, "..");
|
|
299
|
+
const oldFilename = basename2(notePath);
|
|
294
300
|
const correctMatch = oldFilename.match(/^(\d{3,4}) - (\d{4}-\d{2}-\d{2}) - .*\.md$/);
|
|
295
301
|
const legacyMatch = oldFilename.match(/^(\d{3,4})_(\d{4}-\d{2}-\d{2})_.*\.md$/);
|
|
296
302
|
const match = correctMatch || legacyMatch;
|
|
297
|
-
if (!match)
|
|
298
|
-
return notePath;
|
|
299
|
-
}
|
|
303
|
+
if (!match) return notePath;
|
|
300
304
|
const [, noteNumber, date] = match;
|
|
301
305
|
const titleCaseName = meaningfulName.split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
|
|
302
306
|
const paddedNumber = noteNumber.padStart(4, "0");
|
|
303
307
|
const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
|
|
304
|
-
const newPath =
|
|
305
|
-
if (newFilename === oldFilename)
|
|
306
|
-
return notePath;
|
|
307
|
-
}
|
|
308
|
+
const newPath = join4(dir, newFilename);
|
|
309
|
+
if (newFilename === oldFilename) return notePath;
|
|
308
310
|
try {
|
|
309
|
-
|
|
311
|
+
renameSync2(notePath, newPath);
|
|
310
312
|
console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
|
|
311
313
|
return newPath;
|
|
312
314
|
} catch (error) {
|
|
@@ -315,13 +317,13 @@ function renameSessionNote(notePath, meaningfulName) {
|
|
|
315
317
|
}
|
|
316
318
|
}
|
|
317
319
|
function finalizeSessionNote(notePath, summary) {
|
|
318
|
-
if (!
|
|
320
|
+
if (!existsSync4(notePath)) {
|
|
319
321
|
console.error(`Note file not found: ${notePath}`);
|
|
320
322
|
return notePath;
|
|
321
323
|
}
|
|
322
|
-
let content =
|
|
324
|
+
let content = readFileSync3(notePath, "utf-8");
|
|
323
325
|
if (content.includes("**Status:** Completed")) {
|
|
324
|
-
console.error(`Note already finalized: ${
|
|
326
|
+
console.error(`Note already finalized: ${basename2(notePath)}`);
|
|
325
327
|
return notePath;
|
|
326
328
|
}
|
|
327
329
|
content = content.replace("**Status:** In Progress", "**Status:** Completed");
|
|
@@ -346,15 +348,75 @@ ${summary || "Session completed."}`
|
|
|
346
348
|
);
|
|
347
349
|
}
|
|
348
350
|
writeFileSync(notePath, content);
|
|
349
|
-
console.error(`Session note finalized: ${
|
|
351
|
+
console.error(`Session note finalized: ${basename2(notePath)}`);
|
|
350
352
|
const meaningfulName = extractMeaningfulName(content, summary);
|
|
351
353
|
if (meaningfulName) {
|
|
352
|
-
|
|
353
|
-
return newPath;
|
|
354
|
+
return renameSessionNote(notePath, meaningfulName);
|
|
354
355
|
}
|
|
355
356
|
return notePath;
|
|
356
357
|
}
|
|
357
358
|
|
|
359
|
+
// src/hooks/ts/lib/project-utils/todo.ts
|
|
360
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
361
|
+
import { join as join5 } from "path";
|
|
362
|
+
function ensureTodoMd(cwd) {
|
|
363
|
+
const todoPath = findTodoPath(cwd);
|
|
364
|
+
if (!existsSync5(todoPath)) {
|
|
365
|
+
const parentDir = join5(todoPath, "..");
|
|
366
|
+
if (!existsSync5(parentDir)) mkdirSync3(parentDir, { recursive: true });
|
|
367
|
+
const content = `# TODO
|
|
368
|
+
|
|
369
|
+
## Current Session
|
|
370
|
+
|
|
371
|
+
- [ ] (Tasks will be tracked here)
|
|
372
|
+
|
|
373
|
+
## Backlog
|
|
374
|
+
|
|
375
|
+
- [ ] (Future tasks)
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
380
|
+
`;
|
|
381
|
+
writeFileSync2(todoPath, content);
|
|
382
|
+
console.error(`Created TODO.md: ${todoPath}`);
|
|
383
|
+
}
|
|
384
|
+
return todoPath;
|
|
385
|
+
}
|
|
386
|
+
function updateTodoContinue(cwd, noteFilename, state, tokenDisplay) {
|
|
387
|
+
const todoPath = ensureTodoMd(cwd);
|
|
388
|
+
let content = readFileSync4(todoPath, "utf-8");
|
|
389
|
+
content = content.replace(/## Continue\n[\s\S]*?\n---\n+/, "");
|
|
390
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
391
|
+
const stateLines = state ? state.split("\n").filter((l) => l.trim()).slice(0, 10).map((l) => `> ${l}`).join("\n") : `> Working directory: ${cwd}. Check the latest session note for details.`;
|
|
392
|
+
const continueSection = `## Continue
|
|
393
|
+
|
|
394
|
+
> **Last session:** ${noteFilename.replace(".md", "")}
|
|
395
|
+
> **Paused at:** ${now}
|
|
396
|
+
>
|
|
397
|
+
${stateLines}
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
`;
|
|
402
|
+
content = content.replace(/^\s+/, "");
|
|
403
|
+
const titleMatch = content.match(/^(# [^\n]+\n+)/);
|
|
404
|
+
if (titleMatch) {
|
|
405
|
+
content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);
|
|
406
|
+
} else {
|
|
407
|
+
content = continueSection + content;
|
|
408
|
+
}
|
|
409
|
+
content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, "");
|
|
410
|
+
content = content.trimEnd() + `
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
*Last updated: ${now}*
|
|
415
|
+
`;
|
|
416
|
+
writeFileSync2(todoPath, content);
|
|
417
|
+
console.error("TODO.md ## Continue section updated");
|
|
418
|
+
}
|
|
419
|
+
|
|
358
420
|
// src/hooks/ts/stop/stop-hook.ts
|
|
359
421
|
function extractWorkFromTranscript(lines) {
|
|
360
422
|
const workItems = [];
|
|
@@ -465,6 +527,9 @@ function contentToText(content) {
|
|
|
465
527
|
return "";
|
|
466
528
|
}
|
|
467
529
|
async function main() {
|
|
530
|
+
if (isProbeSession()) {
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
468
533
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
469
534
|
console.error(`
|
|
470
535
|
STOP-HOOK TRIGGERED AT ${timestamp}`);
|
|
@@ -500,7 +565,7 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
|
|
|
500
565
|
}
|
|
501
566
|
let transcript;
|
|
502
567
|
try {
|
|
503
|
-
transcript =
|
|
568
|
+
transcript = readFileSync5(transcriptPath, "utf-8");
|
|
504
569
|
console.error(`Transcript loaded: ${transcript.split("\n").length} lines`);
|
|
505
570
|
} catch (e) {
|
|
506
571
|
console.error(`Error reading transcript: ${e}`);
|
|
@@ -603,7 +668,26 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
|
|
|
603
668
|
}
|
|
604
669
|
const summary = message || "Session completed.";
|
|
605
670
|
finalizeSessionNote(currentNotePath, summary);
|
|
606
|
-
console.error(`Session note finalized: ${
|
|
671
|
+
console.error(`Session note finalized: ${basename3(currentNotePath)}`);
|
|
672
|
+
try {
|
|
673
|
+
const stateLines = [];
|
|
674
|
+
stateLines.push(`Working directory: ${cwd}`);
|
|
675
|
+
if (workItems.length > 0) {
|
|
676
|
+
stateLines.push("");
|
|
677
|
+
stateLines.push("Work completed:");
|
|
678
|
+
for (const item of workItems.slice(0, 5)) {
|
|
679
|
+
stateLines.push(`- ${item.title}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (message) {
|
|
683
|
+
stateLines.push("");
|
|
684
|
+
stateLines.push(`Last completed: ${message}`);
|
|
685
|
+
}
|
|
686
|
+
const state = stateLines.join("\n");
|
|
687
|
+
updateTodoContinue(cwd, basename3(currentNotePath), state, "session-end");
|
|
688
|
+
} catch (todoError) {
|
|
689
|
+
console.error(`Could not update TODO.md: ${todoError}`);
|
|
690
|
+
}
|
|
607
691
|
}
|
|
608
692
|
} catch (noteError) {
|
|
609
693
|
console.error(`Could not finalize session note: ${noteError}`);
|