@tekmidian/pai 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +87 -1
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1482 -1628
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +11 -8
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-2ND5WO2j.mjs → daemon-D3hYb5_C.mjs} +669 -218
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/db-DdUperSl.mjs +110 -0
  18. package/dist/db-DdUperSl.mjs.map +1 -0
  19. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  20. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  21. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  22. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  23. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  24. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  25. package/dist/helpers-BEST-4Gx.mjs +420 -0
  26. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  27. package/dist/hooks/capture-all-events.mjs +2 -2
  28. package/dist/hooks/capture-all-events.mjs.map +3 -3
  29. package/dist/hooks/capture-session-summary.mjs +38 -0
  30. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  31. package/dist/hooks/cleanup-session-files.mjs +6 -12
  32. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  33. package/dist/hooks/context-compression-hook.mjs +93 -104
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/initialize-session.mjs +14 -11
  36. package/dist/hooks/initialize-session.mjs.map +4 -4
  37. package/dist/hooks/inject-observations.mjs +220 -0
  38. package/dist/hooks/inject-observations.mjs.map +7 -0
  39. package/dist/hooks/load-core-context.mjs +2 -2
  40. package/dist/hooks/load-core-context.mjs.map +3 -3
  41. package/dist/hooks/load-project-context.mjs +90 -91
  42. package/dist/hooks/load-project-context.mjs.map +4 -4
  43. package/dist/hooks/observe.mjs +354 -0
  44. package/dist/hooks/observe.mjs.map +7 -0
  45. package/dist/hooks/stop-hook.mjs +94 -107
  46. package/dist/hooks/stop-hook.mjs.map +4 -4
  47. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  48. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  49. package/dist/index.d.mts +30 -7
  50. package/dist/index.d.mts.map +1 -1
  51. package/dist/index.mjs +5 -8
  52. package/dist/indexer-D53l5d1U.mjs +1 -0
  53. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  54. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  55. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  56. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  57. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  58. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  59. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  60. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  61. package/dist/note-context-BK24bX8Y.mjs +126 -0
  62. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  63. package/dist/postgres-CKf-EDtS.mjs +846 -0
  64. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  65. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  66. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  67. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  68. package/dist/search-DC1qhkKn.mjs.map +1 -0
  69. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  70. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  71. package/dist/state-C6_vqz7w.mjs +102 -0
  72. package/dist/state-C6_vqz7w.mjs.map +1 -0
  73. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  74. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  75. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  76. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  77. package/dist/themes-BvYF0W8T.mjs +148 -0
  78. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  79. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  80. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  81. package/dist/trace-CRx9lPuc.mjs +137 -0
  82. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  83. package/dist/{vault-indexer-k-kUlaZ-.mjs → vault-indexer-Bi2cRmn7.mjs} +134 -132
  84. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  85. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  86. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  87. package/package.json +1 -2
  88. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  89. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  90. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  91. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  92. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  93. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  94. package/src/hooks/ts/lib/project-utils.ts +40 -1018
  95. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  96. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  97. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  98. package/dist/chunker-CbnBe0s0.mjs +0 -191
  99. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  100. package/dist/config-Cf92lGX_.mjs.map +0 -1
  101. package/dist/daemon-2ND5WO2j.mjs.map +0 -1
  102. package/dist/db-Dp8VXIMR.mjs +0 -212
  103. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  104. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  105. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  106. package/dist/mcp/index.d.mts +0 -1
  107. package/dist/mcp/index.mjs +0 -500
  108. package/dist/mcp/index.mjs.map +0 -1
  109. package/dist/postgres-FXrHDPcE.mjs +0 -358
  110. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  111. package/dist/schemas-BFIgGntb.mjs +0 -3405
  112. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  113. package/dist/search-_oHfguA5.mjs.map +0 -1
  114. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  115. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  116. package/dist/vault-indexer-k-kUlaZ-.mjs.map +0 -1
  117. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  118. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  119. package/templates/README.md +0 -181
  120. package/templates/skills/CORE/Aesthetic.md +0 -333
  121. package/templates/skills/CORE/CONSTITUTION.md +0 -1502
  122. package/templates/skills/CORE/HistorySystem.md +0 -427
  123. package/templates/skills/CORE/HookSystem.md +0 -1082
  124. package/templates/skills/CORE/Prompting.md +0 -509
  125. package/templates/skills/CORE/ProsodyAgentTemplate.md +0 -53
  126. package/templates/skills/CORE/ProsodyGuide.md +0 -416
  127. package/templates/skills/CORE/SKILL.md +0 -741
  128. package/templates/skills/CORE/SkillSystem.md +0 -213
  129. package/templates/skills/CORE/TerminalTabs.md +0 -119
  130. package/templates/skills/CORE/VOICE.md +0 -106
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. package/templates/templates.md +0 -20
@@ -1,7 +1,7 @@
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.ts"],
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 * project-utils.ts - Shared utilities for project context management\n *\n * Provides:\n * - Path encoding (matching Claude Code's scheme)\n * - ntfy.sh notifications (mandatory, synchronous)\n * - Session notes management\n * - Session token calculation\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\n\n// Import from pai-paths which handles .env loading and path resolution\nimport { PAI_DIR } from './pai-paths.js';\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// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with - (hidden directories become --name)\n *\n * This matches Claude Code's internal encoding to ensure Notes\n * are stored in the same project directory as transcripts.\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-') // Slashes become dashes\n .replace(/\\./g, '-') // Dots also become dashes\n .replace(/ /g, '-'); // Spaces become dashes (matches Claude Code native encoding)\n}\n\n/**\n * Get the project directory for a given working directory\n */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/**\n * Get the Notes directory for a project (central location)\n */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory - check local first, fallback to central\n * DOES NOT create the directory - just finds the right location\n *\n * Logic:\n * - If cwd itself IS a Notes directory \u2192 use it directly\n * - If local Notes/ exists \u2192 use it (can be checked into git)\n * - Otherwise \u2192 use central ~/.claude/projects/.../Notes/\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n // FIRST: Check if cwd itself IS a Notes directory\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n // Check local locations\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 // Fallback to central location\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/**\n * Get the Sessions directory for a project (stores .jsonl transcripts)\n */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/**\n * Get the Sessions directory from a project directory path\n */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n/**\n * Check if a messaging MCP server (AIBroker, Whazaa, or Telex) is configured.\n *\n * Uses standard Claude Code config at ~/.claude/settings.json.\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. Sending both\n * would cause duplicate notifications.\n *\n * When WhatsApp is NOT configured, ntfy fires as the fallback channel.\n */\nexport async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {\n // Skip ntfy when WhatsApp is configured \u2014 the AI handles notifications via MCP\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 // Wait before retry\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/**\n * Ensure the Notes directory exists for a project\n * DEPRECATED: Use ensureNotesDirSmart() instead\n */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\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 *\n * This respects the user's choice:\n * - Projects with local Notes/ keep notes there (git-trackable)\n * - Other directories don't get cluttered with auto-created Notes/\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n\n if (found.isLocal) {\n // Local Notes/ exists - use it as-is\n return found;\n }\n\n // No local Notes/ - ensure central exists\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n\n return found;\n}\n\n/**\n * Ensure the Sessions directory exists for a project\n */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n\n return sessionsDir;\n}\n\n/**\n * Ensure the Sessions directory exists (from project dir path)\n */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory\n * @param projectDir - The project directory path\n * @param excludeFile - Optional filename to exclude (e.g., current active session)\n * @param silent - If true, suppress console output\n * Returns the number of files moved\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent: boolean = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) {\n return 0;\n }\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n // Match session files: uuid.jsonl or agent-*.jsonl\n // Skip the excluded file (typically the current active session)\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n try {\n renameSync(sourcePath, destPath);\n if (!silent) {\n console.error(`Moved ${file} \u2192 sessions/`);\n }\n movedCount++;\n } catch (error) {\n if (!silent) {\n console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n }\n\n return movedCount;\n}\n\n/**\n * Get the YYYY/MM subdirectory for the current month inside notesDir.\n * Creates the directory if it doesn't exist.\n */\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 * Get the next note number (4-digit format: 0001, 0002, etc.)\n * ALWAYS uses 4-digit format with space-dash-space separators\n * Format: NNNN - YYYY-MM-DD - Description.md\n * Numbers reset per month (each YYYY/MM directory has its own sequence).\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n // Match CORRECT format: \"0001 - \" (4-digit with space-dash-space)\n // Also match legacy formats for backwards compatibility when detecting max number\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/)) // Starts with 3-4 digits followed by separator\n .filter(f => f.endsWith('.md'))\n .sort();\n\n if (files.length === 0) {\n return '0001'; // Default to 4-digit\n }\n\n // Find the highest number across all formats\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 // ALWAYS return 4-digit format\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 in the current month's YYYY/MM subdirectory first,\n * then falls back to previous month (for sessions spanning month boundaries),\n * then falls back to flat notesDir for legacy notes.\n * Supports multiple formats for backwards compatibility:\n * - CORRECT: \"0001 - YYYY-MM-DD - Description.md\" (space-dash-space)\n * - Legacy: \"001_YYYY-MM-DD_description.md\" (underscores)\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) {\n return null;\n }\n\n // Helper: find latest session note in a directory\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 // 1. Check current month's YYYY/MM directory\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 // 2. Check previous month (for sessions spanning month boundaries)\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 // 3. Fallback: check flat notesDir (legacy notes not yet filed)\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note\n * CORRECT FORMAT: \"NNNN - YYYY-MM-DD - Description.md\"\n * - 4-digit zero-padded number\n * - Space-dash-space separators (NOT underscores)\n * - Title case description\n *\n * IMPORTANT: The initial description is just a PLACEHOLDER.\n * Claude MUST rename the file at session end with a meaningful description\n * based on the actual work done. Never leave it as \"New Session\" or project name.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD\n\n // Use \"New Session\" as placeholder - Claude MUST rename at session end!\n // The project name alone is NOT descriptive enough.\n const safeDescription = 'New Session';\n\n // CORRECT FORMAT: space-dash-space separators, filed into YYYY/MM subdirectory\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - ${safeDescription}.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/**\n * Append checkpoint to current session note\n */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n // Note vanished (cloud sync, cleanup, etc.) \u2014 recreate it\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\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 // Insert before \"## Next Steps\" if it exists, otherwise append\n const nextStepsIndex = content.indexOf('## Next Steps');\n let newContent: string;\n\n if (nextStepsIndex !== -1) {\n newContent = content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex);\n } else {\n newContent = content + checkpointText;\n }\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/**\n * Work item for session notes\n */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/**\n * Add work items to the \"Work Done\" section of a session note\n * This is the main way to capture what was accomplished in a session\n */\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 // Build the work section content\n let workText = '';\n if (sectionTitle) {\n workText += `\\n### ${sectionTitle}\\n\\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 // Find the Work Done section and insert after the comment/placeholder\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 // Fallback: insert before Next Steps\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 * Update the session note title to be more descriptive\n * Called when we know what work was done\n */\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\n // Update the H1 title\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n\n writeFileSync(notePath, content);\n\n // Also rename the file\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Sanitize a string for use in a filename (exported for use elsewhere)\n */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '') // Remove special chars\n .replace(/\\s+/g, '-') // Spaces to hyphens\n .replace(/-+/g, '-') // Collapse multiple hyphens\n .replace(/^-|-$/g, '') // Trim hyphens\n .substring(0, 50); // Limit length\n}\n\n/**\n * Extract a meaningful name from session note content\n * Looks at Work Done section and summary to generate a descriptive name\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n // Try to extract from Work Done section headers (### headings)\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 // Look for ### subheadings which typically describe what was done\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n // Use the first subheading, clean it up\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n // Look for bold text which often indicates key topics\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n // Look for numbered list items (1. Something)\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n // Fall back to summary if provided\n if (summary && summary.length > 5 && summary !== 'Session completed.') {\n // Take first meaningful phrase from summary\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n\n if (cleanSummary.length > 3) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename session note with a meaningful name\n * ALWAYS uses correct format: \"NNNN - YYYY-MM-DD - Description.md\"\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)) {\n return notePath;\n }\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n // Parse existing filename - support multiple formats:\n // CORRECT: \"0001 - 2026-01-02 - Description.md\"\n // Legacy: \"001_2026-01-02_description.md\"\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\n const match = correctMatch || legacyMatch;\n if (!match) {\n return notePath; // Can't parse, don't rename\n }\n\n const [, noteNumber, date] = match;\n\n // Convert to Title Case\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n // ALWAYS use correct format with 4-digit number\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n // Don't rename if name is the same\n if (newFilename === oldFilename) {\n return notePath;\n }\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/**\n * Finalize session note (mark as complete, add summary, rename with meaningful name)\n * IDEMPOTENT: Will only finalize once, subsequent calls are no-ops\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 // IDEMPOTENT CHECK: If already completed, don't modify again\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n // Update status\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n // Add completion timestamp (only if not already present)\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 // Add summary to Next Steps section (only if placeholder exists)\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 // Extract meaningful name and rename the file\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n const newPath = renameSessionNote(notePath, meaningfulName);\n return newPath;\n }\n\n return notePath;\n}\n\n/**\n * Calculate total tokens from a session .jsonl file\n */\nexport function calculateSessionTokens(jsonlPath: string): number {\n if (!existsSync(jsonlPath)) {\n return 0;\n }\n\n try {\n const content = readFileSync(jsonlPath, 'utf-8');\n const lines = content.trim().split('\\n');\n let totalTokens = 0;\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.message?.usage) {\n const usage = entry.message.usage;\n totalTokens += (usage.input_tokens || 0);\n totalTokens += (usage.output_tokens || 0);\n totalTokens += (usage.cache_creation_input_tokens || 0);\n totalTokens += (usage.cache_read_input_tokens || 0);\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return totalTokens;\n } catch (error) {\n console.error(`Error calculating tokens: ${error}`);\n return 0;\n }\n}\n\n/**\n * Find TODO.md - check local first, fallback to central\n */\nexport function findTodoPath(cwd: string): string {\n // Check local locations first\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)) {\n return path;\n }\n }\n\n // Fallback to central location (inside Notes/)\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/**\n * Find CLAUDE.md - check local locations\n * Returns the FIRST found path (for backwards compatibility)\n */\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\n * Returns paths in priority order (most specific first):\n * 1. .claude/CLAUDE.md (project-specific config dir)\n * 2. CLAUDE.md (project root)\n * 3. Notes/CLAUDE.md (notes directory)\n * 4. Prompts/CLAUDE.md (prompts directory)\n *\n * All found files will be loaded and injected into context.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n // Priority order: most specific first\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)) {\n foundPaths.push(path);\n }\n }\n\n return foundPaths;\n}\n\n/**\n * Ensure TODO.md exists\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n // Ensure parent directory exists\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n/**\n * Task item for TODO.md\n */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n/**\n * Update TODO.md with current session tasks\n * Preserves the Backlog section\n * Ensures only ONE timestamp line at the end\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n // Find Backlog section to preserve it (but strip any trailing timestamps/separators)\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n let backlogSection = backlogMatch ? backlogMatch[0].trim() : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n // Format tasks\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n // Build new content with exactly ONE timestamp at the end\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks)\n * Ensures only ONE timestamp line at the end\n * Works regardless of TODO.md structure \u2014 appends if no known section found\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n // Try to insert before Backlog section\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n // No Backlog section \u2014 try before Continue section, or just append\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n // Insert after the Continue section (find the next ## or ---)\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4; // after \\n---\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n // No known section \u2014 just append before the end\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n // Add exactly ONE timestamp at the end\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n * This mirrors \"pause session\" behavior \u2014 gives the next session a starting point.\n * Replaces any existing ## Continue section.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string\n): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove existing ## Continue section (from ## Continue to the first standalone --- line)\n content = content.replace(/## Continue\\n[\\s\\S]*?\\n---\\n+/, '');\n\n const now = new Date().toISOString();\n const stateLines = state\n ? state.split('\\n').filter(l => l.trim()).slice(0, 10).map(l => `> ${l}`).join('\\n')\n : `> Working directory: ${cwd}. Check the latest session note for details.`;\n\n const continueSection = `## Continue\n\n> **Last session:** ${noteFilename.replace('.md', '')}\n> **Paused at:** ${now}\n>\n${stateLines}\n\n---\n\n`;\n\n // Remove leading whitespace from content\n content = content.replace(/^\\s+/, '');\n\n // If content starts with # title, insert after it\n const titleMatch = content.match(/^(# [^\\n]+\\n+)/);\n if (titleMatch) {\n content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);\n } else {\n content = continueSection + content;\n }\n\n // Clean up trailing timestamps and add fresh one\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error('TODO.md ## Continue section updated');\n}\n"],
5
- "mappings": "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",
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
+ "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
+ "mappings": "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",
6
6
  "names": ["readFileSync", "existsSync", "join", "join", "join", "join", "existsSync", "readFileSync"]
7
7
  }
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/hooks/ts/session-start/load-project-context.ts
4
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "fs";
5
- import { join as join3, basename as basename2, dirname } from "path";
4
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync } from "fs";
5
+ import { join as join5, basename as basename3, dirname } from "path";
6
6
  import { execSync } from "child_process";
7
7
 
8
- // src/hooks/ts/lib/project-utils.ts
9
- import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
8
+ // src/hooks/ts/lib/project-utils/paths.ts
9
+ import { existsSync as existsSync2, mkdirSync, readdirSync, renameSync } from "fs";
10
10
  import { join as join2, basename } from "path";
11
- import { homedir as homedir2 } from "os";
12
11
 
13
12
  // src/hooks/ts/lib/pai-paths.ts
14
13
  import { homedir } from "os";
@@ -68,7 +67,8 @@ function validatePAIStructure() {
68
67
  }
69
68
  validatePAIStructure();
70
69
 
71
- // src/hooks/ts/lib/project-utils.ts
70
+ // src/hooks/ts/lib/project-utils/paths.ts
71
+ var PROJECTS_DIR = join2(PAI_DIR, "projects");
72
72
  var PROBE_CWD_PATTERNS = [
73
73
  "/CodexBar/ClaudeProbe",
74
74
  "/ClaudeProbe"
@@ -77,7 +77,6 @@ function isProbeSession(cwd) {
77
77
  const dir = cwd || process.cwd();
78
78
  return PROBE_CWD_PATTERNS.some((pattern) => dir.includes(pattern));
79
79
  }
80
- var PROJECTS_DIR = join2(PAI_DIR, "projects");
81
80
  function encodePath(path) {
82
81
  return path.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
83
82
  }
@@ -105,10 +104,42 @@ function findNotesDir(cwd) {
105
104
  }
106
105
  return { path: getNotesDir(cwd), isLocal: false };
107
106
  }
107
+ function findTodoPath(cwd) {
108
+ const localPaths = [
109
+ join2(cwd, "TODO.md"),
110
+ join2(cwd, "notes", "TODO.md"),
111
+ join2(cwd, "Notes", "TODO.md"),
112
+ join2(cwd, ".claude", "TODO.md")
113
+ ];
114
+ for (const path of localPaths) {
115
+ if (existsSync2(path)) return path;
116
+ }
117
+ return join2(getNotesDir(cwd), "TODO.md");
118
+ }
119
+ function findAllClaudeMdPaths(cwd) {
120
+ const foundPaths = [];
121
+ const localPaths = [
122
+ join2(cwd, ".claude", "CLAUDE.md"),
123
+ join2(cwd, "CLAUDE.md"),
124
+ join2(cwd, "Notes", "CLAUDE.md"),
125
+ join2(cwd, "notes", "CLAUDE.md"),
126
+ join2(cwd, "Prompts", "CLAUDE.md"),
127
+ join2(cwd, "prompts", "CLAUDE.md")
128
+ ];
129
+ for (const path of localPaths) {
130
+ if (existsSync2(path)) foundPaths.push(path);
131
+ }
132
+ return foundPaths;
133
+ }
134
+
135
+ // src/hooks/ts/lib/project-utils/notify.ts
136
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
137
+ import { join as join3 } from "path";
138
+ import { homedir as homedir2 } from "os";
108
139
  function isWhatsAppEnabled() {
109
140
  try {
110
- const settingsPath = join2(homedir2(), ".claude", "settings.json");
111
- if (!existsSync2(settingsPath)) return false;
141
+ const settingsPath = join3(homedir2(), ".claude", "settings.json");
142
+ if (!existsSync3(settingsPath)) return false;
112
143
  const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
113
144
  const enabled = settings.enabledMcpjsonServers || [];
114
145
  return enabled.includes("aibroker") || enabled.includes("whazaa") || enabled.includes("telex");
@@ -152,22 +183,24 @@ async function sendNtfyNotification(message, retries = 2) {
152
183
  console.error("ntfy.sh notification failed after all retries");
153
184
  return false;
154
185
  }
186
+
187
+ // src/hooks/ts/lib/project-utils/session-notes.ts
188
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync, renameSync as renameSync2 } from "fs";
189
+ import { join as join4, basename as basename2 } from "path";
155
190
  function getMonthDir(notesDir) {
156
191
  const now = /* @__PURE__ */ new Date();
157
192
  const year = String(now.getFullYear());
158
193
  const month = String(now.getMonth() + 1).padStart(2, "0");
159
- const monthDir = join2(notesDir, year, month);
160
- if (!existsSync2(monthDir)) {
161
- mkdirSync(monthDir, { recursive: true });
194
+ const monthDir = join4(notesDir, year, month);
195
+ if (!existsSync4(monthDir)) {
196
+ mkdirSync2(monthDir, { recursive: true });
162
197
  }
163
198
  return monthDir;
164
199
  }
165
200
  function getNextNoteNumber(notesDir) {
166
201
  const monthDir = getMonthDir(notesDir);
167
- const files = readdirSync(monthDir).filter((f) => f.match(/^\d{3,4}[\s_-]/)).filter((f) => f.endsWith(".md")).sort();
168
- if (files.length === 0) {
169
- return "0001";
170
- }
202
+ const files = readdirSync2(monthDir).filter((f) => f.match(/^\d{3,4}[\s_-]/)).filter((f) => f.endsWith(".md")).sort();
203
+ if (files.length === 0) return "0001";
171
204
  let maxNumber = 0;
172
205
  for (const file of files) {
173
206
  const digitMatch = file.match(/^(\d+)/);
@@ -179,29 +212,27 @@ function getNextNoteNumber(notesDir) {
179
212
  return String(maxNumber + 1).padStart(4, "0");
180
213
  }
181
214
  function getCurrentNotePath(notesDir) {
182
- if (!existsSync2(notesDir)) {
183
- return null;
184
- }
215
+ if (!existsSync4(notesDir)) return null;
185
216
  const findLatestIn = (dir) => {
186
- if (!existsSync2(dir)) return null;
187
- const files = readdirSync(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
217
+ if (!existsSync4(dir)) return null;
218
+ const files = readdirSync2(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
188
219
  const numA = parseInt(a.match(/^(\d+)/)?.[1] || "0", 10);
189
220
  const numB = parseInt(b.match(/^(\d+)/)?.[1] || "0", 10);
190
221
  return numA - numB;
191
222
  });
192
223
  if (files.length === 0) return null;
193
- return join2(dir, files[files.length - 1]);
224
+ return join4(dir, files[files.length - 1]);
194
225
  };
195
226
  const now = /* @__PURE__ */ new Date();
196
227
  const year = String(now.getFullYear());
197
228
  const month = String(now.getMonth() + 1).padStart(2, "0");
198
- const currentMonthDir = join2(notesDir, year, month);
229
+ const currentMonthDir = join4(notesDir, year, month);
199
230
  const found = findLatestIn(currentMonthDir);
200
231
  if (found) return found;
201
232
  const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
202
233
  const prevYear = String(prevDate.getFullYear());
203
234
  const prevMonth = String(prevDate.getMonth() + 1).padStart(2, "0");
204
- const prevMonthDir = join2(notesDir, prevYear, prevMonth);
235
+ const prevMonthDir = join4(notesDir, prevYear, prevMonth);
205
236
  const prevFound = findLatestIn(prevMonthDir);
206
237
  if (prevFound) return prevFound;
207
238
  return findLatestIn(notesDir);
@@ -209,10 +240,9 @@ function getCurrentNotePath(notesDir) {
209
240
  function createSessionNote(notesDir, description) {
210
241
  const noteNumber = getNextNoteNumber(notesDir);
211
242
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
212
- const safeDescription = "New Session";
213
243
  const monthDir = getMonthDir(notesDir);
214
- const filename = `${noteNumber} - ${date} - ${safeDescription}.md`;
215
- const filepath = join2(monthDir, filename);
244
+ const filename = `${noteNumber} - ${date} - New Session.md`;
245
+ const filepath = join4(monthDir, filename);
216
246
  const content = `# Session ${noteNumber}: ${description}
217
247
 
218
248
  **Date:** ${date}
@@ -238,37 +268,6 @@ function createSessionNote(notesDir, description) {
238
268
  console.error(`Created session note: ${filename}`);
239
269
  return filepath;
240
270
  }
241
- function findTodoPath(cwd) {
242
- const localPaths = [
243
- join2(cwd, "TODO.md"),
244
- join2(cwd, "notes", "TODO.md"),
245
- join2(cwd, "Notes", "TODO.md"),
246
- join2(cwd, ".claude", "TODO.md")
247
- ];
248
- for (const path of localPaths) {
249
- if (existsSync2(path)) {
250
- return path;
251
- }
252
- }
253
- return join2(getNotesDir(cwd), "TODO.md");
254
- }
255
- function findAllClaudeMdPaths(cwd) {
256
- const foundPaths = [];
257
- const localPaths = [
258
- join2(cwd, ".claude", "CLAUDE.md"),
259
- join2(cwd, "CLAUDE.md"),
260
- join2(cwd, "Notes", "CLAUDE.md"),
261
- join2(cwd, "notes", "CLAUDE.md"),
262
- join2(cwd, "Prompts", "CLAUDE.md"),
263
- join2(cwd, "prompts", "CLAUDE.md")
264
- ];
265
- for (const path of localPaths) {
266
- if (existsSync2(path)) {
267
- foundPaths.push(path);
268
- }
269
- }
270
- return foundPaths;
271
- }
272
271
 
273
272
  // src/hooks/ts/session-start/load-project-context.ts
274
273
  function findPaiBinary() {
@@ -281,16 +280,16 @@ function findPaiBinary() {
281
280
  `${process.env.HOME}/.local/bin/pai`
282
281
  ];
283
282
  for (const p of fallbacks) {
284
- if (existsSync3(p)) return p;
283
+ if (existsSync5(p)) return p;
285
284
  }
286
285
  }
287
286
  return "pai";
288
287
  }
289
288
  function getRoutedNotesPath() {
290
- const routingFile = join3(PAI_DIR, "session-routing.json");
291
- if (!existsSync3(routingFile)) return null;
289
+ const routingFile = join5(PAI_DIR, "session-routing.json");
290
+ if (!existsSync5(routingFile)) return null;
292
291
  try {
293
- const routing = JSON.parse(readFileSync3(routingFile, "utf-8"));
292
+ const routing = JSON.parse(readFileSync4(routingFile, "utf-8"));
294
293
  const active = routing?.active_session;
295
294
  if (active?.notes_path) {
296
295
  return active.notes_path;
@@ -319,9 +318,9 @@ async function main() {
319
318
  console.error("Could not parse hook input, using process.cwd()");
320
319
  }
321
320
  const cwd = hookInput?.cwd || process.cwd();
322
- let projectName = basename2(cwd);
321
+ let projectName = basename3(cwd);
323
322
  if (projectName.toLowerCase() === "notes") {
324
- projectName = basename2(dirname(cwd));
323
+ projectName = basename3(dirname(cwd));
325
324
  }
326
325
  console.error(`Working directory: ${cwd}`);
327
326
  console.error(`Project: ${projectName}`);
@@ -337,7 +336,7 @@ async function main() {
337
336
  for (const path of claudeMdPaths) {
338
337
  console.error(` - ${path}`);
339
338
  try {
340
- const content = readFileSync3(path, "utf-8");
339
+ const content = readFileSync4(path, "utf-8");
341
340
  claudeMdContents.push({ path, content });
342
341
  console.error(` Read ${content.length} chars`);
343
342
  } catch (error) {
@@ -351,9 +350,9 @@ async function main() {
351
350
  const routedPath = getRoutedNotesPath();
352
351
  let notesDir;
353
352
  if (routedPath) {
354
- const { mkdirSync: mkdirSync2 } = await import("fs");
355
- if (!existsSync3(routedPath)) {
356
- mkdirSync2(routedPath, { recursive: true });
353
+ const { mkdirSync: mkdirSync3 } = await import("fs");
354
+ if (!existsSync5(routedPath)) {
355
+ mkdirSync3(routedPath, { recursive: true });
357
356
  console.error(`Created routed Notes: ${routedPath}`);
358
357
  } else {
359
358
  console.error(`Notes directory: ${routedPath} (routed via pai route)`);
@@ -365,9 +364,9 @@ async function main() {
365
364
  notesDir = notesInfo.path;
366
365
  console.error(`Notes directory: ${notesDir} (local)`);
367
366
  } else {
368
- if (!existsSync3(notesInfo.path)) {
369
- const { mkdirSync: mkdirSync2 } = await import("fs");
370
- mkdirSync2(notesInfo.path, { recursive: true });
367
+ if (!existsSync5(notesInfo.path)) {
368
+ const { mkdirSync: mkdirSync3 } = await import("fs");
369
+ mkdirSync3(notesInfo.path, { recursive: true });
371
370
  console.error(`Created central Notes: ${notesInfo.path}`);
372
371
  } else {
373
372
  console.error(`Notes directory: ${notesInfo.path} (central)`);
@@ -376,25 +375,25 @@ async function main() {
376
375
  }
377
376
  }
378
377
  const projectDir = getProjectDir(cwd);
379
- if (existsSync3(projectDir)) {
378
+ if (existsSync5(projectDir)) {
380
379
  try {
381
- const files = readdirSync2(projectDir);
380
+ const files = readdirSync3(projectDir);
382
381
  const jsonlFiles = files.filter((f) => f.endsWith(".jsonl")).map((f) => ({
383
382
  name: f,
384
- path: join3(projectDir, f),
385
- mtime: statSync(join3(projectDir, f)).mtime.getTime()
383
+ path: join5(projectDir, f),
384
+ mtime: statSync(join5(projectDir, f)).mtime.getTime()
386
385
  })).sort((a, b) => b.mtime - a.mtime);
387
386
  if (jsonlFiles.length > 1) {
388
- const { mkdirSync: mkdirSync2, renameSync: renameSync2 } = await import("fs");
389
- const sessionsDir = join3(projectDir, "sessions");
390
- if (!existsSync3(sessionsDir)) {
391
- mkdirSync2(sessionsDir, { recursive: true });
387
+ const { mkdirSync: mkdirSync3, renameSync: renameSync3 } = await import("fs");
388
+ const sessionsDir = join5(projectDir, "sessions");
389
+ if (!existsSync5(sessionsDir)) {
390
+ mkdirSync3(sessionsDir, { recursive: true });
392
391
  }
393
392
  for (let i = 1; i < jsonlFiles.length; i++) {
394
393
  const file = jsonlFiles[i];
395
- const destPath = join3(sessionsDir, file.name);
396
- if (!existsSync3(destPath)) {
397
- renameSync2(file.path, destPath);
394
+ const destPath = join5(sessionsDir, file.name);
395
+ if (!existsSync5(destPath)) {
396
+ renameSync3(file.path, destPath);
398
397
  console.error(`Moved old session: ${file.name} \u2192 sessions/`);
399
398
  }
400
399
  }
@@ -404,11 +403,11 @@ async function main() {
404
403
  }
405
404
  }
406
405
  const todoPath = findTodoPath(cwd);
407
- const hasTodo = existsSync3(todoPath);
406
+ const hasTodo = existsSync5(todoPath);
408
407
  if (hasTodo) {
409
408
  console.error(`TODO.md: ${todoPath}`);
410
409
  } else {
411
- const newTodoPath = join3(notesDir, "TODO.md");
410
+ const newTodoPath = join5(notesDir, "TODO.md");
412
411
  const { writeFileSync: writeFileSync2 } = await import("fs");
413
412
  writeFileSync2(newTodoPath, `# TODO
414
413
 
@@ -431,7 +430,7 @@ async function main() {
431
430
  console.error("\nNo previous session notes found - creating new one");
432
431
  } else {
433
432
  try {
434
- const content = readFileSync3(currentNotePath, "utf-8");
433
+ const content = readFileSync4(currentNotePath, "utf-8");
435
434
  if (content.includes("**Status:** Completed") || content.includes("**Completed:**")) {
436
435
  needsNewNote = true;
437
436
  console.error(`
@@ -442,7 +441,7 @@ Previous note completed - creating new one`);
442
441
  }
443
442
  } else {
444
443
  console.error(`
445
- Continuing session note: ${basename2(currentNotePath)}`);
444
+ Continuing session note: ${basename3(currentNotePath)}`);
446
445
  }
447
446
  } catch {
448
447
  needsNewNote = true;
@@ -450,11 +449,11 @@ Continuing session note: ${basename2(currentNotePath)}`);
450
449
  }
451
450
  if (needsNewNote) {
452
451
  activeNotePath = createSessionNote(notesDir, projectName);
453
- console.error(`Created: ${basename2(activeNotePath)}`);
452
+ console.error(`Created: ${basename3(activeNotePath)}`);
454
453
  } else {
455
454
  activeNotePath = currentNotePath;
456
455
  try {
457
- const content = readFileSync3(activeNotePath, "utf-8");
456
+ const content = readFileSync4(activeNotePath, "utf-8");
458
457
  const lines = content.split("\n").slice(0, 12);
459
458
  console.error("--- Current Note Preview ---");
460
459
  for (const line of lines) {
@@ -465,9 +464,9 @@ Continuing session note: ${basename2(currentNotePath)}`);
465
464
  }
466
465
  }
467
466
  }
468
- if (existsSync3(todoPath)) {
467
+ if (existsSync5(todoPath)) {
469
468
  try {
470
- const todoContent = readFileSync3(todoPath, "utf-8");
469
+ const todoContent = readFileSync4(todoPath, "utf-8");
471
470
  const todoLines = todoContent.split("\n").filter((l) => l.includes("[ ]")).slice(0, 5);
472
471
  if (todoLines.length > 0) {
473
472
  console.error("\nOpen TODOs:");
@@ -516,7 +515,7 @@ Working Directory: ${cwd}
516
515
  ${notesDir ? `Notes Directory: ${notesDir}${routedPath ? " (routed via pai route)" : ""}` : "Notes: disabled (no local Notes/ directory)"}
517
516
  ${hasTodo ? `TODO: ${todoPath}` : "TODO: not found"}
518
517
  ${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(", ")}` : "No CLAUDE.md found"}
519
- ${activeNotePath ? `Active Note: ${basename2(activeNotePath)}` : ""}
518
+ ${activeNotePath ? `Active Note: ${basename3(activeNotePath)}` : ""}
520
519
  ${routedPath ? `
521
520
  Note Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ""}
522
521
  ${paiProjectBlock ? `