@tekmidian/pai 0.14.2 → 0.16.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 (148) hide show
  1. package/dist/{auto-route-CLYcToZ5.mjs → auto-route-CdvjEWs5.mjs} +4 -4
  2. package/dist/{auto-route-CLYcToZ5.mjs.map → auto-route-CdvjEWs5.mjs.map} +1 -1
  3. package/dist/{kg-extraction-C8DEUHTS.mjs → checkpoint-block-Bl0HD3U2.mjs} +442 -13
  4. package/dist/checkpoint-block-Bl0HD3U2.mjs.map +1 -0
  5. package/dist/cli/index.mjs +14 -12
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/cli/program.d.mts.map +1 -1
  8. package/dist/cli/program.mjs +31 -16
  9. package/dist/cli/program.mjs.map +1 -1
  10. package/dist/{clusters-BYPw7vfW.mjs → clusters-CKWDwcMX.mjs} +2 -2
  11. package/dist/{clusters-BYPw7vfW.mjs.map → clusters-CKWDwcMX.mjs.map} +1 -1
  12. package/dist/{config-C8m-tPhP.mjs → config-DGOfoGfm.mjs} +1 -1
  13. package/dist/config-DGOfoGfm.mjs.map +1 -0
  14. package/dist/daemon/index.mjs +18 -16
  15. package/dist/daemon/index.mjs.map +1 -1
  16. package/dist/{daemon-COSvHTNu.mjs → daemon-1rKbN-fd.mjs} +31 -30
  17. package/dist/{daemon-COSvHTNu.mjs.map → daemon-1rKbN-fd.mjs.map} +1 -1
  18. package/dist/daemon-mcp/index.mjs +24 -2
  19. package/dist/daemon-mcp/index.mjs.map +1 -1
  20. package/dist/db-BtuN768f.mjs +206 -0
  21. package/dist/db-BtuN768f.mjs.map +1 -0
  22. package/dist/{db-CmYbAVCD.mjs → db-CYmBWcjh.mjs} +1 -1
  23. package/dist/{db-CmYbAVCD.mjs.map → db-CYmBWcjh.mjs.map} +1 -1
  24. package/dist/{detect-KjycLtXM.mjs → detect-Bf2z-oKB.mjs} +1 -1
  25. package/dist/{detect-KjycLtXM.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
  26. package/dist/{detector-ZiOhHszd.mjs → detector-rq9JGTmS.mjs} +2 -2
  27. package/dist/{detector-ZiOhHszd.mjs.map → detector-rq9JGTmS.mjs.map} +1 -1
  28. package/dist/{embeddings-BJPOcbik.mjs → embeddings-DGRAPAYb.mjs} +1 -1
  29. package/dist/{embeddings-BJPOcbik.mjs.map → embeddings-DGRAPAYb.mjs.map} +1 -1
  30. package/dist/{factory-Q88X1bAN.mjs → factory-HcQsQqh0.mjs} +8 -5
  31. package/dist/{factory-Q88X1bAN.mjs.map → factory-HcQsQqh0.mjs.map} +1 -1
  32. package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
  33. package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
  34. package/dist/hooks/capture-all-events.mjs.map +1 -1
  35. package/dist/hooks/capture-session-summary.mjs.map +1 -1
  36. package/dist/hooks/capture-tool-output.mjs.map +1 -1
  37. package/dist/hooks/cleanup-session-files.mjs.map +1 -1
  38. package/dist/hooks/context-compression-hook.mjs +304 -48
  39. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  40. package/dist/hooks/initialize-session.mjs.map +1 -1
  41. package/dist/hooks/inject-observations.mjs.map +1 -1
  42. package/dist/hooks/load-core-context.mjs.map +1 -1
  43. package/dist/hooks/load-project-context.mjs.map +1 -1
  44. package/dist/hooks/observe.mjs.map +1 -1
  45. package/dist/hooks/post-compact-inject.mjs.map +1 -1
  46. package/dist/hooks/security-validator.mjs.map +1 -1
  47. package/dist/hooks/session-commands.mjs.map +1 -1
  48. package/dist/hooks/stop-hook.mjs +299 -43
  49. package/dist/hooks/stop-hook.mjs.map +4 -4
  50. package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
  51. package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
  52. package/dist/hooks/update-tab-on-action.mjs.map +1 -1
  53. package/dist/hooks/update-tab-titles.mjs.map +1 -1
  54. package/dist/hooks/whisper-rules.mjs.map +1 -1
  55. package/dist/index.d.mts +0 -9
  56. package/dist/index.d.mts.map +1 -1
  57. package/dist/index.mjs +10 -9
  58. package/dist/indexer-AEcT8wHf.mjs +1 -0
  59. package/dist/{indexer-backend-Cm5RS7y0.mjs → indexer-backend-BQZXoarl.mjs} +3 -3
  60. package/dist/{indexer-backend-Cm5RS7y0.mjs.map → indexer-backend-BQZXoarl.mjs.map} +1 -1
  61. package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-vO2GV325.mjs} +1 -1
  62. package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-vO2GV325.mjs.map} +1 -1
  63. package/dist/{kg-entity-LXblD7LZ.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  64. package/dist/{kg-entity-LXblD7LZ.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  65. package/dist/{latent-ideas-RyC6eyqI.mjs → latent-ideas-C6IFFT0-.mjs} +4 -4
  66. package/dist/{latent-ideas-RyC6eyqI.mjs.map → latent-ideas-C6IFFT0-.mjs.map} +1 -1
  67. package/dist/{main-resolver-uFxNiDy7.mjs → main-resolver-DimnS2qP.mjs} +22 -22
  68. package/dist/{main-resolver-uFxNiDy7.mjs.map → main-resolver-DimnS2qP.mjs.map} +1 -1
  69. package/dist/{migrate-B02wDDgS.mjs → migrate-fLD6rAdO.mjs} +2 -2
  70. package/dist/{migrate-B02wDDgS.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
  71. package/dist/{neighborhood-CklGIB8r.mjs → neighborhood-CzRNG2Oy.mjs} +2 -2
  72. package/dist/{neighborhood-CklGIB8r.mjs.map → neighborhood-CzRNG2Oy.mjs.map} +1 -1
  73. package/dist/{note-context-qvZlVXVO.mjs → note-context-CrfMbr4R.mjs} +1 -1
  74. package/dist/{note-context-qvZlVXVO.mjs.map → note-context-CrfMbr4R.mjs.map} +1 -1
  75. package/dist/{pai-marker-HVBwwBW-.mjs → pai-marker-B20KqhA8.mjs} +44 -2
  76. package/dist/pai-marker-B20KqhA8.mjs.map +1 -0
  77. package/dist/{pick-gne3kOJG.mjs → pick-B3zEYjin.mjs} +2316 -483
  78. package/dist/pick-B3zEYjin.mjs.map +1 -0
  79. package/dist/{postgres-B451Hnkm.mjs → postgres-zjKqPl4X.mjs} +2 -2
  80. package/dist/{postgres-B451Hnkm.mjs.map → postgres-zjKqPl4X.mjs.map} +1 -1
  81. package/dist/{query-feedback-fapbqRgh.mjs → query-feedback-BoY8_Dbb.mjs} +1 -1
  82. package/dist/{query-feedback-fapbqRgh.mjs.map → query-feedback-BoY8_Dbb.mjs.map} +1 -1
  83. package/dist/{reranker-C08R99zn.mjs → reranker-CMNZcfVx.mjs} +1 -1
  84. package/dist/{reranker-C08R99zn.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  85. package/dist/{search-HcdKtMla.mjs → search-i2nlQ-JM.mjs} +3 -3
  86. package/dist/{search-HcdKtMla.mjs.map → search-i2nlQ-JM.mjs.map} +1 -1
  87. package/dist/skills/End/SKILL.md +78 -0
  88. package/dist/skills/Pause/SKILL.md +77 -0
  89. package/dist/skills/Tasks/SKILL.md +22 -0
  90. package/dist/{sqlite-DQpY1Esi.mjs → sqlite-ChSCYwiC.mjs} +3 -3
  91. package/dist/{sqlite-DQpY1Esi.mjs.map → sqlite-ChSCYwiC.mjs.map} +1 -1
  92. package/dist/{state-BIlxNRUn.mjs → state-BXIdbxDs.mjs} +1 -1
  93. package/dist/{state-BIlxNRUn.mjs.map → state-BXIdbxDs.mjs.map} +1 -1
  94. package/dist/{stop-words-BwplsQ3z.mjs → stop-words-BaMEGVeY.mjs} +1 -1
  95. package/dist/{stop-words-BwplsQ3z.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
  96. package/dist/{sync-uPR4g438.mjs → sync-CmBKOL3K.mjs} +6 -205
  97. package/dist/sync-CmBKOL3K.mjs.map +1 -0
  98. package/dist/{themes-Cg8oG9Ra.mjs → themes-nvRM8v84.mjs} +3 -3
  99. package/dist/{themes-Cg8oG9Ra.mjs.map → themes-nvRM8v84.mjs.map} +1 -1
  100. package/dist/{tools-Op3C2Nm6.mjs → tools-CznjzfYz.mjs} +26 -26
  101. package/dist/{tools-Op3C2Nm6.mjs.map → tools-CznjzfYz.mjs.map} +1 -1
  102. package/dist/{trace-CLK-NPkb.mjs → trace-B8oz1ok0.mjs} +1 -1
  103. package/dist/{trace-CLK-NPkb.mjs.map → trace-B8oz1ok0.mjs.map} +1 -1
  104. package/dist/{utils-CqqgB0dH.mjs → utils-BAxjW3j8.mjs} +1 -1
  105. package/dist/{utils-CqqgB0dH.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
  106. package/dist/{vault-indexer-Dn_BccVK.mjs → vault-indexer-DrttVeEk.mjs} +2 -2
  107. package/dist/{vault-indexer-Dn_BccVK.mjs.map → vault-indexer-DrttVeEk.mjs.map} +1 -1
  108. package/dist/{work-queue-worker-BwouhVkb.mjs → work-queue-worker-BPXLkEik.mjs} +47 -30
  109. package/dist/work-queue-worker-BPXLkEik.mjs.map +1 -0
  110. package/dist/{zettelkasten-NwBD1NsT.mjs → zettelkasten-CTlt1dGv.mjs} +4 -4
  111. package/dist/{zettelkasten-NwBD1NsT.mjs.map → zettelkasten-CTlt1dGv.mjs.map} +1 -1
  112. package/docs/commands/README.md +17 -0
  113. package/docs/commands/backup.md +1 -1
  114. package/docs/commands/clear-names.md +1 -1
  115. package/docs/commands/daemon.md +1 -1
  116. package/docs/commands/db.md +1 -1
  117. package/docs/commands/end.md +4 -1
  118. package/docs/commands/help.md +1 -1
  119. package/docs/commands/kg.md +1 -1
  120. package/docs/commands/mcp.md +1 -1
  121. package/docs/commands/memory.md +1 -1
  122. package/docs/commands/notify.md +1 -1
  123. package/docs/commands/observation.md +1 -1
  124. package/docs/commands/obsidian.md +1 -1
  125. package/docs/commands/pause.md +4 -1
  126. package/docs/commands/project.md +1 -1
  127. package/docs/commands/projects.md +1 -1
  128. package/docs/commands/registry.md +18 -1
  129. package/docs/commands/restore.md +1 -1
  130. package/docs/commands/session.md +271 -0
  131. package/docs/commands/sessions.md +1 -1
  132. package/docs/commands/setup.md +1 -1
  133. package/docs/commands/shell-init.md +1 -1
  134. package/docs/commands/skill.md +1 -1
  135. package/docs/commands/task.md +2 -1
  136. package/docs/commands/topic.md +1 -1
  137. package/docs/commands/update.md +1 -1
  138. package/docs/commands/zettel.md +1 -1
  139. package/package.json +1 -1
  140. package/scripts/build-hooks.mjs +32 -3
  141. package/src/hooks/ts/lib/project-utils/todo.test.ts +132 -0
  142. package/src/hooks/ts/lib/project-utils/todo.ts +60 -34
  143. package/dist/config-C8m-tPhP.mjs.map +0 -1
  144. package/dist/kg-extraction-C8DEUHTS.mjs.map +0 -1
  145. package/dist/pai-marker-HVBwwBW-.mjs.map +0 -1
  146. package/dist/pick-gne3kOJG.mjs.map +0 -1
  147. package/dist/sync-uPR4g438.mjs.map +0 -1
  148. package/dist/work-queue-worker-BwouhVkb.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/ts/session-start/inject-observations.ts", "../../src/hooks/ts/lib/project-utils/paths.ts", "../../src/hooks/ts/lib/pai-paths.ts"],
3
+ "sources": ["../../../src/hooks/ts/session-start/inject-observations.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/hooks/ts/lib/pai-paths.ts"],
4
4
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * inject-observations.ts\n *\n * SessionStart hook that injects recent project observation context into Claude's\n * session as a <system-reminder>. Provides progressive disclosure of recent activity\n * so Claude has immediate awareness of what has been happening in this project.\n *\n * Flow:\n * 1. Read session data (session_id, cwd) from stdin\n * 2. Call daemon via IPC: observation_recent with { cwd, limit: 25 }\n * (daemon resolves project_id from cwd internally via registry lookup)\n * 3. Format as progressive disclosure context block\n * 4. Output to stdout as <system-reminder> (injected into session by Claude Code)\n *\n * Silent on any failure \u2014 never blocks session start.\n */\n\nimport { connect } from 'net';\nimport { randomUUID } from 'crypto';\nimport { isProbeSession } from '../lib/project-utils.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HookData {\n session_id?: string;\n cwd?: string;\n hook_event_name?: string;\n}\n\ninterface ObservationRow {\n id: number;\n session_id: string;\n project_id: number | null;\n project_slug: string | null;\n type: string;\n title: string;\n narrative: string | null;\n created_at: string; // ISO string after JSON serialization\n}\n\ninterface ObservationRecentResult {\n rows: ObservationRow[];\n project_slug?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Inline IPC client \u2014 mirrors the pattern in observe.ts\n// Hooks can't import from src/daemon/ at runtime, so we inline this.\n// ---------------------------------------------------------------------------\n\nasync function callDaemon(method: string, params: Record<string, unknown>): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => reject(new Error('timeout')), 5000);\n let buffer = '';\n\n try {\n const socket = connect('/tmp/pai.sock', () => {\n socket.write(JSON.stringify({ id: randomUUID(), method, params }) + '\\n');\n });\n\n socket.on('data', (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf('\\n');\n if (nl !== -1) {\n clearTimeout(timeout);\n try {\n const response = JSON.parse(buffer.slice(0, nl));\n socket.destroy();\n if (response.ok) resolve(response.result);\n else reject(new Error(response.error ?? 'daemon error'));\n } catch (e) {\n socket.destroy();\n reject(e);\n }\n }\n });\n\n socket.on('error', () => {\n clearTimeout(timeout);\n reject(new Error('daemon unavailable'));\n });\n } catch {\n clearTimeout(timeout);\n reject(new Error('daemon unavailable'));\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Time formatting\n// ---------------------------------------------------------------------------\n\nfunction timeAgo(date: Date): string {\n const diffMs = Date.now() - date.getTime();\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHour = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHour / 24);\n\n if (diffSec < 60) return 'just now';\n if (diffMin < 60) return `${diffMin}m ago`;\n if (diffHour < 24) return `${diffHour}h ago`;\n if (diffDay < 7) return `${diffDay}d ago`;\n\n // Older than a week: show date string\n return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });\n}\n\n// ---------------------------------------------------------------------------\n// Type label\n// ---------------------------------------------------------------------------\n\nfunction typeLabel(type: string): string {\n switch (type) {\n case 'change': return '[change]';\n case 'discovery': return '[discovery]';\n case 'decision': return '[decision]';\n case 'bugfix': return '[bugfix]';\n case 'feature': return '[feature]';\n case 'refactor': return '[refactor]';\n default: return `[${type}]`;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Truncate\n// ---------------------------------------------------------------------------\n\nfunction truncate(s: string, maxLen: number): string {\n return s.length > maxLen ? s.slice(0, maxLen - 1) + '\\u2026' : s;\n}\n\n// ---------------------------------------------------------------------------\n// Format observations as progressive disclosure context\n// ---------------------------------------------------------------------------\n\nfunction formatContext(\n projectSlug: string,\n observations: ObservationRow[]\n): string {\n if (observations.length === 0) return '';\n\n // Count distinct sessions\n const sessionSet = new Set(observations.map(o => o.session_id));\n const sessionCount = sessionSet.size;\n\n // Most recent observation\n const newest = new Date(observations[0].created_at);\n const lastActivity = timeAgo(newest);\n\n // Timeline: show most recent 15, keep titles to 80 chars\n const timelineObs = observations.slice(0, 15);\n const timeline = timelineObs\n .map(o => {\n const t = timeAgo(new Date(o.created_at));\n const label = typeLabel(o.type);\n const title = truncate(o.title, 80);\n return `- [${t}] ${label} ${title}`;\n })\n .join('\\n');\n\n const showingNote = observations.length > 15\n ? `(showing most recent 15 of ${observations.length}, use observation_search for more)`\n : `(showing ${observations.length} observation${observations.length !== 1 ? 's' : ''})`;\n\n const lines: string[] = [\n `<system-reminder>`,\n `OBSERVATION CONTEXT (auto-injected)`,\n ``,\n `## Recent Activity (${projectSlug})`,\n `${observations.length} observations across ${sessionCount} session${sessionCount !== 1 ? 's' : ''} | Last activity: ${lastActivity}`,\n ``,\n `### Recent Timeline`,\n timeline,\n showingNote,\n `</system-reminder>`,\n ];\n\n return lines.join('\\n');\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n try {\n // Skip probe/health-check sessions\n if (isProbeSession()) {\n process.exit(0);\n }\n\n // Skip subagent sessions \u2014 they don't need observation context\n const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';\n const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||\n process.env.CLAUDE_AGENT_TYPE !== undefined;\n if (isSubagent) {\n process.exit(0);\n }\n\n // Read hook data from stdin\n let hookData: HookData = {};\n try {\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) {\n hookData = JSON.parse(raw);\n }\n } catch {\n // Non-fatal \u2014 fall back to process.cwd()\n }\n\n const cwd = hookData.cwd || process.cwd();\n\n // Fetch recent observations for this cwd \u2014 daemon resolves the project internally\n let observations: ObservationRow[];\n let projectSlug: string;\n\n try {\n const result = await callDaemon('observation_recent', { cwd, limit: 25 }) as ObservationRecentResult;\n observations = result?.rows ?? [];\n projectSlug = result?.project_slug ?? '';\n } catch {\n // Daemon unavailable or Postgres not configured \u2014 silent exit\n process.exit(0);\n }\n\n if (!observations || observations.length === 0 || !projectSlug) {\n // No data or no matching project \u2014 nothing to inject\n process.exit(0);\n }\n\n // Format and output\n const context = formatContext(projectSlug, observations);\n if (context) {\n // Output to stdout \u2014 Claude Code captures this and injects into session context\n console.log(context);\n }\n\n process.exit(0);\n } catch {\n // Never block session start\n process.exit(0);\n }\n}\n\nmain();\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
5
  "mappings": "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",
6
6
  "names": ["join", "join", "resolve"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/ts/session-start/load-core-context.ts", "../../src/hooks/ts/lib/pai-paths.ts", "../../src/hooks/ts/lib/project-utils/paths.ts"],
3
+ "sources": ["../../../src/hooks/ts/session-start/load-core-context.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts"],
4
4
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * load-core-context.ts\n *\n * Automatically loads your CORE skill context at session start by reading and injecting\n * the CORE SKILL.md file contents directly into Claude's context as a system-reminder.\n *\n * Purpose:\n * - Read CORE SKILL.md file content\n * - Output content as system-reminder for Claude to process\n * - Ensure complete context (contacts, preferences, security, identity) available at session start\n * - Bypass skill activation logic by directly injecting context\n *\n * Setup:\n * 1. Customize your ${PAI_DIR}/skills/CORE/SKILL.md with your personal context\n * 2. Add this hook to settings.json SessionStart hooks\n * 3. Ensure PAI_DIR environment variable is set (defaults to $HOME/.claude)\n *\n * How it works:\n * - Runs at the start of every Claude Code session\n * - Skips execution for subagent sessions (they don't need CORE context)\n * - Reads your CORE SKILL.md file\n * - Injects content as <system-reminder> which Claude processes automatically\n * - Gives your AI immediate access to your complete personal context\n */\n\nimport { readFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, SKILLS_DIR } from '../lib/pai-paths';\nimport { isProbeSession } from '../lib/project-utils';\n\nasync function main() {\n try {\n // Check if this is a subagent session - if so, exit silently\n const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';\n const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||\n process.env.CLAUDE_AGENT_TYPE !== undefined;\n\n if (isSubagent) {\n // Subagent sessions don't need CORE context loading\n console.error('Subagent session - skipping CORE context loading');\n process.exit(0);\n }\n\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n console.error('Probe session detected - skipping CORE context loading');\n process.exit(0);\n }\n\n // Get CORE skill path using PAI paths library\n const coreSkillPath = join(SKILLS_DIR, 'CORE/SKILL.md');\n\n // Verify CORE skill file exists\n if (!existsSync(coreSkillPath)) {\n console.error(`CORE skill not found at: ${coreSkillPath}`);\n console.error(`Ensure CORE/SKILL.md exists or check PAI_DIR environment variable`);\n process.exit(1);\n }\n\n console.error('Reading CORE context from skill file...');\n\n // Read the CORE SKILL.md file content\n let coreContent = readFileSync(coreSkillPath, 'utf-8');\n\n // Perform Dynamic Variable Substitution\n // This allows SKILL.md to be generic while the session is personalized\n const daName = process.env.DA || 'PAI';\n const daColor = process.env.DA_COLOR || 'blue';\n const engineerName = process.env.ENGINEER_NAME || '';\n\n // Replace placeholders {{DA}}, {{DA_COLOR}}, {{ENGINEER_NAME}}\n coreContent = coreContent\n .replace(/\\{\\{DA\\}\\}/g, daName)\n .replace(/\\{\\{DA_COLOR\\}\\}/g, daColor)\n .replace(/\\{\\{ENGINEER_NAME\\}\\}/g, engineerName);\n\n console.error(`Read ${coreContent.length} characters from CORE SKILL.md (Personalized for ${engineerName} & ${daName})`);\n\n // Determine the local timezone dynamically\n const localTimeZone = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n // Output the CORE content as a system-reminder\n // This will be injected into Claude's context at session start\n const message = `<system-reminder>\nPAI CORE CONTEXT (Auto-loaded at Session Start)\n\nCURRENT DATE/TIME: ${new Date().toLocaleString('en-US', { timeZone: localTimeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' })}\n\nThe following context has been loaded from ${coreSkillPath}:\n\n---\n${coreContent}\n---\n\nThis context is now active for this session. Follow all instructions, preferences, and guidelines contained above.\n</system-reminder>`;\n\n // Write to stdout (will be captured by Claude Code)\n console.log(message);\n\n console.error('CORE context injected into session');\n process.exit(0);\n } catch (error) {\n console.error('Error in load-core-context hook:', error);\n process.exit(1);\n }\n}\n\nmain();\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", "/**\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"],
5
5
  "mappings": "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",
6
6
  "names": ["readFileSync", "existsSync", "join", "join", "join", "join", "existsSync", "readFileSync"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/ts/session-start/load-project-context.ts", "../../src/memory/wakeup.ts", "../../src/hooks/ts/lib/project-utils/paths.ts", "../../src/hooks/ts/lib/pai-paths.ts", "../../src/hooks/ts/lib/project-utils/notify.ts", "../../src/hooks/ts/lib/project-utils/session-notes.ts"],
3
+ "sources": ["../../../src/hooks/ts/session-start/load-project-context.ts", "../../../src/memory/wakeup.ts", "../../../src/hooks/ts/lib/project-utils/paths.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/hooks/ts/lib/project-utils/notify.ts", "../../../src/hooks/ts/lib/project-utils/session-notes.ts"],
4
4
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * load-project-context.ts\n *\n * SessionStart hook that sets up project context:\n * - Checks for CLAUDE.md in various locations (Claude Code handles loading)\n * - Sets up Notes/ directory in ~/.claude/projects/{encoded-path}/\n * - Ensures TODO.md exists\n * - Sends ntfy.sh notification (mandatory)\n * - Displays session continuity info (like session-init.sh)\n *\n * This hook complements Claude Code's native CLAUDE.md loading by:\n * - Setting up the Notes infrastructure\n * - Showing the latest session note for continuity\n * - Sending ntfy.sh notifications\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from 'fs';\nimport { join, basename, dirname, resolve } from 'path';\nimport { homedir } from 'os';\nimport { execSync } from 'child_process';\nimport { buildWakeupContext } from '../../../memory/wakeup.js';\nimport {\n PAI_DIR,\n findNotesDir,\n getProjectDir,\n getCurrentNotePath,\n createSessionNote,\n findTodoPath,\n findAllClaudeMdPaths,\n sendNtfyNotification,\n isProbeSession\n} from '../lib/project-utils';\n\n/**\n * Find the pai CLI binary path dynamically.\n * Tries `which pai` first, then common fallback locations.\n */\nfunction findPaiBinary(): string {\n try {\n return execSync('which pai', { encoding: 'utf-8' }).trim();\n } catch {\n // Fallback locations in order of preference\n const fallbacks = [\n '/usr/local/bin/pai',\n '/opt/homebrew/bin/pai',\n `${process.env.HOME}/.local/bin/pai`,\n ];\n for (const p of fallbacks) {\n if (existsSync(p)) return p;\n }\n }\n return 'pai'; // Last resort: rely on PATH at runtime\n}\n\n/**\n * Check session-routing.json for an active route.\n * Returns the routed Notes path if set, or null to use default behavior.\n */\nfunction getRoutedNotesPath(): string | null {\n const routingFile = join(PAI_DIR, 'session-routing.json');\n if (!existsSync(routingFile)) return null;\n\n try {\n const routing = JSON.parse(readFileSync(routingFile, 'utf-8'));\n const active = routing?.active_session;\n if (active?.notes_path) {\n return active.notes_path;\n }\n } catch {\n // Ignore parse errors\n }\n return null;\n}\n\n/**\n * Project signals that indicate a directory is a real project root.\n */\nconst PROJECT_SIGNALS = [\n '.git',\n 'package.json',\n 'pubspec.yaml',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'setup.py',\n 'build.gradle',\n 'pom.xml',\n 'composer.json',\n 'Gemfile',\n 'Makefile',\n 'CMakeLists.txt',\n 'tsconfig.json',\n 'CLAUDE.md',\n join('Notes', 'PAI.md'),\n];\n\n/**\n * Returns true if the given directory looks like a project root.\n * Checks for the presence of well-known project signal files/dirs.\n */\nfunction hasProjectSignals(dir: string): boolean {\n for (const signal of PROJECT_SIGNALS) {\n if (existsSync(join(dir, signal))) return true;\n }\n return false;\n}\n\n/**\n * Returns true if the directory should NOT be auto-registered.\n * Guards: home directory, shallow paths, temp directories.\n */\nfunction isGuardedPath(dir: string): boolean {\n const home = homedir();\n const resolved = resolve(dir);\n\n // Never register the home directory itself\n if (resolved === home) return true;\n\n // Depth guard: require at least 3 path segments beyond root\n // e.g. /Users/i052341/foo is depth 3 on macOS \u2014 reject it\n const parts = resolved.split('/').filter(Boolean);\n if (parts.length < 3) return true;\n\n // Temp/system directories\n const forbidden = ['/tmp', '/var', '/private/tmp', '/private/var/folders'];\n for (const prefix of forbidden) {\n if (resolved === prefix || resolved.startsWith(prefix + '/')) return true;\n }\n\n return false;\n}\n\ninterface HookInput {\n session_id: string;\n cwd: string;\n hook_event_name: string;\n}\n\nasync function main() {\n console.error('\\nload-project-context.ts starting...');\n\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n console.error('Probe session detected - skipping project context loading');\n process.exit(0);\n }\n\n // Read hook input from stdin\n let hookInput: HookInput | null = null;\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (input.trim()) {\n hookInput = JSON.parse(input);\n }\n } catch (error) {\n console.error('Could not parse hook input, using process.cwd()');\n }\n\n // Get current working directory\n const cwd = hookInput?.cwd || process.cwd();\n\n // Determine meaningful project name\n // If cwd is a Notes directory, use parent directory name instead\n let projectName = basename(cwd);\n if (projectName.toLowerCase() === 'notes') {\n projectName = basename(dirname(cwd));\n }\n\n console.error(`Working directory: ${cwd}`);\n console.error(`Project: ${projectName}`);\n\n // Check if this is a subagent session - skip for subagents\n const isSubagent = process.env.CLAUDE_AGENT_TYPE !== undefined ||\n (process.env.CLAUDE_PROJECT_DIR || '').includes('/.claude/agents/');\n\n if (isSubagent) {\n console.error('Subagent session - skipping project context setup');\n process.exit(0);\n }\n\n // 1. Find and READ all CLAUDE.md files - inject them into context\n // This ensures Claude actually processes the instructions, not just sees them in headers\n const claudeMdPaths = findAllClaudeMdPaths(cwd);\n const claudeMdContents: { path: string; content: string }[] = [];\n\n if (claudeMdPaths.length > 0) {\n console.error(`Found ${claudeMdPaths.length} CLAUDE.md file(s):`);\n for (const path of claudeMdPaths) {\n console.error(` - ${path}`);\n try {\n const content = readFileSync(path, 'utf-8');\n claudeMdContents.push({ path, content });\n console.error(` Read ${content.length} chars`);\n } catch (error) {\n console.error(` Could not read: ${error}`);\n }\n }\n } else {\n console.error('No CLAUDE.md found in project');\n console.error(' Consider creating one at ./CLAUDE.md or ./.claude/CLAUDE.md');\n }\n\n // 2. Find or create Notes directory\n // Priority:\n // 1. Active session routing (pai route <project>) \u2192 routed Obsidian path\n // 2. Local Notes/ in cwd \u2192 use it (git-trackable, e.g. symlink to Obsidian)\n // 3. Central ~/.claude/projects/.../Notes/ \u2192 fallback\n const routedPath = getRoutedNotesPath();\n let notesDir: string;\n\n if (routedPath) {\n // Routing is active - use the configured Obsidian Notes path\n const { mkdirSync } = await import('fs');\n if (!existsSync(routedPath)) {\n mkdirSync(routedPath, { recursive: true });\n console.error(`Created routed Notes: ${routedPath}`);\n } else {\n console.error(`Notes directory: ${routedPath} (routed via pai route)`);\n }\n notesDir = routedPath;\n } else {\n const notesInfo = findNotesDir(cwd);\n\n if (notesInfo.isLocal) {\n notesDir = notesInfo.path;\n console.error(`Notes directory: ${notesDir} (local)`);\n } else {\n // Create central Notes directory\n if (!existsSync(notesInfo.path)) {\n const { mkdirSync } = await import('fs');\n mkdirSync(notesInfo.path, { recursive: true });\n console.error(`Created central Notes: ${notesInfo.path}`);\n } else {\n console.error(`Notes directory: ${notesInfo.path} (central)`);\n }\n notesDir = notesInfo.path;\n }\n }\n\n // 3. Cleanup old .jsonl files from project root (move to sessions/)\n // Keep the newest one for potential resume, move older ones to sessions/\n const projectDir = getProjectDir(cwd);\n if (existsSync(projectDir)) {\n try {\n const files = readdirSync(projectDir);\n const jsonlFiles = files\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({\n name: f,\n path: join(projectDir, f),\n mtime: statSync(join(projectDir, f)).mtime.getTime()\n }))\n .sort((a, b) => b.mtime - a.mtime); // newest first\n\n if (jsonlFiles.length > 1) {\n const { mkdirSync, renameSync } = await import('fs');\n const sessionsDir = join(projectDir, 'sessions');\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n }\n\n // Move all except the newest\n for (let i = 1; i < jsonlFiles.length; i++) {\n const file = jsonlFiles[i];\n const destPath = join(sessionsDir, file.name);\n if (!existsSync(destPath)) {\n renameSync(file.path, destPath);\n console.error(`Moved old session: ${file.name} \u2192 sessions/`);\n }\n }\n }\n } catch (error) {\n console.error(`Could not cleanup old .jsonl files: ${error}`);\n }\n }\n\n // 4. Find or create TODO.md\n const todoPath = findTodoPath(cwd);\n const hasTodo = existsSync(todoPath);\n if (hasTodo) {\n console.error(`TODO.md: ${todoPath}`);\n } else {\n // Create TODO.md in the Notes directory\n const newTodoPath = join(notesDir, 'TODO.md');\n const { writeFileSync } = await import('fs');\n writeFileSync(newTodoPath, `# TODO\\n\\n## Offen\\n\\n- [ ] \\n\\n---\\n\\n*Created: ${new Date().toISOString()}*\\n`);\n console.error(`Created TODO.md: ${newTodoPath}`);\n }\n\n // 5. Check for existing note or create new one\n let activeNotePath: string | null = null;\n\n if (notesDir) { // notesDir is always set now (local or central)\n const currentNotePath = getCurrentNotePath(notesDir);\n\n // Only create a new note if there is truly no note at all.\n // A completed note is still used \u2014 it will be updated or continued.\n // This prevents duplicate notes at month boundaries and on every compaction.\n if (!currentNotePath) {\n // Defensive: ensure projectName is a usable string\n const safeProjectName = (typeof projectName === 'string' && projectName.trim().length > 0)\n ? projectName.trim()\n : 'Untitled Session';\n console.error('\\nNo previous session notes found - creating new one');\n activeNotePath = createSessionNote(notesDir, String(safeProjectName));\n console.error(`Created: ${basename(activeNotePath)}`);\n } else {\n activeNotePath = currentNotePath!;\n console.error(`\\nUsing existing session note: ${basename(activeNotePath)}`);\n // Show preview of current note\n try {\n const content = readFileSync(activeNotePath, 'utf-8');\n const lines = content.split('\\n').slice(0, 12);\n console.error('--- Current Note Preview ---');\n for (const line of lines) {\n console.error(line);\n }\n console.error('--- End Preview ---\\n');\n } catch {\n // Ignore read errors\n }\n }\n }\n\n // 6. Show TODO.md preview\n if (existsSync(todoPath)) {\n try {\n const todoContent = readFileSync(todoPath, 'utf-8');\n const todoLines = todoContent.split('\\n').filter(l => l.includes('[ ]')).slice(0, 5);\n if (todoLines.length > 0) {\n console.error('\\nOpen TODOs:');\n for (const line of todoLines) {\n console.error(` ${line.trim()}`);\n }\n }\n } catch {\n // Ignore read errors\n }\n }\n\n // 7. Send ntfy.sh notification (MANDATORY)\n await sendNtfyNotification(`Session started in ${projectName}`);\n\n // 7.5. Run pai project detect to identify the registered PAI project\n const paiBin = findPaiBinary();\n let paiProjectBlock = '';\n try {\n const { execFileSync } = await import('child_process');\n const raw = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n encoding: 'utf-8',\n env: process.env,\n }).trim();\n\n if (raw) {\n const detected = JSON.parse(raw) as {\n slug?: string;\n display_name?: string;\n root_path?: string;\n match_type?: string;\n relative_path?: string | null;\n session_count?: number;\n status?: string;\n error?: string;\n cwd?: string;\n };\n\n /**\n * Attempt to auto-register the CWD as a new PAI project.\n * Calls `pai project add <cwd>`, then re-detects to confirm registration.\n * Returns true if registration succeeded (or was attempted and project add ran),\n * and sets paiProjectBlock as a side effect on success.\n */\n const tryAutoRegister = async (): Promise<boolean> => {\n if (isGuardedPath(cwd) || !hasProjectSignals(cwd)) return false;\n\n try {\n execFileSync(paiBin, ['project', 'add', cwd], {\n encoding: 'utf-8',\n env: process.env,\n });\n console.error(`PAI auto-registered project at: ${cwd}`);\n\n // Re-run detect to confirm registration\n try {\n const raw2 = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n encoding: 'utf-8',\n env: process.env,\n }).trim();\n\n if (raw2) {\n const detected2 = JSON.parse(raw2) as typeof detected;\n if (detected2.slug) {\n const name2 = detected2.display_name || detected2.slug;\n console.error(`PAI auto-registered: \"${detected2.slug}\" (${detected2.match_type})`);\n paiProjectBlock = `PAI Project Registry: ${name2} (slug: ${detected2.slug}) [AUTO-REGISTERED]\nMatch: ${detected2.match_type ?? 'exact'} | Sessions: 0`;\n return true;\n }\n }\n } catch (detectErr) {\n console.error('PAI auto-registration: project added but re-detect failed:', detectErr);\n return true; // project IS registered, just can't load context\n }\n } catch (addErr) {\n console.error('PAI auto-registration failed (project add):', addErr);\n }\n return false;\n };\n\n if (detected.error === 'no_match') {\n // Attempt auto-registration if the directory looks like a real project\n const autoRegistered = await tryAutoRegister();\n\n if (!autoRegistered) {\n paiProjectBlock = `PAI Project Registry: No registered project matches this directory.\nRun \"pai project add .\" to register this project, or use /route to tag the session.`;\n console.error('PAI detect: no match for', cwd);\n }\n } else if (\n detected.match_type === 'parent' &&\n detected.relative_path &&\n !isGuardedPath(cwd) &&\n hasProjectSignals(cwd)\n ) {\n // The CWD is inside a broader registered parent (e.g. \"i052341\" or \"apps\"),\n // but it has its own project signals \u2014 register it as a distinct project.\n console.error(\n `PAI detect: parent match to \"${detected.slug}\" via relative path \"${detected.relative_path}\" \u2014 CWD looks like its own project, attempting auto-registration`\n );\n const autoRegistered = await tryAutoRegister();\n\n if (!autoRegistered) {\n // Fall through: show the parent match as normal\n const name = detected.display_name || detected.slug;\n const nameSlug = ` (slug: ${detected.slug})`;\n const matchDesc = `parent (+${detected.relative_path ?? ''})`;\n const statusFlag = detected.status && detected.status !== 'active'\n ? ` [${detected.status.toUpperCase()}]`\n : '';\n paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}\nMatch: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== 'active' ? `\\nWARNING: Project status is \"${detected.status}\". Run: pai project health --fix` : ''}`;\n console.error(`PAI detect: kept parent match \"${detected.slug}\" (auto-register not applicable)`);\n }\n } else if (detected.slug) {\n const name = detected.display_name || detected.slug;\n const nameSlug = ` (slug: ${detected.slug})`;\n const matchDesc = detected.match_type === 'exact'\n ? 'exact'\n : `parent (+${detected.relative_path ?? ''})`;\n const statusFlag = detected.status && detected.status !== 'active'\n ? ` [${detected.status.toUpperCase()}]`\n : '';\n paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}\nMatch: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== 'active' ? `\\nWARNING: Project status is \"${detected.status}\". Run: pai project health --fix` : ''}`;\n console.error(`PAI detect: matched \"${detected.slug}\" (${detected.match_type})`);\n }\n }\n } catch (e) {\n // Non-fatal \u2014 don't break session start if pai is unavailable\n console.error('pai project detect failed:', e);\n }\n\n // 8. Output system reminder with session info\n const reminder = `\n<system-reminder>\nPROJECT CONTEXT LOADED\n\nProject: ${projectName}\nWorking Directory: ${cwd}\n${notesDir ? `Notes Directory: ${notesDir}${routedPath ? ' (routed via pai route)' : ''}` : 'Notes: disabled (no local Notes/ directory)'}\n${hasTodo ? `TODO: ${todoPath}` : 'TODO: not found'}\n${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(', ')}` : 'No CLAUDE.md found'}\n${activeNotePath ? `Active Note: ${basename(activeNotePath)}` : ''}\n${routedPath ? `\\nNote Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ''}\n${paiProjectBlock ? `\\n${paiProjectBlock}` : ''}\nSession Commands:\n- \"pause session\" \u2192 Save checkpoint, update TODO, exit (no compact)\n- \"end session\" \u2192 Finalize note, commit if needed, start fresh next time\n- \"pai route clear\" \u2192 Clear note routing (in a new session)\n</system-reminder>\n`;\n\n // Output to stdout for Claude to receive\n console.log(reminder);\n\n // 9. INJECT CLAUDE.md contents as system-reminders\n // This ensures Claude actually reads and processes the instructions\n for (const { path, content } of claudeMdContents) {\n const claudeMdReminder = `\n<system-reminder>\nLOCAL CLAUDE.md LOADED (MANDATORY - READ AND FOLLOW)\n\nSource: ${path}\n\n${content}\n\n---\nTHE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.\n</system-reminder>\n`;\n console.log(claudeMdReminder);\n console.error(`Injected CLAUDE.md content from: ${path}`);\n }\n\n // 10. Inject wake-up context (L0 identity + L1 essential story) if available\n // The detected PAI project root_path is used for L1 note lookup.\n // We derive it from the `paiProjectBlock` detection result by re-running a\n // lightweight registry lookup, or by falling back to cwd for local-notes projects.\n try {\n // Attempt to find the project root path from the registry via `pai project detect --json`\n let wakeupRootPath: string | undefined;\n try {\n const { execFileSync: efs } = await import('child_process');\n const raw2 = efs(paiBin, ['project', 'detect', '--json', cwd], {\n encoding: 'utf-8',\n env: process.env,\n }).trim();\n if (raw2) {\n const det = JSON.parse(raw2) as { root_path?: string; slug?: string };\n if (det.root_path) wakeupRootPath = det.root_path;\n }\n } catch {\n // Non-fatal \u2014 fall back to cwd\n wakeupRootPath = cwd;\n }\n\n const wakeupBlock = buildWakeupContext(wakeupRootPath);\n if (wakeupBlock) {\n const wakeupReminder = `\\n<system-reminder>\\nWAKEUP CONTEXT\\n\\n${wakeupBlock}\\n</system-reminder>\\n`;\n console.log(wakeupReminder);\n console.error('Injected wake-up context (L0+L1)');\n } else {\n console.error('No wake-up context to inject (no identity file or session notes)');\n }\n } catch (wakeupError) {\n // Non-fatal \u2014 don't block session start\n console.error('Wake-up context injection failed:', wakeupError);\n }\n\n console.error('\\nProject context setup complete\\n');\n process.exit(0);\n}\n\nmain().catch(error => {\n console.error('load-project-context.ts error:', error);\n process.exit(0); // Don't block session start\n});\n", "/**\n * Wake-up context system \u2014 progressive context loading inspired by mempalace.\n *\n * Layers:\n * L0 Identity (~100 tokens) \u2014 user identity from ~/.pai/identity.txt. Always loaded.\n * L1 Essential Story (~500-800t) \u2014 top session notes for the project, key lines extracted.\n * L2 On-Demand \u2014 triggered by topic queries (handled by memory_search).\n * L3 Deep Search \u2014 unlimited federated memory search (memory_search tool).\n */\n\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum tokens for the L1 essential story block. Approx 4 chars/token. */\nconst L1_TOKEN_BUDGET = 800;\nconst L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4; // ~3200 chars\n\n/** Maximum session notes to scan when building L1. */\nconst L1_MAX_NOTES = 10;\n\n/** Sections to extract from session notes (in priority order). */\nconst EXTRACT_SECTIONS = [\n \"Work Done\",\n \"Key Decisions\",\n \"Next Steps\",\n \"Checkpoint\",\n];\n\n/** Identity file location. */\nconst IDENTITY_FILE = join(homedir(), \".pai\", \"identity.txt\");\n\n// ---------------------------------------------------------------------------\n// L0: Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Load L0 identity from ~/.pai/identity.txt.\n * Returns the file content, or an empty string if the file does not exist.\n * Never throws.\n */\nexport function loadL0Identity(): string {\n if (!existsSync(IDENTITY_FILE)) return \"\";\n try {\n return readFileSync(IDENTITY_FILE, \"utf-8\").trim();\n } catch {\n return \"\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// L1: Essential Story\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for a project given its root_path from the registry.\n * Checks local Notes/ first, then central ~/.claude/projects/... path.\n */\nfunction findNotesDirForProject(rootPath: string): string | null {\n // Check local Notes directories first\n const localCandidates = [\n join(rootPath, \"Notes\"),\n join(rootPath, \"notes\"),\n join(rootPath, \".claude\", \"Notes\"),\n ];\n for (const p of localCandidates) {\n if (existsSync(p)) return p;\n }\n\n // Fall back to central ~/.claude/projects/{encoded}/Notes\n const encoded = rootPath\n .replace(/\\//g, \"-\")\n .replace(/\\./g, \"-\")\n .replace(/ /g, \"-\");\n const centralNotes = join(\n homedir(),\n \".claude\",\n \"projects\",\n encoded,\n \"Notes\"\n );\n if (existsSync(centralNotes)) return centralNotes;\n\n return null;\n}\n\n/**\n * Recursively find all .md session note files in a Notes directory.\n * Handles both flat layout (Notes/*.md) and month-subdirectory layout\n * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename\n * (note numbers are monotonically increasing, so lexicographic = newest-last,\n * so we reverse).\n */\nfunction findSessionNotes(notesDir: string): string[] {\n const result: string[] = [];\n\n const scanDir = (dir: string) => {\n if (!existsSync(dir)) return;\n let entries: string[];\n try {\n entries = readdirSync(dir, { withFileTypes: true } as Parameters<typeof readdirSync>[1] as any)\n .map((e: any) => ({ name: e.name, isDir: e.isDirectory() }));\n } catch {\n return;\n }\n\n for (const entry of entries as Array<{ name: string; isDir: boolean }>) {\n const fullPath = join(dir, entry.name);\n if (entry.isDir) {\n // Recurse into YYYY/MM subdirectories\n scanDir(fullPath);\n } else if (entry.name.match(/^\\d{3,4}[\\s_-].*\\.md$/)) {\n result.push(fullPath);\n }\n }\n };\n\n scanDir(notesDir);\n\n // Sort: extract leading number, highest = most recent\n result.sort((a, b) => {\n const numA = parseInt(basename(a).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const numB = parseInt(basename(b).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n return numB - numA; // newest first\n });\n\n return result;\n}\n\n/**\n * Extract the most important lines from a session note.\n * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.\n * Returns a condensed string under maxChars.\n */\nfunction extractKeyLines(content: string, maxChars: number): string {\n const lines = content.split(\"\\n\");\n const selected: string[] = [];\n let inTargetSection = false;\n let currentSection = \"\";\n let charCount = 0;\n\n // First pass: collect lines from priority sections\n for (const line of lines) {\n // Detect section headers\n const h2Match = line.match(/^## (.+)$/);\n const h3Match = line.match(/^### (.+)$/);\n if (h2Match) {\n currentSection = h2Match[1];\n inTargetSection = EXTRACT_SECTIONS.some((s) =>\n currentSection.toLowerCase().includes(s.toLowerCase())\n );\n continue;\n }\n if (h3Match) {\n // Checkpoints / sub-sections \u2014 include heading as label\n if (inTargetSection) {\n const label = `[${h3Match[1]}]`;\n if (charCount + label.length < maxChars) {\n selected.push(label);\n charCount += label.length + 1;\n }\n }\n continue;\n }\n\n if (!inTargetSection) continue;\n\n // Skip blank lines and HTML comments\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"<!--\") || trimmed === \"---\") continue;\n\n // Include checkbox items, bold text, and plain text lines\n if (\n trimmed.startsWith(\"- \") ||\n trimmed.startsWith(\"* \") ||\n trimmed.match(/^\\d+\\./) ||\n trimmed.startsWith(\"**\")\n ) {\n if (charCount + trimmed.length + 1 > maxChars) break;\n selected.push(trimmed);\n charCount += trimmed.length + 1;\n }\n }\n\n return selected.join(\"\\n\");\n}\n\n/**\n * Build the L1 essential story block.\n *\n * Reads the most recent session notes for the project and extracts the key\n * lines (Work Done, Key Decisions, Next Steps) within the token budget.\n *\n * @param rootPath The project root path (from the registry).\n * @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).\n * @returns Formatted L1 block, or empty string if no notes found.\n */\nexport function buildL1EssentialStory(\n rootPath: string,\n tokenBudget = L1_TOKEN_BUDGET\n): string {\n const charBudget = tokenBudget * 4;\n const notesDir = findNotesDirForProject(rootPath);\n if (!notesDir) return \"\";\n\n const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);\n if (noteFiles.length === 0) return \"\";\n\n const sections: string[] = [];\n let remaining = charBudget;\n\n for (const noteFile of noteFiles) {\n if (remaining <= 50) break;\n\n let content: string;\n try {\n content = readFileSync(noteFile, \"utf-8\");\n } catch {\n continue;\n }\n\n // Extract the note date and title from the filename\n const name = basename(noteFile);\n const titleMatch = name.match(/^\\d+ - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/);\n const dateLabel = titleMatch ? titleMatch[1] : \"\";\n const titleLabel = titleMatch\n ? titleMatch[2]\n : name.replace(/^\\d+ - /, \"\").replace(/\\.md$/, \"\");\n\n // Skip if nothing useful extracted from this note\n const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);\n const extracted = extractKeyLines(content, perNoteChars);\n if (!extracted) continue;\n\n const noteBlock = `[${dateLabel} - ${titleLabel}]\\n${extracted}`;\n sections.push(noteBlock);\n remaining -= noteBlock.length + 1;\n }\n\n if (sections.length === 0) return \"\";\n\n return sections.join(\"\\n\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Combined: buildWakeupContext\n// ---------------------------------------------------------------------------\n\n/**\n * Build the combined wake-up context block (L0 + L1).\n *\n * Returns a formatted string suitable for injection as a system-reminder,\n * or an empty string if both layers are empty.\n *\n * @param rootPath Project root path for L1 note lookup. Optional.\n * @param tokenBudget L1 token budget. Default 800.\n */\nexport function buildWakeupContext(\n rootPath?: string,\n tokenBudget = L1_TOKEN_BUDGET\n): string {\n const identity = loadL0Identity();\n const essentialStory = rootPath\n ? buildL1EssentialStory(rootPath, tokenBudget)\n : \"\";\n\n if (!identity && !essentialStory) return \"\";\n\n const parts: string[] = [];\n\n if (identity) {\n parts.push(`## L0 Identity\\n\\n${identity}`);\n }\n\n if (essentialStory) {\n parts.push(`## L1 Essential Story\\n\\n${essentialStory}`);\n }\n\n return parts.join(\"\\n\\n\");\n}\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", "/**\n * Push notification helpers \u2014 WhatsApp-aware with ntfy.sh fallback.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\n/**\n * Check if a messaging MCP server (AIBroker, Whazaa, or Telex) is configured.\n * When any messaging server is active, the AI handles notifications via MCP\n * and ntfy is skipped to avoid duplicates.\n */\nexport function isWhatsAppEnabled(): boolean {\n try {\n const settingsPath = join(homedir(), '.claude', 'settings.json');\n if (!existsSync(settingsPath)) return false;\n\n const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n const enabled: string[] = settings.enabledMcpjsonServers || [];\n return enabled.includes('aibroker') || enabled.includes('whazaa') || enabled.includes('telex');\n } catch {\n return false;\n }\n}\n\n/**\n * Send push notification \u2014 WhatsApp-aware with ntfy fallback.\n *\n * When WhatsApp (Whazaa) is enabled in MCP config, ntfy is SKIPPED\n * because the AI sends WhatsApp messages directly via MCP.\n * When WhatsApp is NOT configured, ntfy fires as the fallback channel.\n */\nexport async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {\n if (isWhatsAppEnabled()) {\n console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);\n return true;\n }\n\n const topic = process.env.NTFY_TOPIC;\n\n if (!topic) {\n console.error('NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled');\n return false;\n }\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const response = await fetch(`https://ntfy.sh/${topic}`, {\n method: 'POST',\n body: message,\n headers: {\n 'Title': 'Claude Code',\n 'Priority': 'default',\n },\n });\n\n if (response.ok) {\n console.error(`ntfy.sh notification sent (WhatsApp inactive): \"${message}\"`);\n return true;\n } else {\n console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);\n }\n } catch (error) {\n console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);\n }\n\n if (attempt < retries) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n\n console.error('ntfy.sh notification failed after all retries');\n return false;\n}\n", "/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month \u2192 previous month \u2192 flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.length < 5) return true; // too short to be meaningful\n if (t.startsWith('/') || t.startsWith('~')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n if (/^<[a-z-]+[\\s/>]/i.test(t)) return true; // XML/HTML tags (<task-notification>, etc.)\n if (/^[0-9a-f]{10,}$/i.test(t)) return true; // hex hash strings\n if (/^Exit code \\d+/i.test(t)) return true; // exit code messages\n if (/^Error:/i.test(t)) return true; // error messages\n if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n if (/^\\(Bash completed/i.test(t)) return true; // bash output noise\n if (/^Task Notification$/i.test(t)) return true; // literal \"Task Notification\"\n if (/^New Session$/i.test(t)) return true; // placeholder title\n if (/^Recovered Session$/i.test(t)) return true; // placeholder title\n if (/^Continued Session$/i.test(t)) return true; // placeholder title\n if (/^Untitled Session$/i.test(t)) return true; // placeholder title\n if (/^Context Compression$/i.test(t)) return true; // compression artifact\n if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true; // hash + \"Output\" pattern\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note \u2014 mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n"],
5
5
  "mappings": ";;;AAkBA,SAAS,cAAAA,aAAY,eAAAC,cAAa,gBAAAC,eAAc,gBAAgB;AAChE,SAAS,QAAAC,OAAM,YAAAC,WAAU,SAAS,WAAAC,gBAAe;AACjD,SAAS,WAAAC,gBAAe;AACxB,SAAS,gBAAgB;;;ACXzB,SAAS,YAAY,aAAa,oBAAoB;AACtD,SAAS,MAAM,gBAAgB;AAC/B,SAAS,eAAe;AAOxB,IAAM,kBAAkB;AACxB,IAAM,iBAAiB,kBAAkB;AAGzC,IAAM,eAAe;AAGrB,IAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,gBAAgB,KAAK,QAAQ,GAAG,QAAQ,cAAc;AAWrD,SAAS,iBAAyB;AACvC,MAAI,CAAC,WAAW,aAAa,EAAG,QAAO;AACvC,MAAI;AACF,WAAO,aAAa,eAAe,OAAO,EAAE,KAAK;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUA,SAAS,uBAAuB,UAAiC;AAE/D,QAAM,kBAAkB;AAAA,IACtB,KAAK,UAAU,OAAO;AAAA,IACtB,KAAK,UAAU,OAAO;AAAA,IACtB,KAAK,UAAU,WAAW,OAAO;AAAA,EACnC;AACA,aAAW,KAAK,iBAAiB;AAC/B,QAAI,WAAW,CAAC,EAAG,QAAO;AAAA,EAC5B;AAGA,QAAM,UAAU,SACb,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,GAAG;AACpB,QAAM,eAAe;AAAA,IACnB,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,WAAW,YAAY,EAAG,QAAO;AAErC,SAAO;AACT;AASA,SAAS,iBAAiB,UAA4B;AACpD,QAAM,SAAmB,CAAC;AAE1B,QAAM,UAAU,CAAC,QAAgB;AAC/B,QAAI,CAAC,WAAW,GAAG,EAAG;AACtB,QAAI;AACJ,QAAI;AACF,gBAAU,YAAY,KAAK,EAAE,eAAe,KAAK,CAA6C,EAC3F,IAAI,CAAC,OAAY,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,YAAY,EAAE,EAAE;AAAA,IAC/D,QAAQ;AACN;AAAA,IACF;AAEA,eAAW,SAAS,SAAoD;AACtE,YAAM,WAAW,KAAK,KAAK,MAAM,IAAI;AACrC,UAAI,MAAM,OAAO;AAEf,gBAAQ,QAAQ;AAAA,MAClB,WAAW,MAAM,KAAK,MAAM,uBAAuB,GAAG;AACpD,eAAO,KAAK,QAAQ;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,UAAQ,QAAQ;AAGhB,SAAO,KAAK,CAAC,GAAG,MAAM;AACpB,UAAM,OAAO,SAAS,SAAS,CAAC,EAAE,MAAM,QAAQ,IAAI,CAAC,KAAK,KAAK,EAAE;AACjE,UAAM,OAAO,SAAS,SAAS,CAAC,EAAE,MAAM,QAAQ,IAAI,CAAC,KAAK,KAAK,EAAE;AACjE,WAAO,OAAO;AAAA,EAChB,CAAC;AAED,SAAO;AACT;AAOA,SAAS,gBAAgB,SAAiB,UAA0B;AAClE,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,WAAqB,CAAC;AAC5B,MAAI,kBAAkB;AACtB,MAAI,iBAAiB;AACrB,MAAI,YAAY;AAGhB,aAAW,QAAQ,OAAO;AAExB,UAAM,UAAU,KAAK,MAAM,WAAW;AACtC,UAAM,UAAU,KAAK,MAAM,YAAY;AACvC,QAAI,SAAS;AACX,uBAAiB,QAAQ,CAAC;AAC1B,wBAAkB,iBAAiB;AAAA,QAAK,CAAC,MACvC,eAAe,YAAY,EAAE,SAAS,EAAE,YAAY,CAAC;AAAA,MACvD;AACA;AAAA,IACF;AACA,QAAI,SAAS;AAEX,UAAI,iBAAiB;AACnB,cAAM,QAAQ,IAAI,QAAQ,CAAC,CAAC;AAC5B,YAAI,YAAY,MAAM,SAAS,UAAU;AACvC,mBAAS,KAAK,KAAK;AACnB,uBAAa,MAAM,SAAS;AAAA,QAC9B;AAAA,MACF;AACA;AAAA,IACF;AAEA,QAAI,CAAC,gBAAiB;AAGtB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,WAAW,QAAQ,WAAW,MAAM,KAAK,YAAY,MAAO;AAGjE,QACE,QAAQ,WAAW,IAAI,KACvB,QAAQ,WAAW,IAAI,KACvB,QAAQ,MAAM,QAAQ,KACtB,QAAQ,WAAW,IAAI,GACvB;AACA,UAAI,YAAY,QAAQ,SAAS,IAAI,SAAU;AAC/C,eAAS,KAAK,OAAO;AACrB,mBAAa,QAAQ,SAAS;AAAA,IAChC;AAAA,EACF;AAEA,SAAO,SAAS,KAAK,IAAI;AAC3B;AAYO,SAAS,sBACd,UACA,cAAc,iBACN;AACR,QAAM,aAAa,cAAc;AACjC,QAAM,WAAW,uBAAuB,QAAQ;AAChD,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,YAAY,iBAAiB,QAAQ,EAAE,MAAM,GAAG,YAAY;AAClE,MAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,QAAM,WAAqB,CAAC;AAC5B,MAAI,YAAY;AAEhB,aAAW,YAAY,WAAW;AAChC,QAAI,aAAa,GAAI;AAErB,QAAI;AACJ,QAAI;AACF,gBAAU,aAAa,UAAU,OAAO;AAAA,IAC1C,QAAQ;AACN;AAAA,IACF;AAGA,UAAM,OAAO,SAAS,QAAQ;AAC9B,UAAM,aAAa,KAAK,MAAM,wCAAwC;AACtE,UAAM,YAAY,aAAa,WAAW,CAAC,IAAI;AAC/C,UAAM,aAAa,aACf,WAAW,CAAC,IACZ,KAAK,QAAQ,WAAW,EAAE,EAAE,QAAQ,SAAS,EAAE;AAGnD,UAAM,eAAe,KAAK,IAAI,WAAW,KAAK,MAAM,aAAa,UAAU,MAAM,IAAI,GAAG;AACxF,UAAM,YAAY,gBAAgB,SAAS,YAAY;AACvD,QAAI,CAAC,UAAW;AAEhB,UAAM,YAAY,IAAI,SAAS,MAAM,UAAU;AAAA,EAAM,SAAS;AAC9D,aAAS,KAAK,SAAS;AACvB,iBAAa,UAAU,SAAS;AAAA,EAClC;AAEA,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,SAAO,SAAS,KAAK,MAAM;AAC7B;AAeO,SAAS,mBACd,UACA,cAAc,iBACN;AACR,QAAM,WAAW,eAAe;AAChC,QAAM,iBAAiB,WACnB,sBAAsB,UAAU,WAAW,IAC3C;AAEJ,MAAI,CAAC,YAAY,CAAC,eAAgB,QAAO;AAEzC,QAAM,QAAkB,CAAC;AAEzB,MAAI,UAAU;AACZ,UAAM,KAAK;AAAA;AAAA,EAAqB,QAAQ,EAAE;AAAA,EAC5C;AAEA,MAAI,gBAAgB;AAClB,UAAM,KAAK;AAAA;AAAA,EAA4B,cAAc,EAAE;AAAA,EACzD;AAEA,SAAO,MAAM,KAAK,MAAM;AAC1B;;;ACvRA,SAAS,cAAAC,aAAY,WAAW,eAAAC,cAAa,kBAAkB;AAC/D,SAAS,QAAAC,OAAM,YAAAC,iBAAgB;;;ACQ/B,SAAS,WAAAC,gBAAe;AACxB,SAAS,SAAS,QAAAC,aAAY;AAC9B,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AAMzC,SAAS,cAAoB;AAE3B,QAAM,gBAAgB;AAAA,IACpB,QAAQ,QAAQ,IAAI,WAAW,IAAI,MAAM;AAAA,IACzC,QAAQH,SAAQ,GAAG,WAAW,MAAM;AAAA,EACtC;AAEA,aAAW,WAAW,eAAe;AACnC,QAAIE,YAAW,OAAO,GAAG;AACvB,UAAI;AACF,cAAM,UAAUC,cAAa,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,WAAWH,SAAQ,CAAC;AAC1C,oBAAQ,MAAM,QAAQ,cAAcA,SAAQ,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,QAAQA,SAAQ,GAAG,SAAS;AAKzB,IAAM,YAAYC,MAAK,SAAS,OAAO;AACvC,IAAM,aAAaA,MAAK,SAAS,QAAQ;AACzC,IAAM,aAAaA,MAAK,SAAS,QAAQ;AACzC,IAAM,cAAcA,MAAK,SAAS,SAAS;AAC3C,IAAM,eAAeA,MAAK,SAAS,UAAU;AAMpD,SAAS,uBAA6B;AACpC,MAAI,CAACC,YAAW,OAAO,GAAG;AACxB,YAAQ,MAAM,2BAA2B,OAAO,EAAE;AAClD,YAAQ,MAAM,2DAA2D;AACzE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAACA,YAAW,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,eAAeE,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;AAQO,SAAS,WAAW,MAAsB;AAC/C,SAAO,KACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,GAAG;AACtB;AAGO,SAAS,cAAc,KAAqB;AACjD,QAAM,UAAU,WAAW,GAAG;AAC9B,SAAOA,MAAK,cAAc,OAAO;AACnC;AAGO,SAAS,YAAY,KAAqB;AAC/C,SAAOA,MAAK,cAAc,GAAG,GAAG,OAAO;AACzC;AAMO,SAAS,aAAa,KAAiD;AAC5E,QAAM,cAAcC,UAAS,GAAG,EAAE,YAAY;AAC9C,MAAI,gBAAgB,WAAWC,YAAW,GAAG,GAAG;AAC9C,WAAO,EAAE,MAAM,KAAK,SAAS,KAAK;AAAA,EACpC;AAEA,QAAM,aAAa;AAAA,IACjBF,MAAK,KAAK,OAAO;AAAA,IACjBA,MAAK,KAAK,OAAO;AAAA,IACjBA,MAAK,KAAK,WAAW,OAAO;AAAA,EAC9B;AAEA,aAAW,QAAQ,YAAY;AAC7B,QAAIE,YAAW,IAAI,GAAG;AACpB,aAAO,EAAE,MAAM,SAAS,KAAK;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,YAAY,GAAG,GAAG,SAAS,MAAM;AAClD;AAmGO,SAAS,aAAa,KAAqB;AAChD,QAAM,aAAa;AAAA,IACjBC,MAAK,KAAK,SAAS;AAAA,IACnBA,MAAK,KAAK,SAAS,SAAS;AAAA,IAC5BA,MAAK,KAAK,SAAS,SAAS;AAAA,IAC5BA,MAAK,KAAK,WAAW,SAAS;AAAA,EAChC;AAEA,aAAW,QAAQ,YAAY;AAC7B,QAAIC,YAAW,IAAI,EAAG,QAAO;AAAA,EAC/B;AAEA,SAAOD,MAAK,YAAY,GAAG,GAAG,SAAS;AACzC;AAWO,SAAS,qBAAqB,KAAuB;AAC1D,QAAM,aAAuB,CAAC;AAE9B,QAAM,aAAa;AAAA,IACjBE,MAAK,KAAK,WAAW,WAAW;AAAA,IAChCA,MAAK,KAAK,WAAW;AAAA,IACrBA,MAAK,KAAK,SAAS,WAAW;AAAA,IAC9BA,MAAK,KAAK,SAAS,WAAW;AAAA,IAC9BA,MAAK,KAAK,WAAW,WAAW;AAAA,IAChCA,MAAK,KAAK,WAAW,WAAW;AAAA,EAClC;AAEA,aAAW,QAAQ,YAAY;AAC7B,QAAIC,YAAW,IAAI,EAAG,YAAW,KAAK,IAAI;AAAA,EAC5C;AAEA,SAAO;AACT;;;AErNA,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;AACzC,SAAS,QAAAC,aAAY;AACrB,SAAS,WAAAC,gBAAe;AAOjB,SAAS,oBAA6B;AAC3C,MAAI;AACF,UAAM,eAAeD,MAAKC,SAAQ,GAAG,WAAW,eAAe;AAC/D,QAAI,CAACH,YAAW,YAAY,EAAG,QAAO;AAEtC,UAAM,WAAW,KAAK,MAAMC,cAAa,cAAc,OAAO,CAAC;AAC/D,UAAM,UAAoB,SAAS,yBAAyB,CAAC;AAC7D,WAAO,QAAQ,SAAS,UAAU,KAAK,QAAQ,SAAS,QAAQ,KAAK,QAAQ,SAAS,OAAO;AAAA,EAC/F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,eAAsB,qBAAqB,SAAiB,UAAU,GAAqB;AACzF,MAAI,kBAAkB,GAAG;AACvB,YAAQ,MAAM,8DAAyD;AACvE,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,QAAQ,IAAI;AAE1B,MAAI,CAAC,OAAO;AACV,YAAQ,MAAM,0EAAqE;AACnF,WAAO;AAAA,EACT;AAEA,WAAS,UAAU,GAAG,WAAW,SAAS,WAAW;AACnD,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,mBAAmB,KAAK,IAAI;AAAA,QACvD,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,SAAS;AAAA,UACP,SAAS;AAAA,UACT,YAAY;AAAA,QACd;AAAA,MACF,CAAC;AAED,UAAI,SAAS,IAAI;AACf,gBAAQ,MAAM,mDAAmD,OAAO,GAAG;AAC3E,eAAO;AAAA,MACT,OAAO;AACL,gBAAQ,MAAM,mBAAmB,UAAU,CAAC,YAAY,SAAS,MAAM,EAAE;AAAA,MAC3E;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,mBAAmB,UAAU,CAAC,WAAW,KAAK,EAAE;AAAA,IAChE;AAEA,QAAI,UAAU,SAAS;AACrB,YAAM,IAAI,QAAQ,CAAAG,aAAW,WAAWA,UAAS,GAAI,CAAC;AAAA,IACxD;AAAA,EACF;AAEA,UAAQ,MAAM,+CAA+C;AAC7D,SAAO;AACT;;;ACtEA,SAAS,cAAAC,aAAY,aAAAC,YAAW,eAAAC,cAAa,gBAAAC,eAAc,eAAe,cAAAC,mBAAkB;AAC5F,SAAS,QAAAC,OAAM,YAAAC,iBAAgB;AAO/B,SAAS,YAAY,UAA0B;AAC7C,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAO,OAAO,IAAI,YAAY,CAAC;AACrC,QAAM,QAAQ,OAAO,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACxD,QAAM,WAAWD,MAAK,UAAU,MAAM,KAAK;AAC3C,MAAI,CAACL,YAAW,QAAQ,GAAG;AACzB,IAAAC,WAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,EACzC;AACA,SAAO;AACT;AAUO,SAAS,kBAAkB,UAA0B;AAC1D,QAAM,WAAW,YAAY,QAAQ;AAErC,QAAM,QAAQC,aAAY,QAAQ,EAC/B,OAAO,OAAK,EAAE,MAAM,gBAAgB,CAAC,EACrC,KAAK;AAER,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,MAAI,YAAY;AAChB,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,KAAK,MAAM,QAAQ;AACtC,QAAI,YAAY;AACd,YAAM,MAAM,SAAS,WAAW,CAAC,GAAG,EAAE;AACtC,UAAI,MAAM,UAAW,aAAY;AAAA,IACnC;AAAA,EACF;AAEA,SAAO,OAAO,YAAY,CAAC,EAAE,SAAS,GAAG,GAAG;AAC9C;AAMO,SAAS,mBAAmB,UAAiC;AAClE,MAAI,CAACF,YAAW,QAAQ,EAAG,QAAO;AAElC,QAAM,eAAe,CAAC,QAA+B;AACnD,QAAI,CAACA,YAAW,GAAG,EAAG,QAAO;AAC7B,UAAM,QAAQE,aAAY,GAAG,EAC1B,OAAO,OAAK,EAAE,MAAM,uBAAuB,CAAC,EAC5C,KAAK,CAAC,GAAG,MAAM;AACd,YAAM,OAAO,SAAS,EAAE,MAAM,QAAQ,IAAI,CAAC,KAAK,KAAK,EAAE;AACvD,YAAM,OAAO,SAAS,EAAE,MAAM,QAAQ,IAAI,CAAC,KAAK,KAAK,EAAE;AACvD,aAAO,OAAO;AAAA,IAChB,CAAC;AACH,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,WAAOG,MAAK,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC;AAAA,EAC1C;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,OAAO,OAAO,IAAI,YAAY,CAAC;AACrC,QAAM,QAAQ,OAAO,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACxD,QAAM,kBAAkBA,MAAK,UAAU,MAAM,KAAK;AAClD,QAAM,QAAQ,aAAa,eAAe;AAC1C,MAAI,MAAO,QAAO;AAElB,QAAM,WAAW,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC;AAClE,QAAM,WAAW,OAAO,SAAS,YAAY,CAAC;AAC9C,QAAM,YAAY,OAAO,SAAS,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACjE,QAAM,eAAeA,MAAK,UAAU,UAAU,SAAS;AACvD,QAAM,YAAY,aAAa,YAAY;AAC3C,MAAI,UAAW,QAAO;AAEtB,SAAO,aAAa,QAAQ;AAC9B;AAOO,SAAS,kBAAkB,UAAkB,aAA6B;AAC/E,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,QAAM,QAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAClD,QAAM,WAAW,YAAY,QAAQ;AACrC,QAAM,WAAW,GAAG,UAAU,MAAM,IAAI;AACxC,QAAM,WAAWA,MAAK,UAAU,QAAQ;AAExC,QAAM,UAAU,aAAa,UAAU,KAAK,WAAW;AAAA;AAAA,YAE7C,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBd,gBAAc,UAAU,OAAO;AAC/B,UAAQ,MAAM,yBAAyB,QAAQ,EAAE;AAEjD,SAAO;AACT;;;ALxFA,SAAS,gBAAwB;AAC/B,MAAI;AACF,WAAO,SAAS,aAAa,EAAE,UAAU,QAAQ,CAAC,EAAE,KAAK;AAAA,EAC3D,QAAQ;AAEN,UAAM,YAAY;AAAA,MAChB;AAAA,MACA;AAAA,MACA,GAAG,QAAQ,IAAI,IAAI;AAAA,IACrB;AACA,eAAW,KAAK,WAAW;AACzB,UAAIE,YAAW,CAAC,EAAG,QAAO;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,qBAAoC;AAC3C,QAAM,cAAcC,MAAK,SAAS,sBAAsB;AACxD,MAAI,CAACD,YAAW,WAAW,EAAG,QAAO;AAErC,MAAI;AACF,UAAM,UAAU,KAAK,MAAME,cAAa,aAAa,OAAO,CAAC;AAC7D,UAAM,SAAS,SAAS;AACxB,QAAI,QAAQ,YAAY;AACtB,aAAO,OAAO;AAAA,IAChB;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAKA,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACAD,MAAK,SAAS,QAAQ;AACxB;AAMA,SAAS,kBAAkB,KAAsB;AAC/C,aAAW,UAAU,iBAAiB;AACpC,QAAID,YAAWC,MAAK,KAAK,MAAM,CAAC,EAAG,QAAO;AAAA,EAC5C;AACA,SAAO;AACT;AAMA,SAAS,cAAc,KAAsB;AAC3C,QAAM,OAAOE,SAAQ;AACrB,QAAM,WAAWC,SAAQ,GAAG;AAG5B,MAAI,aAAa,KAAM,QAAO;AAI9B,QAAM,QAAQ,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAChD,MAAI,MAAM,SAAS,EAAG,QAAO;AAG7B,QAAM,YAAY,CAAC,QAAQ,QAAQ,gBAAgB,sBAAsB;AACzE,aAAW,UAAU,WAAW;AAC9B,QAAI,aAAa,UAAU,SAAS,WAAW,SAAS,GAAG,EAAG,QAAO;AAAA,EACvE;AAEA,SAAO;AACT;AAQA,eAAe,OAAO;AACpB,UAAQ,MAAM,uCAAuC;AAGrD,MAAI,eAAe,GAAG;AACpB,YAAQ,MAAM,2DAA2D;AACzE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,MAAI,YAA8B;AAClC,MAAI;AACF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,UAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AACpD,QAAI,MAAM,KAAK,GAAG;AAChB,kBAAY,KAAK,MAAM,KAAK;AAAA,IAC9B;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,iDAAiD;AAAA,EACjE;AAGA,QAAM,MAAM,WAAW,OAAO,QAAQ,IAAI;AAI1C,MAAI,cAAcC,UAAS,GAAG;AAC9B,MAAI,YAAY,YAAY,MAAM,SAAS;AACzC,kBAAcA,UAAS,QAAQ,GAAG,CAAC;AAAA,EACrC;AAEA,UAAQ,MAAM,sBAAsB,GAAG,EAAE;AACzC,UAAQ,MAAM,YAAY,WAAW,EAAE;AAGvC,QAAM,aAAa,QAAQ,IAAI,sBAAsB,WACjC,QAAQ,IAAI,sBAAsB,IAAI,SAAS,kBAAkB;AAErF,MAAI,YAAY;AACd,YAAQ,MAAM,mDAAmD;AACjE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAIA,QAAM,gBAAgB,qBAAqB,GAAG;AAC9C,QAAM,mBAAwD,CAAC;AAE/D,MAAI,cAAc,SAAS,GAAG;AAC5B,YAAQ,MAAM,SAAS,cAAc,MAAM,qBAAqB;AAChE,eAAW,QAAQ,eAAe;AAChC,cAAQ,MAAM,QAAQ,IAAI,EAAE;AAC5B,UAAI;AACF,cAAM,UAAUH,cAAa,MAAM,OAAO;AAC1C,yBAAiB,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,gBAAQ,MAAM,aAAa,QAAQ,MAAM,QAAQ;AAAA,MACnD,SAAS,OAAO;AACd,gBAAQ,MAAM,wBAAwB,KAAK,EAAE;AAAA,MAC/C;AAAA,IACF;AAAA,EACF,OAAO;AACL,YAAQ,MAAM,+BAA+B;AAC7C,YAAQ,MAAM,gEAAgE;AAAA,EAChF;AAOA,QAAM,aAAa,mBAAmB;AACtC,MAAI;AAEJ,MAAI,YAAY;AAEd,UAAM,EAAE,WAAAI,WAAU,IAAI,MAAM,OAAO,IAAI;AACvC,QAAI,CAACN,YAAW,UAAU,GAAG;AAC3B,MAAAM,WAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AACzC,cAAQ,MAAM,yBAAyB,UAAU,EAAE;AAAA,IACrD,OAAO;AACL,cAAQ,MAAM,oBAAoB,UAAU,yBAAyB;AAAA,IACvE;AACA,eAAW;AAAA,EACb,OAAO;AACL,UAAM,YAAY,aAAa,GAAG;AAElC,QAAI,UAAU,SAAS;AACrB,iBAAW,UAAU;AACrB,cAAQ,MAAM,oBAAoB,QAAQ,UAAU;AAAA,IACtD,OAAO;AAEL,UAAI,CAACN,YAAW,UAAU,IAAI,GAAG;AAC/B,cAAM,EAAE,WAAAM,WAAU,IAAI,MAAM,OAAO,IAAI;AACvC,QAAAA,WAAU,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AAC7C,gBAAQ,MAAM,0BAA0B,UAAU,IAAI,EAAE;AAAA,MAC1D,OAAO;AACL,gBAAQ,MAAM,oBAAoB,UAAU,IAAI,YAAY;AAAA,MAC9D;AACA,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF;AAIA,QAAM,aAAa,cAAc,GAAG;AACpC,MAAIN,YAAW,UAAU,GAAG;AAC1B,QAAI;AACF,YAAM,QAAQO,aAAY,UAAU;AACpC,YAAM,aAAa,MAChB,OAAO,OAAK,EAAE,SAAS,QAAQ,CAAC,EAChC,IAAI,QAAM;AAAA,QACT,MAAM;AAAA,QACN,MAAMN,MAAK,YAAY,CAAC;AAAA,QACxB,OAAO,SAASA,MAAK,YAAY,CAAC,CAAC,EAAE,MAAM,QAAQ;AAAA,MACrD,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEnC,UAAI,WAAW,SAAS,GAAG;AACzB,cAAM,EAAE,WAAAK,YAAW,YAAAE,YAAW,IAAI,MAAM,OAAO,IAAI;AACnD,cAAM,cAAcP,MAAK,YAAY,UAAU;AAC/C,YAAI,CAACD,YAAW,WAAW,GAAG;AAC5B,UAAAM,WAAU,aAAa,EAAE,WAAW,KAAK,CAAC;AAAA,QAC5C;AAGA,iBAAS,IAAI,GAAG,IAAI,WAAW,QAAQ,KAAK;AAC1C,gBAAM,OAAO,WAAW,CAAC;AACzB,gBAAM,WAAWL,MAAK,aAAa,KAAK,IAAI;AAC5C,cAAI,CAACD,YAAW,QAAQ,GAAG;AACzB,YAAAQ,YAAW,KAAK,MAAM,QAAQ;AAC9B,oBAAQ,MAAM,sBAAsB,KAAK,IAAI,mBAAc;AAAA,UAC7D;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,uCAAuC,KAAK,EAAE;AAAA,IAC9D;AAAA,EACF;AAGA,QAAM,WAAW,aAAa,GAAG;AACjC,QAAM,UAAUR,YAAW,QAAQ;AACnC,MAAI,SAAS;AACX,YAAQ,MAAM,YAAY,QAAQ,EAAE;AAAA,EACtC,OAAO;AAEL,UAAM,cAAcC,MAAK,UAAU,SAAS;AAC5C,UAAM,EAAE,eAAAQ,eAAc,IAAI,MAAM,OAAO,IAAI;AAC3C,IAAAA,eAAc,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAAoD,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,CAAK;AAC5G,YAAQ,MAAM,oBAAoB,WAAW,EAAE;AAAA,EACjD;AAGA,MAAI,iBAAgC;AAEpC,MAAI,UAAU;AACZ,UAAM,kBAAkB,mBAAmB,QAAQ;AAKnD,QAAI,CAAC,iBAAiB;AAEpB,YAAM,kBAAmB,OAAO,gBAAgB,YAAY,YAAY,KAAK,EAAE,SAAS,IACpF,YAAY,KAAK,IACjB;AACJ,cAAQ,MAAM,sDAAsD;AACpE,uBAAiB,kBAAkB,UAAU,OAAO,eAAe,CAAC;AACpE,cAAQ,MAAM,YAAYJ,UAAS,cAAc,CAAC,EAAE;AAAA,IACtD,OAAO;AACL,uBAAiB;AACjB,cAAQ,MAAM;AAAA,+BAAkCA,UAAS,cAAc,CAAC,EAAE;AAE1E,UAAI;AACF,cAAM,UAAUH,cAAa,gBAAgB,OAAO;AACpD,cAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE,MAAM,GAAG,EAAE;AAC7C,gBAAQ,MAAM,8BAA8B;AAC5C,mBAAW,QAAQ,OAAO;AACxB,kBAAQ,MAAM,IAAI;AAAA,QACpB;AACA,gBAAQ,MAAM,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAGA,MAAIF,YAAW,QAAQ,GAAG;AACxB,QAAI;AACF,YAAM,cAAcE,cAAa,UAAU,OAAO;AAClD,YAAM,YAAY,YAAY,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC;AACnF,UAAI,UAAU,SAAS,GAAG;AACxB,gBAAQ,MAAM,eAAe;AAC7B,mBAAW,QAAQ,WAAW;AAC5B,kBAAQ,MAAM,MAAM,KAAK,KAAK,CAAC,EAAE;AAAA,QACnC;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,qBAAqB,sBAAsB,WAAW,EAAE;AAG9D,QAAM,SAAS,cAAc;AAC7B,MAAI,kBAAkB;AACtB,MAAI;AACF,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,eAAe;AACrD,UAAM,MAAM,aAAa,QAAQ,CAAC,WAAW,UAAU,UAAU,GAAG,GAAG;AAAA,MACrE,UAAU;AAAA,MACV,KAAK,QAAQ;AAAA,IACf,CAAC,EAAE,KAAK;AAER,QAAI,KAAK;AACP,YAAM,WAAW,KAAK,MAAM,GAAG;AAkB/B,YAAM,kBAAkB,YAA8B;AACpD,YAAI,cAAc,GAAG,KAAK,CAAC,kBAAkB,GAAG,EAAG,QAAO;AAE1D,YAAI;AACF,uBAAa,QAAQ,CAAC,WAAW,OAAO,GAAG,GAAG;AAAA,YAC5C,UAAU;AAAA,YACV,KAAK,QAAQ;AAAA,UACf,CAAC;AACD,kBAAQ,MAAM,mCAAmC,GAAG,EAAE;AAGtD,cAAI;AACF,kBAAM,OAAO,aAAa,QAAQ,CAAC,WAAW,UAAU,UAAU,GAAG,GAAG;AAAA,cACtE,UAAU;AAAA,cACV,KAAK,QAAQ;AAAA,YACf,CAAC,EAAE,KAAK;AAER,gBAAI,MAAM;AACR,oBAAM,YAAY,KAAK,MAAM,IAAI;AACjC,kBAAI,UAAU,MAAM;AAClB,sBAAM,QAAQ,UAAU,gBAAgB,UAAU;AAClD,wBAAQ,MAAM,yBAAyB,UAAU,IAAI,MAAM,UAAU,UAAU,GAAG;AAClF,kCAAkB,yBAAyB,KAAK,WAAW,UAAU,IAAI;AAAA,SAChF,UAAU,cAAc,OAAO;AACxB,uBAAO;AAAA,cACT;AAAA,YACF;AAAA,UACF,SAAS,WAAW;AAClB,oBAAQ,MAAM,8DAA8D,SAAS;AACrF,mBAAO;AAAA,UACT;AAAA,QACF,SAAS,QAAQ;AACf,kBAAQ,MAAM,+CAA+C,MAAM;AAAA,QACrE;AACA,eAAO;AAAA,MACT;AAEA,UAAI,SAAS,UAAU,YAAY;AAEjC,cAAM,iBAAiB,MAAM,gBAAgB;AAE7C,YAAI,CAAC,gBAAgB;AACnB,4BAAkB;AAAA;AAElB,kBAAQ,MAAM,4BAA4B,GAAG;AAAA,QAC/C;AAAA,MACF,WACE,SAAS,eAAe,YACxB,SAAS,iBACT,CAAC,cAAc,GAAG,KAClB,kBAAkB,GAAG,GACrB;AAGA,gBAAQ;AAAA,UACN,gCAAgC,SAAS,IAAI,wBAAwB,SAAS,aAAa;AAAA,QAC7F;AACA,cAAM,iBAAiB,MAAM,gBAAgB;AAE7C,YAAI,CAAC,gBAAgB;AAEnB,gBAAM,OAAO,SAAS,gBAAgB,SAAS;AAC/C,gBAAM,WAAW,WAAW,SAAS,IAAI;AACzC,gBAAM,YAAY,YAAY,SAAS,iBAAiB,EAAE;AAC1D,gBAAM,aAAa,SAAS,UAAU,SAAS,WAAW,WACtD,KAAK,SAAS,OAAO,YAAY,CAAC,MAClC;AACJ,4BAAkB,yBAAyB,IAAI,GAAG,UAAU,GAAG,QAAQ;AAAA,SACxE,SAAS,gBAAgB,SAAS,iBAAiB,CAAC,GAAG,SAAS,UAAU,SAAS,WAAW,WAAW;AAAA,8BAAiC,SAAS,MAAM,qCAAqC,EAAE;AAC/L,kBAAQ,MAAM,kCAAkC,SAAS,IAAI,kCAAkC;AAAA,QACjG;AAAA,MACF,WAAW,SAAS,MAAM;AACxB,cAAM,OAAO,SAAS,gBAAgB,SAAS;AAC/C,cAAM,WAAW,WAAW,SAAS,IAAI;AACzC,cAAM,YAAY,SAAS,eAAe,UACtC,UACA,YAAY,SAAS,iBAAiB,EAAE;AAC5C,cAAM,aAAa,SAAS,UAAU,SAAS,WAAW,WACtD,KAAK,SAAS,OAAO,YAAY,CAAC,MAClC;AACJ,0BAAkB,yBAAyB,IAAI,GAAG,UAAU,GAAG,QAAQ;AAAA,SACtE,SAAS,gBAAgB,SAAS,iBAAiB,CAAC,GAAG,SAAS,UAAU,SAAS,WAAW,WAAW;AAAA,8BAAiC,SAAS,MAAM,qCAAqC,EAAE;AACjM,gBAAQ,MAAM,wBAAwB,SAAS,IAAI,MAAM,SAAS,UAAU,GAAG;AAAA,MACjF;AAAA,IACF;AAAA,EACF,SAAS,GAAG;AAEV,YAAQ,MAAM,8BAA8B,CAAC;AAAA,EAC/C;AAGA,QAAM,WAAW;AAAA;AAAA;AAAA;AAAA,WAIR,WAAW;AAAA,qBACD,GAAG;AAAA,EACtB,WAAW,oBAAoB,QAAQ,GAAG,aAAa,4BAA4B,EAAE,KAAK,6CAA6C;AAAA,EACvI,UAAU,SAAS,QAAQ,KAAK,iBAAiB;AAAA,EACjD,cAAc,SAAS,IAAI,cAAc,cAAc,KAAK,IAAI,CAAC,KAAK,oBAAoB;AAAA,EAC1F,iBAAiB,gBAAgBG,UAAS,cAAc,CAAC,KAAK,EAAE;AAAA,EAChE,aAAa;AAAA,wEAA2E,EAAE;AAAA,EAC1F,kBAAkB;AAAA,EAAK,eAAe,KAAK,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS7C,UAAQ,IAAI,QAAQ;AAIpB,aAAW,EAAE,MAAM,QAAQ,KAAK,kBAAkB;AAChD,UAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA,UAInB,IAAI;AAAA;AAAA,EAEZ,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAML,YAAQ,IAAI,gBAAgB;AAC5B,YAAQ,MAAM,oCAAoC,IAAI,EAAE;AAAA,EAC1D;AAMA,MAAI;AAEF,QAAI;AACJ,QAAI;AACF,YAAM,EAAE,cAAc,IAAI,IAAI,MAAM,OAAO,eAAe;AAC1D,YAAM,OAAO,IAAI,QAAQ,CAAC,WAAW,UAAU,UAAU,GAAG,GAAG;AAAA,QAC7D,UAAU;AAAA,QACV,KAAK,QAAQ;AAAA,MACf,CAAC,EAAE,KAAK;AACR,UAAI,MAAM;AACR,cAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAI,IAAI,UAAW,kBAAiB,IAAI;AAAA,MAC1C;AAAA,IACF,QAAQ;AAEN,uBAAiB;AAAA,IACnB;AAEA,UAAM,cAAc,mBAAmB,cAAc;AACrD,QAAI,aAAa;AACf,YAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAA0C,WAAW;AAAA;AAAA;AAC5E,cAAQ,IAAI,cAAc;AAC1B,cAAQ,MAAM,kCAAkC;AAAA,IAClD,OAAO;AACL,cAAQ,MAAM,kEAAkE;AAAA,IAClF;AAAA,EACF,SAAS,aAAa;AAEpB,YAAQ,MAAM,qCAAqC,WAAW;AAAA,EAChE;AAEA,UAAQ,MAAM,oCAAoC;AAClD,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK,EAAE,MAAM,WAAS;AACpB,UAAQ,MAAM,kCAAkC,KAAK;AACrD,UAAQ,KAAK,CAAC;AAChB,CAAC;",
6
6
  "names": ["existsSync", "readdirSync", "readFileSync", "join", "basename", "resolve", "homedir", "existsSync", "readdirSync", "join", "basename", "homedir", "join", "existsSync", "readFileSync", "join", "basename", "existsSync", "join", "existsSync", "join", "existsSync", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "join", "readFileSync", "homedir", "resolve", "basename", "mkdirSync", "readdirSync", "renameSync", "writeFileSync"]
@@ -1,6 +1,6 @@
1
1
  {
2
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"],
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
4
  "sourcesContent": ["#!/usr/bin/env node\n/**\n * PostToolUse Hook - Observation Capture\n *\n * Classifies each tool call as a structured observation and sends it to the\n * PAI daemon via IPC. Fire-and-forget with a 5-second timeout. Never blocks\n * Claude Code (always exits 0).\n */\n\nimport { connect } from 'net';\nimport { sha256 } from '../../../utils/hash.js';\nimport { basename } from 'path';\nimport { isProbeSession } from '../lib/project-utils.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface HookData {\n session_id: string;\n tool_name: string;\n tool_input: Record<string, unknown>;\n tool_response?: unknown;\n cwd?: string;\n}\n\ntype ObservationType = 'change' | 'discovery' | 'decision' | 'feature';\n\ninterface Observation {\n type: ObservationType;\n title: string;\n narrative: string;\n tool_name: string;\n tool_input_summary: string;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Tools to skip entirely\n// ---------------------------------------------------------------------------\n\nconst SKIP_TOOLS = new Set([\n 'ToolSearch',\n 'AskUserQuestion',\n 'EnterPlanMode',\n 'ExitPlanMode',\n 'Skill',\n]);\n\n// ---------------------------------------------------------------------------\n// Inline IPC sender \u2014 avoids importing src/daemon/ipc-client.ts at build time\n// ---------------------------------------------------------------------------\n\nfunction sendToDaemon(method: string, params: Record<string, unknown>): Promise<void> {\n return new Promise((resolve) => {\n const timeout = setTimeout(() => { resolve(); }, 5000);\n try {\n const socket = connect('/tmp/pai.sock', () => {\n const req = JSON.stringify({ id: Date.now().toString(), method, params }) + '\\n';\n socket.write(req);\n socket.on('data', () => { clearTimeout(timeout); socket.destroy(); resolve(); });\n socket.on('error', () => { clearTimeout(timeout); resolve(); });\n });\n socket.on('error', () => { clearTimeout(timeout); resolve(); });\n } catch {\n clearTimeout(timeout);\n resolve();\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Concept extraction from file paths\n// ---------------------------------------------------------------------------\n\nconst SKIP_SEGMENTS = new Set([\n 'src', 'dist', 'lib', 'bin', 'test', 'tests', 'spec', 'specs',\n 'node_modules', 'Users', 'home', 'usr', 'var', 'tmp', 'etc',\n 'hooks', 'scripts', 'config', 'configs', 'assets', 'static',\n 'public', 'private', 'build', 'out', 'output', 'generated',\n 'ts', 'js', 'mjs', 'cjs',\n]);\n\nfunction extractConcepts(paths: string[]): string[] {\n const concepts = new Set<string>();\n for (const p of paths) {\n const segments = p.split('/').filter(Boolean);\n for (const seg of segments) {\n // Drop extensions\n const clean = seg.replace(/\\.[^.]+$/, '');\n if (clean.length > 2 && !SKIP_SEGMENTS.has(clean) && !/^\\d+$/.test(clean)) {\n concepts.add(clean);\n }\n }\n }\n return Array.from(concepts).slice(0, 10);\n}\n\n// ---------------------------------------------------------------------------\n// Inline classifier\n// ---------------------------------------------------------------------------\n\nfunction str(v: unknown): string {\n if (typeof v === 'string') return v;\n return '';\n}\n\nfunction truncate(s: string, len: number): string {\n return s.length > len ? s.slice(0, len) : s;\n}\n\nfunction classify(toolName: string, toolInput: Record<string, unknown>): Observation | null {\n if (SKIP_TOOLS.has(toolName)) return null;\n\n let type: ObservationType = 'discovery';\n let title = '';\n let narrative = '';\n let tool_input_summary = '';\n const files_read: string[] = [];\n const files_modified: string[] = [];\n\n switch (toolName) {\n case 'Edit':\n case 'MultiEdit': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'change';\n title = `Modified ${name}`;\n narrative = fp ? `Edited ${fp}` : 'Edited a file';\n tool_input_summary = fp;\n files_modified.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Write':\n case 'NotebookEdit': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'change';\n title = `Created ${name}`;\n narrative = fp ? `Wrote ${fp}` : 'Wrote a file';\n tool_input_summary = fp;\n files_modified.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Read': {\n const fp = str(toolInput.file_path);\n const name = fp ? basename(fp) : 'file';\n type = 'discovery';\n title = `Read ${name}`;\n narrative = fp ? `Read ${fp}` : 'Read a file';\n tool_input_summary = fp;\n files_read.push(...(fp ? [fp] : []));\n break;\n }\n\n case 'Grep': {\n const pattern = str(toolInput.pattern);\n type = 'discovery';\n title = `Searched for '${truncate(pattern, 40)}'`;\n narrative = `Grep search: ${pattern}`;\n tool_input_summary = pattern;\n const gPath = str(toolInput.path || toolInput.file_path);\n if (gPath) files_read.push(gPath);\n break;\n }\n\n case 'Glob': {\n const pattern = str(toolInput.pattern);\n type = 'discovery';\n title = `Found files: ${truncate(pattern, 40)}`;\n narrative = `Glob pattern: ${pattern}`;\n tool_input_summary = pattern;\n break;\n }\n\n case 'Bash': {\n const cmd = str(toolInput.command);\n const cmdLower = cmd.toLowerCase();\n\n if (/git\\s+commit/.test(cmdLower)) {\n type = 'decision';\n // Try to extract the commit message after -m \"...\"\n const mMatch = cmd.match(/-m\\s+[\"']([^\"']+)/);\n const msg = mMatch ? mMatch[1] : cmd;\n title = `Committed: ${truncate(msg, 60)}`;\n narrative = `Git commit: ${msg}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/git\\s+push/.test(cmdLower)) {\n type = 'decision';\n title = 'Pushed to remote';\n narrative = `Git push: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/\\b(jest|vitest|pytest|bun\\s+test|npm\\s+test|yarn\\s+test|pnpm\\s+test|node\\s+--test)\\b/.test(cmdLower)) {\n type = 'feature';\n title = 'Ran tests';\n narrative = `Test run: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else if (/\\b(build|compile|bun\\s+run\\s+build|tsc|esbuild|webpack|vite\\s+build)\\b/.test(cmdLower)) {\n type = 'feature';\n title = 'Built project';\n narrative = `Build: ${truncate(cmd, 80)}`;\n tool_input_summary = truncate(cmd, 120);\n } else {\n type = 'discovery';\n title = `Ran: ${truncate(cmd, 60)}`;\n narrative = `Bash: ${truncate(cmd, 120)}`;\n tool_input_summary = truncate(cmd, 120);\n }\n break;\n }\n\n case 'Task': {\n const prompt = str(toolInput.prompt || toolInput.description);\n type = 'discovery';\n title = `Delegated: ${truncate(prompt, 60)}`;\n narrative = `Spawned agent: ${truncate(prompt, 200)}`;\n tool_input_summary = truncate(prompt, 200);\n break;\n }\n\n case 'WebFetch': {\n const url = str(toolInput.url);\n type = 'discovery';\n title = `Fetched: ${truncate(url, 60)}`;\n narrative = `Web fetch: ${url}`;\n tool_input_summary = url;\n break;\n }\n\n case 'WebSearch': {\n const query = str(toolInput.query);\n type = 'discovery';\n title = `Searched web: ${truncate(query, 60)}`;\n narrative = `Web search: ${query}`;\n tool_input_summary = query;\n break;\n }\n\n default: {\n // mcp__* tools and anything else\n if (toolName.startsWith('mcp__')) {\n type = 'discovery';\n title = `MCP: ${toolName}`;\n narrative = `Called MCP tool ${toolName}`;\n tool_input_summary = toolName;\n } else {\n // Generic fallback \u2014 still capture rather than skip\n type = 'discovery';\n title = `Tool: ${toolName}`;\n narrative = `Called ${toolName}`;\n tool_input_summary = JSON.stringify(toolInput).slice(0, 120);\n }\n break;\n }\n }\n\n const allPaths = [...files_read, ...files_modified];\n const concepts = extractConcepts(allPaths);\n\n return {\n type,\n title,\n narrative,\n tool_name: toolName,\n tool_input_summary,\n files_read,\n files_modified,\n concepts,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Skill telemetry capture (self-educating skill system, Phase 1)\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the invoked skill name from a tool call, or null if this call is not\n * a skill invocation. Two signals:\n * - the `Skill` tool (tool_input.skill)\n * - Bash `skill-workflow-notification WORKFLOW SKILL` (2nd arg is the skill)\n */\nfunction extractSkillInvocation(\n toolName: string,\n toolInput: Record<string, unknown>\n): string | null {\n if (toolName === 'Skill') {\n return str(toolInput.skill) || null;\n }\n if (toolName === 'Bash') {\n // Matches both the hyphenated doc form and the real camelCase tool path:\n // ~/.claude/Tools/SkillWorkflowNotification GIT CORE\n // skill-workflow-notification GIT CORE\n // Capture order is WORKFLOW then SKILL \u2014 we want the skill (2nd arg).\n const cmd = str(toolInput.command);\n const m = cmd.match(/skill[-]?workflow[-]?notification\\s+(\\S+)\\s+(\\S+)/i);\n if (m) return m[2];\n }\n return null;\n}\n\nasync function maybeRecordSkill(hookData: HookData): Promise<void> {\n const skill = extractSkillInvocation(hookData.tool_name, hookData.tool_input);\n if (!skill) return;\n await sendToDaemon('skill_telemetry_record', {\n skill_name: skill,\n source: 'local',\n cwd: hookData.cwd ?? '',\n });\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n try {\n // Read stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n const raw = Buffer.concat(chunks).toString('utf-8').trim();\n if (!raw) process.exit(0);\n\n const hookData: HookData = JSON.parse(raw);\n\n // Skip probe/health-check sessions\n if (isProbeSession(hookData.cwd)) process.exit(0);\n\n // Record skill telemetry BEFORE the SKIP gate (the Skill tool is skipped\n // for observations but is exactly the signal we want for telemetry).\n await maybeRecordSkill(hookData);\n\n // Skip uninteresting tools\n if (SKIP_TOOLS.has(hookData.tool_name)) process.exit(0);\n\n // Classify\n const obs = classify(hookData.tool_name, hookData.tool_input);\n if (!obs) process.exit(0);\n\n // Content-hash dedup key\n const hash = sha256(hookData.session_id + hookData.tool_name + obs.title).slice(0, 16);\n\n // Fire-and-forget to daemon\n await sendToDaemon('observation_store', {\n session_id: hookData.session_id,\n type: obs.type,\n title: obs.title,\n narrative: obs.narrative,\n tool_name: obs.tool_name,\n tool_input_summary: obs.tool_input_summary,\n files_read: obs.files_read,\n files_modified: obs.files_modified,\n concepts: obs.concepts,\n content_hash: hash,\n cwd: hookData.cwd ?? '',\n });\n\n process.exit(0);\n } catch {\n // Never block Claude Code\n process.exit(0);\n }\n}\n\nmain();\n", "/**\n * Shared hashing utilities. Centralises all SHA-256 usage so every module\n * obtains digests through the same function rather than inlining createHash.\n */\n\nimport { createHash } from \"node:crypto\";\n\n/**\n * Compute a SHA-256 hex digest of the given string.\n * Aliased as sha256File for compatibility with existing call-sites that use\n * that name to hash file contents.\n */\nexport function sha256(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/** Alias kept for backwards compatibility with memory/indexer call-sites. */\nexport const sha256File = sha256;\n", "/**\n * Path utilities \u2014 encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory.\n * Returns the number of files moved.\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n try {\n renameSync(sourcePath, destPath);\n if (!silent) console.error(`Moved ${file} \u2192 sessions/`);\n movedCount++;\n } catch (error) {\n if (!silent) console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n\n return movedCount;\n}\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["basename", "join", "join", "resolve", "basename"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/ts/session-start/post-compact-inject.ts"],
3
+ "sources": ["../../../src/hooks/ts/session-start/post-compact-inject.ts"],
4
4
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * post-compact-inject.ts \u2014 SessionStart hook (matcher: \"compact\")\n *\n * Fires AFTER auto/manual compaction. Reads the session state that\n * the PreCompact hook saved to a temp file and outputs it to stdout,\n * which Claude Code injects into the post-compaction context.\n *\n * This is the ONLY way to influence what Claude knows after compaction:\n * PreCompact hooks have no stdout support, but SessionStart does.\n *\n * Flow:\n * PreCompact \u2192 context-compression-hook.ts saves state to /tmp/pai-compact-state-{sessionId}.txt\n * Compaction runs (conversation is summarized)\n * SessionStart(compact) \u2192 THIS HOOK reads that file \u2192 stdout \u2192 context\n */\n\nimport { readFileSync, existsSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { tmpdir } from 'os';\n\ninterface HookInput {\n session_id: string;\n transcript_path?: string;\n cwd?: string;\n hook_event_name: string;\n source?: string;\n}\n\nasync function main() {\n let hookInput: HookInput | null = null;\n\n try {\n const decoder = new TextDecoder();\n let input = '';\n const timeoutPromise = new Promise<void>((resolve) => { setTimeout(resolve, 500); });\n const readPromise = (async () => {\n for await (const chunk of process.stdin) {\n input += decoder.decode(chunk, { stream: true });\n }\n })();\n await Promise.race([readPromise, timeoutPromise]);\n if (input.trim()) {\n hookInput = JSON.parse(input) as HookInput;\n }\n } catch {\n // Silently handle input errors\n }\n\n if (!hookInput?.session_id) {\n console.error('post-compact-inject: no session_id, exiting');\n process.exit(0);\n }\n\n // Look for the state file saved by context-compression-hook during PreCompact\n const stateFile = join(tmpdir(), `pai-compact-state-${hookInput.session_id}.txt`);\n\n if (!existsSync(stateFile)) {\n console.error(`post-compact-inject: no state file found at ${stateFile}`);\n process.exit(0);\n }\n\n try {\n const state = readFileSync(stateFile, 'utf-8').trim();\n\n if (state.length > 0) {\n // Output to stdout \u2014 Claude Code injects this into the post-compaction context\n console.log(state);\n console.error(`post-compact-inject: injected ${state.length} chars of session state`);\n }\n\n // Clean up the temp file\n unlinkSync(stateFile);\n console.error(`post-compact-inject: cleaned up ${stateFile}`);\n } catch (err) {\n console.error(`post-compact-inject: error reading state file: ${err}`);\n }\n\n process.exit(0);\n}\n\nmain().catch(() => {\n process.exit(0);\n});\n"],
5
5
  "mappings": ";;;AAkBA,SAAS,cAAc,YAAY,kBAAkB;AACrD,SAAS,YAAY;AACrB,SAAS,cAAc;AAUvB,eAAe,OAAO;AACpB,MAAI,YAA8B;AAElC,MAAI;AACF,UAAM,UAAU,IAAI,YAAY;AAChC,QAAI,QAAQ;AACZ,UAAM,iBAAiB,IAAI,QAAc,CAAC,YAAY;AAAE,iBAAW,SAAS,GAAG;AAAA,IAAG,CAAC;AACnF,UAAM,eAAe,YAAY;AAC/B,uBAAiB,SAAS,QAAQ,OAAO;AACvC,iBAAS,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAAA,MACjD;AAAA,IACF,GAAG;AACH,UAAM,QAAQ,KAAK,CAAC,aAAa,cAAc,CAAC;AAChD,QAAI,MAAM,KAAK,GAAG;AAChB,kBAAY,KAAK,MAAM,KAAK;AAAA,IAC9B;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI,CAAC,WAAW,YAAY;AAC1B,YAAQ,MAAM,6CAA6C;AAC3D,YAAQ,KAAK,CAAC;AAAA,EAChB;AAGA,QAAM,YAAY,KAAK,OAAO,GAAG,qBAAqB,UAAU,UAAU,MAAM;AAEhF,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAQ,MAAM,+CAA+C,SAAS,EAAE;AACxE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI;AACF,UAAM,QAAQ,aAAa,WAAW,OAAO,EAAE,KAAK;AAEpD,QAAI,MAAM,SAAS,GAAG;AAEpB,cAAQ,IAAI,KAAK;AACjB,cAAQ,MAAM,iCAAiC,MAAM,MAAM,yBAAyB;AAAA,IACtF;AAGA,eAAW,SAAS;AACpB,YAAQ,MAAM,mCAAmC,SAAS,EAAE;AAAA,EAC9D,SAAS,KAAK;AACZ,YAAQ,MAAM,kDAAkD,GAAG,EAAE;AAAA,EACvE;AAEA,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK,EAAE,MAAM,MAAM;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;",
6
6
  "names": []
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/ts/pre-tool-use/security-validator.ts"],
3
+ "sources": ["../../../src/hooks/ts/pre-tool-use/security-validator.ts"],
4
4
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * security-validator.ts - PreToolUse Security Validation Hook\n *\n * Fast pattern-based security validation for Bash commands.\n * Blocks commands matching known attack patterns before execution.\n *\n * Design Principles:\n * - Fast path: Most commands allowed with minimal processing\n * - Pre-compiled regex patterns at module load\n * - Only log/block on high-confidence attack detection\n * - Fail open on errors (don't break legitimate work)\n *\n * CUSTOMIZATION REQUIRED:\n * This template includes basic examples. Add your own security patterns\n * based on your threat model and environment.\n */\n\nimport { appendFileSync, mkdirSync, existsSync } from 'fs';\nimport { dirname } from 'path';\n\n// ============================================================================\n// ATTACK PATTERNS - CUSTOMIZE THESE FOR YOUR ENVIRONMENT\n// ============================================================================\n\n// Example: Reverse Shell Patterns (BLOCK - rarely legitimate)\nconst REVERSE_SHELL_PATTERNS: RegExp[] = [\n /\\/dev\\/(tcp|udp)\\/[0-9]/, // Bash TCP/UDP device\n /bash\\s+-i\\s+>&?\\s*\\/dev\\//, // Interactive bash redirect\n // Add your own reverse shell patterns here\n];\n\n// Example: Instruction Override (BLOCK - prompt injection)\nconst INSTRUCTION_OVERRIDE_PATTERNS: RegExp[] = [\n /ignore\\s+(all\\s+)?previous\\s+instructions?/i,\n /disregard\\s+(all\\s+)?(prior|previous)\\s+(instructions?|rules?)/i,\n // Add your own prompt injection patterns here\n];\n\n// Example: Catastrophic Deletion Patterns (BLOCK - filesystem destruction)\nconst CATASTROPHIC_DELETION_PATTERNS: RegExp[] = [\n // Trailing tilde bypass\n /\\s+~\\/?(\\s*$|\\s+)/, // Space then ~/ at end\n /\\brm\\s+(-[rfivd]+\\s+)*\\S+\\s+~\\/?/, // rm something ~/\n\n // Relative path recursive deletion\n /\\brm\\s+(-[rfivd]+\\s+)*\\.\\/\\s*$/, // rm -rf ./\n /\\brm\\s+(-[rfivd]+\\s+)*\\.\\.\\/\\s*$/, // rm -rf ../\n\n // Add your own dangerous deletion patterns here\n];\n\n// Example: Dangerous File Operations (BLOCK - data destruction)\nconst DANGEROUS_FILE_OPS_PATTERNS: RegExp[] = [\n /\\bchmod\\s+(-R\\s+)?0{3,}/, // chmod 000\n // Add your own dangerous file operation patterns here\n];\n\n// OPTIONAL: Operations that require confirmation instead of blocking\nconst DANGEROUS_GIT_PATTERNS: RegExp[] = [\n /\\bgit\\s+push\\s+.*(-f\\b|--force)/i, // git push --force\n /\\bgit\\s+reset\\s+--hard/i, // git reset --hard\n // Add your own git safety patterns here\n];\n\n// Combined patterns for fast iteration\nconst ALL_BLOCK_PATTERNS: { category: string; patterns: RegExp[] }[] = [\n { category: 'reverse_shell', patterns: REVERSE_SHELL_PATTERNS },\n { category: 'instruction_override', patterns: INSTRUCTION_OVERRIDE_PATTERNS },\n { category: 'catastrophic_deletion', patterns: CATASTROPHIC_DELETION_PATTERNS },\n { category: 'dangerous_file_ops', patterns: DANGEROUS_FILE_OPS_PATTERNS },\n];\n\nconst CONFIRM_PATTERNS: { category: string; patterns: RegExp[] }[] = [\n { category: 'dangerous_git', patterns: DANGEROUS_GIT_PATTERNS },\n];\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface HookInput {\n session_id: string;\n tool_name: string;\n tool_input: Record<string, unknown> | string;\n}\n\ninterface HookOutput {\n permissionDecision: 'allow' | 'deny';\n additionalContext?: string;\n feedback?: string;\n}\n\n// ============================================================================\n// DETECTION LOGIC\n// ============================================================================\n\ninterface DetectionResult {\n blocked: boolean;\n requiresConfirmation?: boolean;\n category?: string;\n pattern?: string;\n}\n\nfunction detectAttack(content: string): DetectionResult {\n // First check for hard blocks\n for (const { category, patterns } of ALL_BLOCK_PATTERNS) {\n for (const pattern of patterns) {\n if (pattern.test(content)) {\n return { blocked: true, category, pattern: pattern.source };\n }\n }\n }\n\n // Then check for confirmation-required patterns\n for (const { category, patterns } of CONFIRM_PATTERNS) {\n for (const pattern of patterns) {\n if (pattern.test(content)) {\n return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };\n }\n }\n }\n\n return { blocked: false };\n}\n\n// ============================================================================\n// ASYNC LOGGING (fire-and-forget on block only)\n// ============================================================================\n\nfunction logSecurityEvent(event: Record<string, unknown>): void {\n const logPath = `${process.env.PAI_DIR || '~/.claude'}/history/security/security-events.jsonl`;\n const entry = JSON.stringify({ timestamp: new Date().toISOString(), ...event }) + '\\n';\n\n try {\n const dir = dirname(logPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n appendFileSync(logPath, entry);\n } catch {\n // Silently fail - logging should never break the hook\n }\n}\n\n// ============================================================================\n// MAIN HOOK LOGIC\n// ============================================================================\n\nasync function main(): Promise<void> {\n let input: HookInput;\n\n try {\n // Read stdin\n const chunks: Buffer[] = [];\n const timeoutPromise = new Promise<Buffer[]>((_, reject) =>\n setTimeout(() => reject(new Error('timeout')), 100)\n );\n const readPromise = (async () => {\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n return chunks;\n })();\n\n let text = '';\n try {\n const result = await Promise.race([readPromise, timeoutPromise]);\n text = Buffer.concat(result).toString('utf-8');\n } catch {\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n return;\n }\n\n if (!text.trim()) {\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n return;\n }\n\n input = JSON.parse(text);\n } catch {\n // Parse error or timeout - fail open\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n return;\n }\n\n // Only validate Bash commands\n if (input.tool_name !== 'Bash') {\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n return;\n }\n\n // Extract command string\n const command = typeof input.tool_input === 'string'\n ? input.tool_input\n : (input.tool_input?.command as string) || '';\n\n if (!command) {\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n return;\n }\n\n // Check all patterns\n const result = detectAttack(command);\n\n if (result.blocked) {\n // Log and block\n logSecurityEvent({\n type: 'attack_blocked',\n category: result.category,\n pattern: result.pattern,\n command: command.slice(0, 200), // Truncate for log\n session_id: input.session_id,\n });\n\n const output: HookOutput = {\n permissionDecision: 'deny',\n additionalContext: `SECURITY: Blocked ${result.category} pattern`,\n feedback: `This command matched a security pattern (${result.category}). If this is legitimate, please rephrase the command.`,\n };\n\n console.log(JSON.stringify(output));\n process.exit(2); // Exit 2 = blocking error\n }\n\n if (result.requiresConfirmation) {\n // Log warning and require confirmation\n logSecurityEvent({\n type: 'confirmation_required',\n category: result.category,\n pattern: result.pattern,\n command: command.slice(0, 200),\n session_id: input.session_id,\n });\n\n const output: HookOutput = {\n permissionDecision: 'deny',\n additionalContext: `DANGEROUS: ${result.category} operation requires confirmation`,\n feedback: `This is a dangerous operation (${command.slice(0, 50)}...). This can cause data loss. If you're sure, explicitly confirm this command.`,\n };\n\n console.log(JSON.stringify(output));\n process.exit(2); // Exit 2 = requires user confirmation\n }\n\n // Allow - no logging, immediate exit\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n}\n\n// ============================================================================\n// RUN\n// ============================================================================\n\nmain().catch(() => {\n // On any error, fail open\n console.log(JSON.stringify({ permissionDecision: 'allow' }));\n});\n"],
5
5
  "mappings": "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",
6
6
  "names": ["result"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/hooks/ts/session-start/session-commands.ts"],
3
+ "sources": ["../../../src/hooks/ts/session-start/session-commands.ts"],
4
4
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * session-commands.ts\n *\n * Tiny SessionStart hook that injects critical session commands inline.\n * Stays under 2KB to avoid persisted-output truncation.\n *\n * This runs BEFORE load-core-context so these commands are always available\n * even when the full CORE skill gets truncated.\n */\n\nconst commands = `<system-reminder>\nSESSION COMMANDS (always available):\n\n- \"go\" / \"continue\" / \"weiter\" \u2192 Read Notes/TODO.md, find ## Continue section, resume work\n- \"pause session\" \u2192 Save state to TODO.md, update session note, stop (no compact)\n- \"end session\" \u2192 Finalize note, commit if needed, start fresh next time\n- \"cpp\" \u2192 Release workflow: version bump \u2192 build \u2192 npm publish \u2192 git add ALL \u2192 ONE commit \u2192 git push\n\nWhen user says \"go\" with no other context, ALWAYS check TODO.md first.\n\nIMPORTANT: Full PAI context may be truncated due to size. For complete instructions (response format, workflows, principles), READ: ~/.claude/Skills/CORE/SKILL.md\n</system-reminder>`;\n\nconsole.log(commands);\n"],
5
5
  "mappings": ";;;AAYA,IAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAajB,QAAQ,IAAI,QAAQ;",
6
6
  "names": []