@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.
Files changed (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +107 -3
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1897 -1569
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +12 -9
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
  18. package/dist/db-BtuN768f.mjs.map +1 -0
  19. package/dist/db-DdUperSl.mjs +110 -0
  20. package/dist/db-DdUperSl.mjs.map +1 -0
  21. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  22. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  23. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  24. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  25. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  26. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  27. package/dist/helpers-BEST-4Gx.mjs +420 -0
  28. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  29. package/dist/hooks/capture-all-events.mjs +19 -4
  30. package/dist/hooks/capture-all-events.mjs.map +4 -4
  31. package/dist/hooks/capture-session-summary.mjs +38 -0
  32. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  33. package/dist/hooks/cleanup-session-files.mjs +6 -12
  34. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  35. package/dist/hooks/context-compression-hook.mjs +105 -111
  36. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  37. package/dist/hooks/initialize-session.mjs +26 -17
  38. package/dist/hooks/initialize-session.mjs.map +4 -4
  39. package/dist/hooks/inject-observations.mjs +220 -0
  40. package/dist/hooks/inject-observations.mjs.map +7 -0
  41. package/dist/hooks/load-core-context.mjs +18 -2
  42. package/dist/hooks/load-core-context.mjs.map +4 -4
  43. package/dist/hooks/load-project-context.mjs +102 -97
  44. package/dist/hooks/load-project-context.mjs.map +4 -4
  45. package/dist/hooks/observe.mjs +354 -0
  46. package/dist/hooks/observe.mjs.map +7 -0
  47. package/dist/hooks/stop-hook.mjs +174 -90
  48. package/dist/hooks/stop-hook.mjs.map +4 -4
  49. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  50. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  51. package/dist/index.d.mts +32 -9
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +6 -9
  54. package/dist/indexer-D53l5d1U.mjs +1 -0
  55. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  56. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  57. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  58. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  59. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  60. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  61. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  62. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  63. package/dist/note-context-BK24bX8Y.mjs +126 -0
  64. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  65. package/dist/postgres-CKf-EDtS.mjs +846 -0
  66. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  67. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  68. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  69. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  70. package/dist/search-DC1qhkKn.mjs.map +1 -0
  71. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  72. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  73. package/dist/state-C6_vqz7w.mjs +102 -0
  74. package/dist/state-C6_vqz7w.mjs.map +1 -0
  75. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  76. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  77. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  78. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  79. package/dist/themes-BvYF0W8T.mjs +148 -0
  80. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  81. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  82. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  83. package/dist/trace-CRx9lPuc.mjs +137 -0
  84. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  85. package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
  86. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  87. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  88. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  89. package/package.json +1 -2
  90. package/src/hooks/ts/capture-all-events.ts +6 -0
  91. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  92. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  93. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  94. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  95. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  96. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  97. package/src/hooks/ts/lib/project-utils.ts +40 -999
  98. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  99. package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
  100. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  101. package/src/hooks/ts/session-start/initialize-session.ts +7 -1
  102. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  103. package/src/hooks/ts/session-start/load-core-context.ts +7 -0
  104. package/src/hooks/ts/session-start/load-project-context.ts +8 -1
  105. package/src/hooks/ts/stop/stop-hook.ts +28 -0
  106. package/templates/claude-md.template.md +7 -74
  107. package/templates/skills/user/.gitkeep +0 -0
  108. package/dist/chunker-CbnBe0s0.mjs +0 -191
  109. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  110. package/dist/config-Cf92lGX_.mjs.map +0 -1
  111. package/dist/daemon-D9evGlgR.mjs.map +0 -1
  112. package/dist/db-4lSqLFb8.mjs.map +0 -1
  113. package/dist/db-Dp8VXIMR.mjs +0 -212
  114. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  115. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  116. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  117. package/dist/mcp/index.d.mts +0 -1
  118. package/dist/mcp/index.mjs +0 -500
  119. package/dist/mcp/index.mjs.map +0 -1
  120. package/dist/postgres-FXrHDPcE.mjs +0 -358
  121. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  122. package/dist/schemas-BFIgGntb.mjs +0 -3405
  123. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  124. package/dist/search-_oHfguA5.mjs.map +0 -1
  125. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  126. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  127. package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
  128. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  129. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  130. package/templates/README.md +0 -181
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. 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": ";;;AASA,SAAS,eAAe;;;ACJxB,SAAS,kBAAkB;AAOpB,SAAS,OAAO,SAAyB;AAC9C,SAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAC1D;;;ADHA,SAAS,YAAAA,iBAAgB;;;AENzB,SAAS,QAAAC,OAAM,gBAAgB;;;ACQ/B,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAS,YAAY,oBAAoB;AAMzC,SAAS,cAAoB;AAE3B,QAAM,gBAAgB;AAAA,IACpB,QAAQ,QAAQ,IAAI,WAAW,IAAI,MAAM;AAAA,IACzC,QAAQ,QAAQ,GAAG,WAAW,MAAM;AAAA,EACtC;AAEA,aAAW,WAAW,eAAe;AACnC,QAAI,WAAW,OAAO,GAAG;AACvB,UAAI;AACF,cAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,mBAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,gBAAM,UAAU,KAAK,KAAK;AAE1B,cAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AAEzC,gBAAM,UAAU,QAAQ,QAAQ,GAAG;AACnC,cAAI,UAAU,GAAG;AACf,kBAAM,MAAM,QAAQ,UAAU,GAAG,OAAO,EAAE,KAAK;AAC/C,gBAAI,QAAQ,QAAQ,UAAU,UAAU,CAAC,EAAE,KAAK;AAGhD,gBAAK,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAC3C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,GAAI;AAClD,sBAAQ,MAAM,MAAM,GAAG,EAAE;AAAA,YAC3B;AAGA,oBAAQ,MAAM,QAAQ,WAAW,QAAQ,CAAC;AAC1C,oBAAQ,MAAM,QAAQ,cAAc,QAAQ,CAAC;AAG7C,gBAAI,QAAQ,IAAI,GAAG,MAAM,QAAW;AAClC,sBAAQ,IAAI,GAAG,IAAI;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAEA;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAGA,YAAY;AAQL,IAAM,UAAU,QAAQ,IAAI,UAC/B,QAAQ,QAAQ,IAAI,OAAO,IAC3B,QAAQ,QAAQ,GAAG,SAAS;AAKzB,IAAM,YAAY,KAAK,SAAS,OAAO;AACvC,IAAM,aAAa,KAAK,SAAS,QAAQ;AACzC,IAAM,aAAa,KAAK,SAAS,QAAQ;AACzC,IAAM,cAAc,KAAK,SAAS,SAAS;AAC3C,IAAM,eAAe,KAAK,SAAS,UAAU;AAMpD,SAAS,uBAA6B;AACpC,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,YAAQ,MAAM,2BAA2B,OAAO,EAAE;AAClD,YAAQ,MAAM,2DAA2D;AACzE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAQ,MAAM,kCAAkC,SAAS,EAAE;AAC3D,YAAQ,MAAM,sCAAsC;AACpD,YAAQ,MAAM,uBAAuB,OAAO,EAAE;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAIA,qBAAqB;;;ADpGd,IAAM,eAAeC,MAAK,SAAS,UAAU;AAMpD,IAAM,qBAAqB;AAAA,EACzB;AAAA,EACA;AACF;AAMO,SAAS,eAAe,KAAuB;AACpD,QAAM,MAAM,OAAO,QAAQ,IAAI;AAC/B,SAAO,mBAAmB,KAAK,aAAW,IAAI,SAAS,OAAO,CAAC;AACjE;;;AFeA,IAAM,aAAa,oBAAI,IAAI;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,SAAS,aAAa,QAAgB,QAAgD;AACpF,SAAO,IAAI,QAAQ,CAACC,aAAY;AAC9B,UAAM,UAAU,WAAW,MAAM;AAAE,MAAAA,SAAQ;AAAA,IAAG,GAAG,GAAI;AACrD,QAAI;AACF,YAAM,SAAS,QAAQ,iBAAiB,MAAM;AAC5C,cAAM,MAAM,KAAK,UAAU,EAAE,IAAI,KAAK,IAAI,EAAE,SAAS,GAAG,QAAQ,OAAO,CAAC,IAAI;AAC5E,eAAO,MAAM,GAAG;AAChB,eAAO,GAAG,QAAQ,MAAM;AAAE,uBAAa,OAAO;AAAG,iBAAO,QAAQ;AAAG,UAAAA,SAAQ;AAAA,QAAG,CAAC;AAC/E,eAAO,GAAG,SAAS,MAAM;AAAE,uBAAa,OAAO;AAAG,UAAAA,SAAQ;AAAA,QAAG,CAAC;AAAA,MAChE,CAAC;AACD,aAAO,GAAG,SAAS,MAAM;AAAE,qBAAa,OAAO;AAAG,QAAAA,SAAQ;AAAA,MAAG,CAAC;AAAA,IAChE,QAAQ;AACN,mBAAa,OAAO;AACpB,MAAAA,SAAQ;AAAA,IACV;AAAA,EACF,CAAC;AACH;AAMA,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAQ;AAAA,EACtD;AAAA,EAAgB;AAAA,EAAS;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EACtD;AAAA,EAAS;AAAA,EAAW;AAAA,EAAU;AAAA,EAAW;AAAA,EAAU;AAAA,EACnD;AAAA,EAAU;AAAA,EAAW;AAAA,EAAS;AAAA,EAAO;AAAA,EAAU;AAAA,EAC/C;AAAA,EAAM;AAAA,EAAM;AAAA,EAAO;AACrB,CAAC;AAED,SAAS,gBAAgB,OAA2B;AAClD,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,KAAK,OAAO;AACrB,UAAM,WAAW,EAAE,MAAM,GAAG,EAAE,OAAO,OAAO;AAC5C,eAAW,OAAO,UAAU;AAE1B,YAAM,QAAQ,IAAI,QAAQ,YAAY,EAAE;AACxC,UAAI,MAAM,SAAS,KAAK,CAAC,cAAc,IAAI,KAAK,KAAK,CAAC,QAAQ,KAAK,KAAK,GAAG;AACzE,iBAAS,IAAI,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,QAAQ,EAAE,MAAM,GAAG,EAAE;AACzC;AAMA,SAAS,IAAI,GAAoB;AAC/B,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,SAAO;AACT;AAEA,SAAS,SAAS,GAAW,KAAqB;AAChD,SAAO,EAAE,SAAS,MAAM,EAAE,MAAM,GAAG,GAAG,IAAI;AAC5C;AAEA,SAAS,SAAS,UAAkB,WAAwD;AAC1F,MAAI,WAAW,IAAI,QAAQ,EAAG,QAAO;AAErC,MAAI,OAAwB;AAC5B,MAAI,QAAQ;AACZ,MAAI,YAAY;AAChB,MAAI,qBAAqB;AACzB,QAAM,aAAuB,CAAC;AAC9B,QAAM,iBAA2B,CAAC;AAElC,UAAQ,UAAU;AAAA,IAChB,KAAK;AAAA,IACL,KAAK,aAAa;AAChB,YAAM,KAAK,IAAI,UAAU,SAAS;AAClC,YAAM,OAAO,KAAKC,UAAS,EAAE,IAAI;AACjC,aAAO;AACP,cAAQ,YAAY,IAAI;AACxB,kBAAY,KAAK,UAAU,EAAE,KAAK;AAClC,2BAAqB;AACrB,qBAAe,KAAK,GAAI,KAAK,CAAC,EAAE,IAAI,CAAC,CAAE;AACvC;AAAA,IACF;AAAA,IAEA,KAAK;AAAA,IACL,KAAK,gBAAgB;AACnB,YAAM,KAAK,IAAI,UAAU,SAAS;AAClC,YAAM,OAAO,KAAKA,UAAS,EAAE,IAAI;AACjC,aAAO;AACP,cAAQ,WAAW,IAAI;AACvB,kBAAY,KAAK,SAAS,EAAE,KAAK;AACjC,2BAAqB;AACrB,qBAAe,KAAK,GAAI,KAAK,CAAC,EAAE,IAAI,CAAC,CAAE;AACvC;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,KAAK,IAAI,UAAU,SAAS;AAClC,YAAM,OAAO,KAAKA,UAAS,EAAE,IAAI;AACjC,aAAO;AACP,cAAQ,QAAQ,IAAI;AACpB,kBAAY,KAAK,QAAQ,EAAE,KAAK;AAChC,2BAAqB;AACrB,iBAAW,KAAK,GAAI,KAAK,CAAC,EAAE,IAAI,CAAC,CAAE;AACnC;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,UAAU,IAAI,UAAU,OAAO;AACrC,aAAO;AACP,cAAQ,iBAAiB,SAAS,SAAS,EAAE,CAAC;AAC9C,kBAAY,gBAAgB,OAAO;AACnC,2BAAqB;AACrB,YAAM,QAAQ,IAAI,UAAU,QAAQ,UAAU,SAAS;AACvD,UAAI,MAAO,YAAW,KAAK,KAAK;AAChC;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,UAAU,IAAI,UAAU,OAAO;AACrC,aAAO;AACP,cAAQ,gBAAgB,SAAS,SAAS,EAAE,CAAC;AAC7C,kBAAY,iBAAiB,OAAO;AACpC,2BAAqB;AACrB;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,MAAM,IAAI,UAAU,OAAO;AACjC,YAAM,WAAW,IAAI,YAAY;AAEjC,UAAI,eAAe,KAAK,QAAQ,GAAG;AACjC,eAAO;AAEP,cAAM,SAAS,IAAI,MAAM,mBAAmB;AAC5C,cAAM,MAAM,SAAS,OAAO,CAAC,IAAI;AACjC,gBAAQ,cAAc,SAAS,KAAK,EAAE,CAAC;AACvC,oBAAY,eAAe,GAAG;AAC9B,6BAAqB,SAAS,KAAK,GAAG;AAAA,MACxC,WAAW,aAAa,KAAK,QAAQ,GAAG;AACtC,eAAO;AACP,gBAAQ;AACR,oBAAY,aAAa,SAAS,KAAK,EAAE,CAAC;AAC1C,6BAAqB,SAAS,KAAK,GAAG;AAAA,MACxC,WAAW,uFAAuF,KAAK,QAAQ,GAAG;AAChH,eAAO;AACP,gBAAQ;AACR,oBAAY,aAAa,SAAS,KAAK,EAAE,CAAC;AAC1C,6BAAqB,SAAS,KAAK,GAAG;AAAA,MACxC,WAAW,yEAAyE,KAAK,QAAQ,GAAG;AAClG,eAAO;AACP,gBAAQ;AACR,oBAAY,UAAU,SAAS,KAAK,EAAE,CAAC;AACvC,6BAAqB,SAAS,KAAK,GAAG;AAAA,MACxC,OAAO;AACL,eAAO;AACP,gBAAQ,QAAQ,SAAS,KAAK,EAAE,CAAC;AACjC,oBAAY,SAAS,SAAS,KAAK,GAAG,CAAC;AACvC,6BAAqB,SAAS,KAAK,GAAG;AAAA,MACxC;AACA;AAAA,IACF;AAAA,IAEA,KAAK,QAAQ;AACX,YAAM,SAAS,IAAI,UAAU,UAAU,UAAU,WAAW;AAC5D,aAAO;AACP,cAAQ,cAAc,SAAS,QAAQ,EAAE,CAAC;AAC1C,kBAAY,kBAAkB,SAAS,QAAQ,GAAG,CAAC;AACnD,2BAAqB,SAAS,QAAQ,GAAG;AACzC;AAAA,IACF;AAAA,IAEA,KAAK,YAAY;AACf,YAAM,MAAM,IAAI,UAAU,GAAG;AAC7B,aAAO;AACP,cAAQ,YAAY,SAAS,KAAK,EAAE,CAAC;AACrC,kBAAY,cAAc,GAAG;AAC7B,2BAAqB;AACrB;AAAA,IACF;AAAA,IAEA,KAAK,aAAa;AAChB,YAAM,QAAQ,IAAI,UAAU,KAAK;AACjC,aAAO;AACP,cAAQ,iBAAiB,SAAS,OAAO,EAAE,CAAC;AAC5C,kBAAY,eAAe,KAAK;AAChC,2BAAqB;AACrB;AAAA,IACF;AAAA,IAEA,SAAS;AAEP,UAAI,SAAS,WAAW,OAAO,GAAG;AAChC,eAAO;AACP,gBAAQ,QAAQ,QAAQ;AACxB,oBAAY,mBAAmB,QAAQ;AACvC,6BAAqB;AAAA,MACvB,OAAO;AAEL,eAAO;AACP,gBAAQ,SAAS,QAAQ;AACzB,oBAAY,UAAU,QAAQ;AAC9B,6BAAqB,KAAK,UAAU,SAAS,EAAE,MAAM,GAAG,GAAG;AAAA,MAC7D;AACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,CAAC,GAAG,YAAY,GAAG,cAAc;AAClD,QAAM,WAAW,gBAAgB,QAAQ;AAEzC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAMA,eAAe,OAAO;AACpB,MAAI;AAEF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,KAAe;AAAA,IAC7B;AACA,UAAM,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO,EAAE,KAAK;AACzD,QAAI,CAAC,IAAK,SAAQ,KAAK,CAAC;AAExB,UAAM,WAAqB,KAAK,MAAM,GAAG;AAGzC,QAAI,eAAe,SAAS,GAAG,EAAG,SAAQ,KAAK,CAAC;AAGhD,QAAI,WAAW,IAAI,SAAS,SAAS,EAAG,SAAQ,KAAK,CAAC;AAGtD,UAAM,MAAM,SAAS,SAAS,WAAW,SAAS,UAAU;AAC5D,QAAI,CAAC,IAAK,SAAQ,KAAK,CAAC;AAGxB,UAAM,OAAO,OAAO,SAAS,aAAa,SAAS,YAAY,IAAI,KAAK,EAAE,MAAM,GAAG,EAAE;AAGrF,UAAM,aAAa,qBAAqB;AAAA,MACtC,YAAY,SAAS;AAAA,MACrB,MAAM,IAAI;AAAA,MACV,OAAO,IAAI;AAAA,MACX,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,MACf,oBAAoB,IAAI;AAAA,MACxB,YAAY,IAAI;AAAA,MAChB,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,cAAc;AAAA,MACd,KAAK,SAAS,OAAO;AAAA,IACvB,CAAC;AAED,YAAQ,KAAK,CAAC;AAAA,EAChB,QAAQ;AAEN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
6
+ "names": ["basename", "join", "join", "resolve", "basename"]
7
+ }
@@ -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 readFileSync3 } from "fs";
11
- import { basename as basename2, dirname } from "path";
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, readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
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 { homedir: homedir2 } = __require("os");
110
- const settingsPath = join2(homedir2(), ".claude", "settings.json");
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
- function ensureSessionsDirFromProjectDir(projectDir) {
156
- const sessionsDir = getSessionsDirFromProjectDir(projectDir);
157
- if (!existsSync2(sessionsDir)) {
158
- mkdirSync(sessionsDir, { recursive: true });
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 (!existsSync2(notesDir)) {
191
- return null;
192
- }
206
+ if (!existsSync4(notesDir)) return null;
193
207
  const findLatestIn = (dir) => {
194
- if (!existsSync2(dir)) return null;
195
- const files = readdirSync(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
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 join2(dir, files[files.length - 1]);
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 = join2(notesDir, year, month);
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 = join2(notesDir, prevYear, prevMonth);
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 (!existsSync2(notePath)) {
232
+ if (!existsSync4(notePath)) {
219
233
  console.error(`Note file not found: ${notePath}`);
220
234
  return;
221
235
  }
222
- let content = readFileSync2(notePath, "utf-8");
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: ${basename(notePath)}`);
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 || !existsSync2(notePath)) {
290
- return notePath;
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 = join2(dir, newFilename);
305
- if (newFilename === oldFilename) {
306
- return notePath;
307
- }
308
+ const newPath = join4(dir, newFilename);
309
+ if (newFilename === oldFilename) return notePath;
308
310
  try {
309
- renameSync(notePath, newPath);
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 (!existsSync2(notePath)) {
320
+ if (!existsSync4(notePath)) {
319
321
  console.error(`Note file not found: ${notePath}`);
320
322
  return notePath;
321
323
  }
322
- let content = readFileSync2(notePath, "utf-8");
324
+ let content = readFileSync3(notePath, "utf-8");
323
325
  if (content.includes("**Status:** Completed")) {
324
- console.error(`Note already finalized: ${basename(notePath)}`);
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: ${basename(notePath)}`);
351
+ console.error(`Session note finalized: ${basename2(notePath)}`);
350
352
  const meaningfulName = extractMeaningfulName(content, summary);
351
353
  if (meaningfulName) {
352
- const newPath = renameSessionNote(notePath, meaningfulName);
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 = readFileSync3(transcriptPath, "utf-8");
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: ${basename2(currentNotePath)}`);
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}`);