@tekmidian/pai 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +1 -1
- package/dist/daemon/index.mjs +1 -1
- package/dist/{daemon-D3hYb5_C.mjs → daemon-DJoesjez.mjs} +847 -4
- package/dist/daemon-DJoesjez.mjs.map +1 -0
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +4 -23
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/stop-hook.mjs +206 -125
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/lib/project-utils/session-notes.ts +24 -5
- package/src/hooks/ts/session-start/load-project-context.ts +9 -25
- package/src/hooks/ts/stop/stop-hook.ts +259 -199
- package/dist/daemon-D3hYb5_C.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/ts/stop/stop-hook.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", "../../src/hooks/ts/lib/project-utils/todo.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\nimport { readFileSync } from 'fs';\nimport { join, basename, dirname } from 'path';\nimport {\n sendNtfyNotification,\n getCurrentNotePath,\n finalizeSessionNote,\n moveSessionFilesToSessionsDir,\n addWorkToSessionNote,\n findNotesDir,\n isProbeSession,\n updateTodoContinue,\n WorkItem\n} from '../lib/project-utils';\n\n/**\n * Extract work items from transcript for session note\n * Looks for SUMMARY, ACTIONS, RESULTS sections in assistant responses\n */\nfunction extractWorkFromTranscript(lines: string[]): WorkItem[] {\n const workItems: WorkItem[] = [];\n const seenSummaries = new Set<string>();\n\n // Process all assistant messages to find work summaries\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = contentToText(entry.message.content);\n\n // Look for SUMMARY: lines (our standard format)\n const summaryMatch = content.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const summary = summaryMatch[1].trim();\n if (summary && !seenSummaries.has(summary) && summary.length > 5) {\n seenSummaries.add(summary);\n\n // Try to extract details from ACTIONS or RESULTS\n const details: string[] = [];\n\n const actionsMatch = content.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n // Extract bullet points or numbered items\n const actionLines = actionsMatch[1].split('\\n')\n .map(l => l.replace(/^[-*\u2022]\\s*/, '').replace(/^\\d+\\.\\s*/, '').trim())\n .filter(l => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3)); // Max 3 action items\n }\n\n workItems.push({\n title: summary,\n details: details.length > 0 ? details : undefined,\n completed: true\n });\n }\n }\n\n // Also look for COMPLETED: lines as backup\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch && workItems.length === 0) {\n const completed = completedMatch[1].trim().replace(/\\*+/g, '');\n if (completed && !seenSummaries.has(completed) && completed.length > 5) {\n seenSummaries.add(completed);\n workItems.push({\n title: completed,\n completed: true\n });\n }\n }\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return workItems;\n}\n\n/**\n * Generate 4-word tab title summarizing what was done\n */\nfunction generateTabTitle(prompt: string, completedLine?: string): string {\n // If we have a completed line, try to use it for a better summary\n if (completedLine) {\n const cleanCompleted = completedLine\n .replace(/\\*+/g, '')\n .replace(/\\[.*?\\]/g, '')\n .replace(/COMPLETED:\\s*/gi, '')\n .trim();\n\n // Extract meaningful words from the completed line\n const completedWords = cleanCompleted.split(/\\s+/)\n .filter(word => word.length > 2 &&\n !['the', 'and', 'but', 'for', 'are', 'with', 'his', 'her', 'this', 'that', 'you', 'can', 'will', 'have', 'been', 'your', 'from', 'they', 'were', 'said', 'what', 'them', 'just', 'told', 'how', 'does', 'into', 'about', 'completed'].includes(word.toLowerCase()))\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());\n\n if (completedWords.length >= 2) {\n // Build a 4-word summary from completed line\n const summary = completedWords.slice(0, 4);\n while (summary.length < 4) {\n summary.push('Done');\n }\n return summary.slice(0, 4).join(' ');\n }\n }\n\n // Fall back to parsing the prompt\n const cleanPrompt = prompt.replace(/[^\\w\\s]/g, ' ').trim();\n const words = cleanPrompt.split(/\\s+/).filter(word =>\n word.length > 2 &&\n !['the', 'and', 'but', 'for', 'are', 'with', 'his', 'her', 'this', 'that', 'you', 'can', 'will', 'have', 'been', 'your', 'from', 'they', 'were', 'said', 'what', 'them', 'just', 'told', 'how', 'does', 'into', 'about'].includes(word.toLowerCase())\n );\n\n const lowerPrompt = prompt.toLowerCase();\n\n // Find action verb if present\n const actionVerbs = ['test', 'rename', 'fix', 'debug', 'research', 'write', 'create', 'make', 'build', 'implement', 'analyze', 'review', 'update', 'modify', 'generate', 'develop', 'design', 'deploy', 'configure', 'setup', 'install', 'remove', 'delete', 'add', 'check', 'verify', 'validate', 'optimize', 'refactor', 'enhance', 'improve', 'send', 'email', 'help', 'updated', 'fixed', 'created', 'built', 'added'];\n\n let titleWords: string[] = [];\n\n // Check for action verb\n for (const verb of actionVerbs) {\n if (lowerPrompt.includes(verb)) {\n // Convert to past tense for summary\n let pastTense = verb;\n if (verb === 'write') pastTense = 'Wrote';\n else if (verb === 'make') pastTense = 'Made';\n else if (verb === 'send') pastTense = 'Sent';\n else if (verb.endsWith('e')) pastTense = verb.charAt(0).toUpperCase() + verb.slice(1, -1) + 'ed';\n else pastTense = verb.charAt(0).toUpperCase() + verb.slice(1) + 'ed';\n\n titleWords.push(pastTense);\n break;\n }\n }\n\n // Add most meaningful remaining words\n const remainingWords = words\n .filter(word => !actionVerbs.includes(word.toLowerCase()))\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());\n\n // Fill up to 4 words total\n for (const word of remainingWords) {\n if (titleWords.length < 4) {\n titleWords.push(word);\n } else {\n break;\n }\n }\n\n // If we don't have enough words, add generic ones\n if (titleWords.length === 0) {\n titleWords.push('Completed');\n }\n if (titleWords.length === 1) {\n titleWords.push('Task');\n }\n if (titleWords.length === 2) {\n titleWords.push('Successfully');\n }\n if (titleWords.length === 3) {\n titleWords.push('Done');\n }\n\n return titleWords.slice(0, 4).join(' ');\n}\n\n/**\n * Set terminal tab title (works with Kitty, Ghostty, iTerm2, etc.)\n */\nfunction setTerminalTabTitle(title: string): void {\n const term = process.env.TERM || '';\n\n if (term.includes('ghostty')) {\n process.stderr.write(`\\x1b]2;${title}\\x07`);\n process.stderr.write(`\\x1b]0;${title}\\x07`);\n process.stderr.write(`\\x1b]7;${title}\\x07`);\n process.stderr.write(`\\x1b]2;${title}\\x1b\\\\`);\n } else if (term.includes('kitty')) {\n process.stderr.write(`\\x1b]0;${title}\\x07`);\n process.stderr.write(`\\x1b]2;${title}\\x07`);\n process.stderr.write(`\\x1b]30;${title}\\x07`);\n } else {\n process.stderr.write(`\\x1b]0;${title}\\x07`);\n process.stderr.write(`\\x1b]2;${title}\\x07`);\n }\n\n if (process.stderr.isTTY) {\n process.stderr.write('');\n }\n}\n\n// Helper to safely turn Claude content (string or array of blocks) into plain text\nfunction contentToText(content: any): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === 'string') return c;\n if (c?.text) return c.text;\n if (c?.content) return String(c.content);\n return '';\n })\n .join(' ')\n .trim();\n }\n return '';\n}\n\nasync function main() {\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n process.exit(0);\n }\n\n const timestamp = new Date().toISOString();\n console.error(`\\nSTOP-HOOK TRIGGERED AT ${timestamp}`);\n\n // Get input\n let input = '';\n const decoder = new TextDecoder();\n\n try {\n for await (const chunk of process.stdin) {\n input += decoder.decode(chunk, { stream: true });\n }\n } catch (e) {\n console.error(`Error reading input: ${e}`);\n process.exit(0);\n }\n\n if (!input) {\n console.error('No input received');\n process.exit(0);\n }\n\n let transcriptPath: string;\n let cwd: string;\n try {\n const parsed = JSON.parse(input);\n transcriptPath = parsed.transcript_path;\n cwd = parsed.cwd || process.cwd();\n console.error(`Transcript path: ${transcriptPath}`);\n console.error(`Working directory: ${cwd}`);\n } catch (e) {\n console.error(`Error parsing input JSON: ${e}`);\n process.exit(0);\n }\n\n if (!transcriptPath) {\n console.error('No transcript_path in input');\n process.exit(0);\n }\n\n // Read the transcript\n let transcript;\n try {\n transcript = readFileSync(transcriptPath, 'utf-8');\n console.error(`Transcript loaded: ${transcript.split('\\n').length} lines`);\n } catch (e) {\n console.error(`Error reading transcript: ${e}`);\n process.exit(0);\n }\n\n // Parse the JSON lines to find what happened in this session\n const lines = transcript.trim().split('\\n');\n\n // Get the last user query for context\n let lastUserQuery = '';\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]);\n if (entry.type === 'user' && entry.message?.content) {\n const content = entry.message.content;\n if (typeof content === 'string') {\n lastUserQuery = content;\n } else if (Array.isArray(content)) {\n for (const item of content) {\n if (item.type === 'text' && item.text) {\n lastUserQuery = item.text;\n break;\n }\n }\n }\n if (lastUserQuery) break;\n }\n } catch (e) {\n // Skip invalid JSON\n }\n }\n\n // Extract the completion message from the last assistant response\n let message = '';\n\n const lastResponse = lines[lines.length - 1];\n try {\n const entry = JSON.parse(lastResponse);\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = contentToText(entry.message.content);\n\n // Look for COMPLETED line\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch) {\n message = completedMatch[1].trim()\n .replace(/\\*+/g, '')\n .replace(/\\[.*?\\]/g, '')\n .trim();\n console.error(`COMPLETION: ${message}`);\n }\n }\n } catch (e) {\n console.error('Error parsing assistant response:', e);\n }\n\n // Set tab title\n let tabTitle = message || '';\n\n if (!tabTitle && lastUserQuery) {\n try {\n const entry = JSON.parse(lastResponse);\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = contentToText(entry.message.content);\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/im);\n if (completedMatch) {\n tabTitle = completedMatch[1].trim()\n .replace(/\\*+/g, '')\n .replace(/\\[.*?\\]/g, '')\n .trim();\n }\n }\n } catch (e) {}\n\n if (!tabTitle) {\n tabTitle = generateTabTitle(lastUserQuery, '');\n }\n }\n\n if (tabTitle) {\n try {\n const escapedTitle = tabTitle.replace(/'/g, \"'\\\\''\");\n const { execSync } = await import('child_process');\n execSync(`printf '\\\\033]0;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]2;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]30;${escapedTitle}\\\\007' >&2`);\n console.error(`Tab title set to: \"${tabTitle}\"`);\n } catch (e) {\n console.error(`Failed to set tab title: ${e}`);\n }\n }\n\n console.error(`User query: ${lastUserQuery || 'No query found'}`);\n console.error(`Message: ${message || 'No completion message'}`);\n\n // Final tab title override as the very last action\n if (message) {\n const finalTabTitle = message.slice(0, 50);\n process.stderr.write(`\\x1b]2;${finalTabTitle}\\x07`);\n }\n\n // Send ntfy.sh notification\n if (message) {\n await sendNtfyNotification(message);\n } else {\n await sendNtfyNotification('Session ended');\n }\n\n // Finalize session note if one exists\n try {\n const notesInfo = findNotesDir(cwd);\n console.error(`Notes directory: ${notesInfo.path} (${notesInfo.isLocal ? 'local' : 'central'})`);\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n // FIRST: Extract and add work items from transcript\n const workItems = extractWorkFromTranscript(lines);\n if (workItems.length > 0) {\n addWorkToSessionNote(currentNotePath, workItems);\n console.error(`Added ${workItems.length} work item(s) to session note`);\n } else {\n // If no structured work items found, at least add the completion message\n if (message) {\n addWorkToSessionNote(currentNotePath, [{\n title: message,\n completed: true\n }]);\n console.error(`Added completion message to session note`);\n }\n }\n\n // THEN: Finalize the note\n const summary = message || 'Session completed.';\n finalizeSessionNote(currentNotePath, summary);\n console.error(`Session note finalized: ${basename(currentNotePath)}`);\n\n // Update TODO.md ## Continue section so next session has context\n try {\n const stateLines: string[] = [];\n stateLines.push(`Working directory: ${cwd}`);\n if (workItems.length > 0) {\n stateLines.push('');\n stateLines.push('Work completed:');\n for (const item of workItems.slice(0, 5)) {\n stateLines.push(`- ${item.title}`);\n }\n }\n if (message) {\n stateLines.push('');\n stateLines.push(`Last completed: ${message}`);\n }\n const state = stateLines.join('\\n');\n updateTodoContinue(cwd, basename(currentNotePath), state, 'session-end');\n } catch (todoError) {\n console.error(`Could not update TODO.md: ${todoError}`);\n }\n }\n } catch (noteError) {\n console.error(`Could not finalize session note: ${noteError}`);\n }\n\n // Move all session .jsonl files to sessions/ subdirectory\n try {\n const transcriptDir = dirname(transcriptPath);\n const movedCount = moveSessionFilesToSessionsDir(transcriptDir);\n if (movedCount > 0) {\n console.error(`Moved ${movedCount} session file(s) to sessions/`);\n }\n } catch (moveError) {\n console.error(`Could not move session files: ${moveError}`);\n }\n\n console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${new Date().toISOString()}\\n`);\n}\n\nmain().catch(() => {});\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/** 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 * 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 (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 (firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems) return sanitizeForFilename(numberedItems[1]);\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.') {\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) return sanitizeForFilename(cleanSummary);\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", "/**\n * TODO.md management \u2014 creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { findTodoPath } from './paths.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\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// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp 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 const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\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 exactly ONE timestamp line at the end.\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 const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\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 * 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\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 content = content.replace(/^\\s+/, '');\n\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 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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",
|
|
6
|
-
"names": ["readFileSync", "basename", "existsSync", "join", "join", "existsSync", "join", "existsSync", "join", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "readdirSync", "join", "existsSync", "readFileSync", "basename", "join", "renameSync", "existsSync", "readFileSync", "basename", "existsSync", "mkdirSync", "readFileSync", "writeFileSync", "join", "existsSync", "join", "mkdirSync", "writeFileSync", "readFileSync", "writeFileSync", "
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\nimport { readFileSync } from 'fs';\nimport { join, basename, dirname } from 'path';\nimport { connect } from 'net';\nimport { randomUUID } from 'crypto';\nimport {\n sendNtfyNotification,\n getCurrentNotePath,\n finalizeSessionNote,\n moveSessionFilesToSessionsDir,\n addWorkToSessionNote,\n findNotesDir,\n isProbeSession,\n updateTodoContinue,\n WorkItem\n} from '../lib/project-utils';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DAEMON_SOCKET = process.env.PAI_SOCKET ?? '/tmp/pai.sock';\nconst DAEMON_TIMEOUT_MS = 3_000;\n\n// ---------------------------------------------------------------------------\n// Helper: safely convert Claude content (string | Block[]) to plain text\n// ---------------------------------------------------------------------------\n\nfunction contentToText(content: any): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === 'string') return c;\n if (c?.text) return c.text;\n if (c?.content) return String(c.content);\n return '';\n })\n .join(' ')\n .trim();\n }\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Helper: extract COMPLETED: line from the last assistant response\n// ---------------------------------------------------------------------------\n\nfunction extractCompletedMessage(lines: string[]): string {\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]);\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = contentToText(entry.message.content);\n const m = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (m) {\n return m[1].trim().replace(/\\*+/g, '').replace(/\\[.*?\\]/g, '').trim();\n }\n }\n } catch {\n // Skip invalid JSON\n }\n }\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Daemon IPC relay \u2014 fast path\n// ---------------------------------------------------------------------------\n\n/**\n * Try to enqueue work with the daemon over its Unix socket.\n * Returns true on success, false if the daemon is unreachable.\n * Times out after DAEMON_TIMEOUT_MS so the hook doesn't block.\n */\nfunction enqueueWithDaemon(payload: {\n transcriptPath: string;\n cwd: string;\n message: string;\n}): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = '';\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer !== null) { clearTimeout(timer); timer = null; }\n try { client.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const client = connect(DAEMON_SOCKET, () => {\n const msg = JSON.stringify({\n id: randomUUID(),\n method: 'work_queue_enqueue',\n params: {\n type: 'session-end',\n priority: 2,\n payload: {\n transcriptPath: payload.transcriptPath,\n cwd: payload.cwd,\n message: payload.message,\n },\n },\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf('\\n');\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n try {\n const response = JSON.parse(line) as { ok: boolean; error?: string };\n if (response.ok) {\n console.error(`STOP-HOOK: Work enqueued with daemon (id=${(response as any).result?.id}).`);\n finish(true);\n } else {\n console.error(`STOP-HOOK: Daemon rejected enqueue: ${response.error}`);\n finish(false);\n }\n } catch {\n finish(false);\n }\n });\n\n client.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {\n console.error('STOP-HOOK: Daemon not running \u2014 falling back to direct execution.');\n } else {\n console.error(`STOP-HOOK: Daemon socket error: ${e.message}`);\n }\n finish(false);\n });\n\n client.on('end', () => { if (!done) finish(false); });\n\n timer = setTimeout(() => {\n console.error(`STOP-HOOK: Daemon timeout after ${DAEMON_TIMEOUT_MS}ms \u2014 falling back.`);\n finish(false);\n }, DAEMON_TIMEOUT_MS);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Direct execution \u2014 fallback path (original stop-hook logic)\n// ---------------------------------------------------------------------------\n\n/**\n * Extract work items from transcript for session note.\n * Looks for SUMMARY, ACTIONS, RESULTS sections in assistant responses.\n */\nfunction extractWorkFromTranscript(lines: string[]): WorkItem[] {\n const workItems: WorkItem[] = [];\n const seenSummaries = new Set<string>();\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = contentToText(entry.message.content);\n\n // Look for SUMMARY: lines (our standard format)\n const summaryMatch = content.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const summary = summaryMatch[1].trim();\n if (summary && !seenSummaries.has(summary) && summary.length > 5) {\n seenSummaries.add(summary);\n\n // Try to extract details from ACTIONS section\n const details: string[] = [];\n const actionsMatch = content.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n const actionLines = actionsMatch[1].split('\\n')\n .map(l => l.replace(/^[-*\u2022]\\s*/, '').replace(/^\\d+\\.\\s*/, '').trim())\n .filter(l => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3));\n }\n\n workItems.push({\n title: summary,\n details: details.length > 0 ? details : undefined,\n completed: true\n });\n }\n }\n\n // Also look for COMPLETED: lines as backup\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch && workItems.length === 0) {\n const completed = completedMatch[1].trim().replace(/\\*+/g, '').replace(/\\[.*?\\]/g, '');\n if (completed && !seenSummaries.has(completed) && completed.length > 5) {\n seenSummaries.add(completed);\n workItems.push({ title: completed, completed: true });\n }\n }\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return workItems;\n}\n\n/**\n * Generate 4-word tab title summarizing what was done.\n */\nfunction generateTabTitle(prompt: string, completedLine?: string): string {\n if (completedLine) {\n const cleanCompleted = completedLine\n .replace(/\\*+/g, '')\n .replace(/\\[.*?\\]/g, '')\n .replace(/COMPLETED:\\s*/gi, '')\n .trim();\n\n const completedWords = cleanCompleted.split(/\\s+/)\n .filter(word => word.length > 2 &&\n !['the', 'and', 'but', 'for', 'are', 'with', 'his', 'her', 'this', 'that', 'you', 'can', 'will', 'have', 'been', 'your', 'from', 'they', 'were', 'said', 'what', 'them', 'just', 'told', 'how', 'does', 'into', 'about', 'completed'].includes(word.toLowerCase()))\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());\n\n if (completedWords.length >= 2) {\n const summary = completedWords.slice(0, 4);\n while (summary.length < 4) summary.push('Done');\n return summary.slice(0, 4).join(' ');\n }\n }\n\n const cleanPrompt = prompt.replace(/[^\\w\\s]/g, ' ').trim();\n const words = cleanPrompt.split(/\\s+/).filter(word =>\n word.length > 2 &&\n !['the', 'and', 'but', 'for', 'are', 'with', 'his', 'her', 'this', 'that', 'you', 'can', 'will', 'have', 'been', 'your', 'from', 'they', 'were', 'said', 'what', 'them', 'just', 'told', 'how', 'does', 'into', 'about'].includes(word.toLowerCase())\n );\n\n const lowerPrompt = prompt.toLowerCase();\n const actionVerbs = ['test', 'rename', 'fix', 'debug', 'research', 'write', 'create', 'make', 'build', 'implement', 'analyze', 'review', 'update', 'modify', 'generate', 'develop', 'design', 'deploy', 'configure', 'setup', 'install', 'remove', 'delete', 'add', 'check', 'verify', 'validate', 'optimize', 'refactor', 'enhance', 'improve', 'send', 'email', 'help', 'updated', 'fixed', 'created', 'built', 'added'];\n let titleWords: string[] = [];\n\n for (const verb of actionVerbs) {\n if (lowerPrompt.includes(verb)) {\n let pastTense = verb;\n if (verb === 'write') pastTense = 'Wrote';\n else if (verb === 'make') pastTense = 'Made';\n else if (verb === 'send') pastTense = 'Sent';\n else if (verb.endsWith('e')) pastTense = verb.charAt(0).toUpperCase() + verb.slice(1, -1) + 'ed';\n else pastTense = verb.charAt(0).toUpperCase() + verb.slice(1) + 'ed';\n titleWords.push(pastTense);\n break;\n }\n }\n\n const remainingWords = words\n .filter(word => !actionVerbs.includes(word.toLowerCase()))\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());\n\n for (const word of remainingWords) {\n if (titleWords.length < 4) titleWords.push(word);\n else break;\n }\n\n if (titleWords.length === 0) titleWords.push('Completed');\n if (titleWords.length === 1) titleWords.push('Task');\n if (titleWords.length === 2) titleWords.push('Successfully');\n if (titleWords.length === 3) titleWords.push('Done');\n\n return titleWords.slice(0, 4).join(' ');\n}\n\n/**\n * Do the heavy work directly in the hook process.\n * Used when the daemon is unreachable.\n */\nasync function executeDirectly(\n lines: string[],\n transcriptPath: string,\n cwd: string,\n message: string,\n lastUserQuery: string\n): Promise<void> {\n // Set terminal tab title\n let tabTitle = message || '';\n if (!tabTitle && lastUserQuery) {\n tabTitle = generateTabTitle(lastUserQuery, '');\n }\n\n if (tabTitle) {\n try {\n const escapedTitle = tabTitle.replace(/'/g, \"'\\\\''\");\n const { execSync } = await import('child_process');\n execSync(`printf '\\\\033]0;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]2;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]30;${escapedTitle}\\\\007' >&2`);\n console.error(`Tab title set to: \"${tabTitle}\"`);\n } catch (e) {\n console.error(`Failed to set tab title: ${e}`);\n }\n }\n\n // Final tab title override\n if (message) {\n const finalTabTitle = message.slice(0, 50);\n process.stderr.write(`\\x1b]2;${finalTabTitle}\\x07`);\n }\n\n // Finalize session note\n try {\n const notesInfo = findNotesDir(cwd);\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n const workItems = extractWorkFromTranscript(lines);\n if (workItems.length > 0) {\n addWorkToSessionNote(currentNotePath, workItems);\n console.error(`Added ${workItems.length} work item(s) to session note`);\n } else if (message) {\n addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);\n console.error(`Added completion message to session note`);\n }\n\n const summary = message || 'Session completed.';\n finalizeSessionNote(currentNotePath, summary);\n console.error(`Session note finalized: ${basename(currentNotePath)}`);\n\n try {\n const stateLines: string[] = [];\n stateLines.push(`Working directory: ${cwd}`);\n if (workItems.length > 0) {\n stateLines.push('', 'Work completed:');\n for (const item of workItems.slice(0, 5)) {\n stateLines.push(`- ${item.title}`);\n }\n }\n if (message) {\n stateLines.push('', `Last completed: ${message}`);\n }\n updateTodoContinue(cwd, basename(currentNotePath), stateLines.join('\\n'), 'session-end');\n } catch (todoError) {\n console.error(`Could not update TODO.md: ${todoError}`);\n }\n }\n } catch (noteError) {\n console.error(`Could not finalize session note: ${noteError}`);\n }\n\n // Move session .jsonl files to sessions/\n try {\n const transcriptDir = dirname(transcriptPath);\n const movedCount = moveSessionFilesToSessionsDir(transcriptDir);\n if (movedCount > 0) {\n console.error(`Moved ${movedCount} session file(s) to sessions/`);\n }\n } catch (moveError) {\n console.error(`Could not move session files: ${moveError}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\nasync function main() {\n if (isProbeSession()) {\n process.exit(0);\n }\n\n const timestamp = new Date().toISOString();\n console.error(`\\nSTOP-HOOK TRIGGERED AT ${timestamp}`);\n\n // Read stdin\n let input = '';\n const decoder = new TextDecoder();\n try {\n for await (const chunk of process.stdin) {\n input += decoder.decode(chunk, { stream: true });\n }\n } catch (e) {\n console.error(`Error reading input: ${e}`);\n process.exit(0);\n }\n\n if (!input) {\n console.error('No input received');\n process.exit(0);\n }\n\n let transcriptPath: string;\n let cwd: string;\n try {\n const parsed = JSON.parse(input);\n transcriptPath = parsed.transcript_path;\n cwd = parsed.cwd || process.cwd();\n console.error(`Transcript path: ${transcriptPath}`);\n console.error(`Working directory: ${cwd}`);\n } catch (e) {\n console.error(`Error parsing input JSON: ${e}`);\n process.exit(0);\n }\n\n if (!transcriptPath) {\n console.error('No transcript_path in input');\n process.exit(0);\n }\n\n // Read transcript\n let transcript: string;\n try {\n transcript = readFileSync(transcriptPath, 'utf-8');\n console.error(`Transcript loaded: ${transcript.split('\\n').length} lines`);\n } catch (e) {\n console.error(`Error reading transcript: ${e}`);\n process.exit(0);\n }\n\n const lines = transcript.trim().split('\\n');\n\n // Extract last user query for tab title / fallback\n let lastUserQuery = '';\n for (let i = lines.length - 1; i >= 0; i--) {\n try {\n const entry = JSON.parse(lines[i]);\n if (entry.type === 'user' && entry.message?.content) {\n const content = entry.message.content;\n if (typeof content === 'string') {\n lastUserQuery = content;\n } else if (Array.isArray(content)) {\n for (const item of content) {\n if (item.type === 'text' && item.text) {\n lastUserQuery = item.text;\n break;\n }\n }\n }\n if (lastUserQuery) break;\n }\n } catch {\n // Skip invalid JSON\n }\n }\n\n // Extract completion message\n const message = extractCompletedMessage(lines);\n\n console.error(`User query: ${lastUserQuery || 'No query found'}`);\n console.error(`Message: ${message || 'No completion message'}`);\n\n // Always set terminal tab title immediately (fast, no daemon needed)\n let tabTitle = message || '';\n if (!tabTitle && lastUserQuery) {\n tabTitle = generateTabTitle(lastUserQuery, '');\n }\n if (tabTitle) {\n try {\n const { execSync } = await import('child_process');\n const escapedTitle = tabTitle.replace(/'/g, \"'\\\\''\");\n execSync(`printf '\\\\033]0;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]2;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]30;${escapedTitle}\\\\007' >&2`);\n console.error(`Tab title set to: \"${tabTitle}\"`);\n } catch (e) {\n console.error(`Failed to set tab title: ${e}`);\n }\n }\n if (message) {\n process.stderr.write(`\\x1b]2;${message.slice(0, 50)}\\x07`);\n }\n\n // Send ntfy.sh notification (fast, fire-and-forget)\n if (message) {\n await sendNtfyNotification(message);\n } else {\n await sendNtfyNotification('Session ended');\n }\n\n // -----------------------------------------------------------------------\n // Relay heavy work to daemon \u2014 fall back to direct execution if unavailable\n // -----------------------------------------------------------------------\n const relayed = await enqueueWithDaemon({\n transcriptPath,\n cwd,\n message,\n });\n\n if (!relayed) {\n console.error('STOP-HOOK: Using direct execution fallback.');\n await executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery);\n }\n\n console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${new Date().toISOString()}\\n`);\n}\n\nmain().catch(() => {});\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/** 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, and \"[object Object]\" artifacts.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (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 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", "/**\n * TODO.md management \u2014 creation, task updates, checkpoints, and Continue section.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { findTodoPath } from './paths.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Task item for TODO.md. */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Ensure TODO.md exists. Creates it with default structure if missing.\n * Returns the path to the TODO.md file.\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\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// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Update TODO.md with current session tasks.\n * Preserves the Backlog section and ensures exactly ONE timestamp 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 const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n const backlogSection = backlogMatch\n ? backlogMatch[0].trim()\n : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\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 exactly ONE timestamp line at the end.\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 const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4;\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\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 * 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\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 content = content.replace(/^\\s+/, '');\n\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 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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",
|
|
6
|
+
"names": ["readFileSync", "basename", "existsSync", "join", "join", "existsSync", "join", "existsSync", "join", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "readdirSync", "join", "existsSync", "readFileSync", "basename", "join", "renameSync", "existsSync", "readFileSync", "basename", "existsSync", "mkdirSync", "readFileSync", "writeFileSync", "join", "existsSync", "join", "mkdirSync", "writeFileSync", "readFileSync", "writeFileSync", "resolve", "basename", "readFileSync"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/ts/post-tool-use/sync-todo-to-md.ts", "../../src/hooks/ts/lib/project-utils/paths.ts", "../../src/hooks/ts/lib/pai-paths.ts", "../../src/hooks/ts/lib/project-utils/session-notes.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n/**\n * sync-todo-to-md.ts\n *\n * PostToolUse hook for TodoWrite that:\n * 1. Syncs Claude's todos to TODO.md \"Current Session\" section\n * 2. PRESERVES all user-managed sections (Plans, Completed, Backlog, etc.)\n * 3. Adds completed items to the session note\n *\n * IMPORTANT: This hook PRESERVES user content. It only updates \"Current Session\".\n */\n\nimport { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport {\n findTodoPath,\n findNotesDir,\n getCurrentNotePath,\n addWorkToSessionNote,\n type WorkItem\n} from '../lib/project-utils';\n\ninterface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n activeForm: string;\n}\n\ninterface HookInput {\n session_id: string;\n cwd: string;\n tool_name: string;\n tool_input: {\n todos: TodoItem[];\n };\n}\n\n/**\n * Format current session todos as markdown\n */\nfunction formatSessionTodos(todos: TodoItem[]): string {\n const inProgress = todos.filter(t => t.status === 'in_progress');\n const pending = todos.filter(t => t.status === 'pending');\n const completed = todos.filter(t => t.status === 'completed');\n\n let content = '';\n\n if (inProgress.length > 0) {\n content += `### In Progress\\n\\n`;\n for (const todo of inProgress) {\n content += `- [ ] **${todo.content}** _(${todo.activeForm})_\\n`;\n }\n content += '\\n';\n }\n\n if (pending.length > 0) {\n content += `### Pending\\n\\n`;\n for (const todo of pending) {\n content += `- [ ] ${todo.content}\\n`;\n }\n content += '\\n';\n }\n\n if (completed.length > 0) {\n content += `### Completed\\n\\n`;\n for (const todo of completed) {\n content += `- [x] ${todo.content}\\n`;\n }\n content += '\\n';\n }\n\n if (todos.length === 0) {\n content += `_(No active session tasks)_\\n\\n`;\n }\n\n return content;\n}\n\n/**\n * Extract all sections from TODO.md EXCEPT \"Current Session\"\n * These are user-managed sections that should be preserved.\n */\nfunction extractPreservedSections(content: string): string {\n let preserved = '';\n\n // Match all ## sections that are NOT \"Current Session\"\n const sectionRegex = /\\n(## (?!Current Session)[^\\n]+[\\s\\S]*?)(?=\\n## |\\n---\\n+\\*Last updated|$)/g;\n const matches = content.matchAll(sectionRegex);\n\n for (const match of matches) {\n preserved += match[1];\n }\n\n return preserved;\n}\n\n/**\n * Fix malformed headings: Remove --- prefix from headings (---# \u2192 #)\n * Claude sometimes incorrectly merges horizontal rules with headings.\n */\nfunction fixMalformedHeadings(content: string): string {\n return content.replace(/^---#/gm, '#');\n}\n\n/**\n * Build new TODO.md preserving user sections\n */\nfunction buildTodoContent(todos: TodoItem[], existingContent: string): string {\n const now = new Date().toISOString();\n\n // Get all preserved sections (everything except Current Session)\n const preserved = extractPreservedSections(existingContent);\n\n // Build new content\n let content = `# TODO\n\n## Current Session\n\n${formatSessionTodos(todos)}`;\n\n // Add preserved sections\n if (preserved.trim()) {\n content += preserved;\n }\n\n // Ensure we end with exactly one timestamp\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)*$/, '');\n content += `\\n---\\n\\n*Last updated: ${now}*\\n`;\n\n return content;\n}\n\nasync function main() {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const stdinData = Buffer.concat(chunks).toString('utf-8');\n\n if (!stdinData.trim()) {\n console.error('No input received');\n process.exit(0);\n }\n\n const hookInput: HookInput = JSON.parse(stdinData);\n\n if (hookInput.tool_name !== 'TodoWrite') {\n process.exit(0);\n }\n\n const todos = hookInput.tool_input?.todos;\n\n if (!todos || !Array.isArray(todos)) {\n console.error('No todos in tool input');\n process.exit(0);\n }\n\n const cwd = hookInput.cwd || process.cwd();\n\n // Find TODO.md path\n const todoPath = findTodoPath(cwd);\n\n // Create TODO.md if it doesn't exist\n if (!existsSync(todoPath)) {\n const parentDir = todoPath.replace(/\\/[^/]+$/, '');\n mkdirSync(parentDir, { recursive: true });\n console.error(`Creating TODO.md at ${todoPath}`);\n }\n\n // Read existing content to preserve user sections\n let existingContent = '';\n try {\n existingContent = readFileSync(todoPath, 'utf-8');\n } catch (e) {\n // New file, no content to preserve\n }\n\n // Build and write new content (with heading fix)\n let newContent = buildTodoContent(todos, existingContent);\n newContent = fixMalformedHeadings(newContent);\n writeFileSync(todoPath, newContent);\n\n const stats = {\n inProgress: todos.filter(t => t.status === 'in_progress').length,\n pending: todos.filter(t => t.status === 'pending').length,\n completed: todos.filter(t => t.status === 'completed').length\n };\n console.error(`TODO.md synced: ${stats.inProgress} in progress, ${stats.pending} pending, ${stats.completed} completed`);\n\n // Add completed items to session note (if local Notes/ exists)\n const completedTodos = todos.filter(t => t.status === 'completed');\n\n if (completedTodos.length > 0) {\n const notesInfo = findNotesDir(cwd);\n\n if (notesInfo.isLocal) {\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n let noteContent = '';\n try {\n noteContent = readFileSync(currentNotePath, 'utf-8');\n } catch (e) {\n console.error('Could not read session note:', e);\n }\n\n const newlyCompleted = completedTodos.filter(t => !noteContent.includes(t.content));\n\n if (newlyCompleted.length > 0) {\n const workItems: WorkItem[] = newlyCompleted.map(t => ({\n title: t.content,\n completed: true\n }));\n\n addWorkToSessionNote(currentNotePath, workItems);\n console.error(`Added ${newlyCompleted.length} completed item(s) to session note`);\n }\n }\n }\n }\n\n } catch (error) {\n console.error('sync-todo-to-md error:', error);\n }\n\n process.exit(0);\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", "/**\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/** 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 * 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 (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 (firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems) return sanitizeForFilename(numberedItems[1]);\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.') {\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) return sanitizeForFilename(cleanSummary);\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"],
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n/**\n * sync-todo-to-md.ts\n *\n * PostToolUse hook for TodoWrite that:\n * 1. Syncs Claude's todos to TODO.md \"Current Session\" section\n * 2. PRESERVES all user-managed sections (Plans, Completed, Backlog, etc.)\n * 3. Adds completed items to the session note\n *\n * IMPORTANT: This hook PRESERVES user content. It only updates \"Current Session\".\n */\n\nimport { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport {\n findTodoPath,\n findNotesDir,\n getCurrentNotePath,\n addWorkToSessionNote,\n type WorkItem\n} from '../lib/project-utils';\n\ninterface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n activeForm: string;\n}\n\ninterface HookInput {\n session_id: string;\n cwd: string;\n tool_name: string;\n tool_input: {\n todos: TodoItem[];\n };\n}\n\n/**\n * Format current session todos as markdown\n */\nfunction formatSessionTodos(todos: TodoItem[]): string {\n const inProgress = todos.filter(t => t.status === 'in_progress');\n const pending = todos.filter(t => t.status === 'pending');\n const completed = todos.filter(t => t.status === 'completed');\n\n let content = '';\n\n if (inProgress.length > 0) {\n content += `### In Progress\\n\\n`;\n for (const todo of inProgress) {\n content += `- [ ] **${todo.content}** _(${todo.activeForm})_\\n`;\n }\n content += '\\n';\n }\n\n if (pending.length > 0) {\n content += `### Pending\\n\\n`;\n for (const todo of pending) {\n content += `- [ ] ${todo.content}\\n`;\n }\n content += '\\n';\n }\n\n if (completed.length > 0) {\n content += `### Completed\\n\\n`;\n for (const todo of completed) {\n content += `- [x] ${todo.content}\\n`;\n }\n content += '\\n';\n }\n\n if (todos.length === 0) {\n content += `_(No active session tasks)_\\n\\n`;\n }\n\n return content;\n}\n\n/**\n * Extract all sections from TODO.md EXCEPT \"Current Session\"\n * These are user-managed sections that should be preserved.\n */\nfunction extractPreservedSections(content: string): string {\n let preserved = '';\n\n // Match all ## sections that are NOT \"Current Session\"\n const sectionRegex = /\\n(## (?!Current Session)[^\\n]+[\\s\\S]*?)(?=\\n## |\\n---\\n+\\*Last updated|$)/g;\n const matches = content.matchAll(sectionRegex);\n\n for (const match of matches) {\n preserved += match[1];\n }\n\n return preserved;\n}\n\n/**\n * Fix malformed headings: Remove --- prefix from headings (---# \u2192 #)\n * Claude sometimes incorrectly merges horizontal rules with headings.\n */\nfunction fixMalformedHeadings(content: string): string {\n return content.replace(/^---#/gm, '#');\n}\n\n/**\n * Build new TODO.md preserving user sections\n */\nfunction buildTodoContent(todos: TodoItem[], existingContent: string): string {\n const now = new Date().toISOString();\n\n // Get all preserved sections (everything except Current Session)\n const preserved = extractPreservedSections(existingContent);\n\n // Build new content\n let content = `# TODO\n\n## Current Session\n\n${formatSessionTodos(todos)}`;\n\n // Add preserved sections\n if (preserved.trim()) {\n content += preserved;\n }\n\n // Ensure we end with exactly one timestamp\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)*$/, '');\n content += `\\n---\\n\\n*Last updated: ${now}*\\n`;\n\n return content;\n}\n\nasync function main() {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const stdinData = Buffer.concat(chunks).toString('utf-8');\n\n if (!stdinData.trim()) {\n console.error('No input received');\n process.exit(0);\n }\n\n const hookInput: HookInput = JSON.parse(stdinData);\n\n if (hookInput.tool_name !== 'TodoWrite') {\n process.exit(0);\n }\n\n const todos = hookInput.tool_input?.todos;\n\n if (!todos || !Array.isArray(todos)) {\n console.error('No todos in tool input');\n process.exit(0);\n }\n\n const cwd = hookInput.cwd || process.cwd();\n\n // Find TODO.md path\n const todoPath = findTodoPath(cwd);\n\n // Create TODO.md if it doesn't exist\n if (!existsSync(todoPath)) {\n const parentDir = todoPath.replace(/\\/[^/]+$/, '');\n mkdirSync(parentDir, { recursive: true });\n console.error(`Creating TODO.md at ${todoPath}`);\n }\n\n // Read existing content to preserve user sections\n let existingContent = '';\n try {\n existingContent = readFileSync(todoPath, 'utf-8');\n } catch (e) {\n // New file, no content to preserve\n }\n\n // Build and write new content (with heading fix)\n let newContent = buildTodoContent(todos, existingContent);\n newContent = fixMalformedHeadings(newContent);\n writeFileSync(todoPath, newContent);\n\n const stats = {\n inProgress: todos.filter(t => t.status === 'in_progress').length,\n pending: todos.filter(t => t.status === 'pending').length,\n completed: todos.filter(t => t.status === 'completed').length\n };\n console.error(`TODO.md synced: ${stats.inProgress} in progress, ${stats.pending} pending, ${stats.completed} completed`);\n\n // Add completed items to session note (if local Notes/ exists)\n const completedTodos = todos.filter(t => t.status === 'completed');\n\n if (completedTodos.length > 0) {\n const notesInfo = findNotesDir(cwd);\n\n if (notesInfo.isLocal) {\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n let noteContent = '';\n try {\n noteContent = readFileSync(currentNotePath, 'utf-8');\n } catch (e) {\n console.error('Could not read session note:', e);\n }\n\n const newlyCompleted = completedTodos.filter(t => !noteContent.includes(t.content));\n\n if (newlyCompleted.length > 0) {\n const workItems: WorkItem[] = newlyCompleted.map(t => ({\n title: t.content,\n completed: true\n }));\n\n addWorkToSessionNote(currentNotePath, workItems);\n console.error(`Added ${newlyCompleted.length} completed item(s) to session note`);\n }\n }\n }\n }\n\n } catch (error) {\n console.error('sync-todo-to-md error:', error);\n }\n\n process.exit(0);\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", "/**\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/** 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, and \"[object Object]\" artifacts.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (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 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": "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",
|
|
6
6
|
"names": ["writeFileSync", "existsSync", "readFileSync", "mkdirSync", "existsSync", "join", "join", "join", "existsSync", "join", "existsSync", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "readdirSync", "join", "existsSync", "readFileSync", "basename", "existsSync", "mkdirSync", "readFileSync", "writeFileSync"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -215,6 +215,21 @@ export function sanitizeForFilename(str: string): string {
|
|
|
215
215
|
.substring(0, 50);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Return true if the candidate string should be rejected as a meaningful name.
|
|
220
|
+
* Rejects file paths, shebangs, timestamps, and "[object Object]" artifacts.
|
|
221
|
+
*/
|
|
222
|
+
function isMeaninglessCandidate(text: string): boolean {
|
|
223
|
+
const t = text.trim();
|
|
224
|
+
if (!t) return true;
|
|
225
|
+
if (t.startsWith('/')) return true; // file path
|
|
226
|
+
if (t.startsWith('#!')) return true; // shebang
|
|
227
|
+
if (t.includes('[object Object]')) return true; // serialization artifact
|
|
228
|
+
if (/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp
|
|
229
|
+
if (/^\d{1,2}:\d{2}(:\d{2})?(\s*(AM|PM))?$/i.test(t)) return true; // time-only
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
218
233
|
/**
|
|
219
234
|
* Extract a meaningful name from session note content and summary.
|
|
220
235
|
* Looks at Work Done section headers, bold text, and summary.
|
|
@@ -228,7 +243,7 @@ export function extractMeaningfulName(noteContent: string, summary: string): str
|
|
|
228
243
|
const subheadings = workDoneSection.match(/### ([^\n]+)/g);
|
|
229
244
|
if (subheadings && subheadings.length > 0) {
|
|
230
245
|
const firstHeading = subheadings[0].replace('### ', '').trim();
|
|
231
|
-
if (firstHeading.length > 5 && firstHeading.length < 60) {
|
|
246
|
+
if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {
|
|
232
247
|
return sanitizeForFilename(firstHeading);
|
|
233
248
|
}
|
|
234
249
|
}
|
|
@@ -236,23 +251,27 @@ export function extractMeaningfulName(noteContent: string, summary: string): str
|
|
|
236
251
|
const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
|
|
237
252
|
if (boldMatches && boldMatches.length > 0) {
|
|
238
253
|
const firstBold = boldMatches[0].replace(/\*\*/g, '').trim();
|
|
239
|
-
if (firstBold.length > 3 && firstBold.length < 50) {
|
|
254
|
+
if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {
|
|
240
255
|
return sanitizeForFilename(firstBold);
|
|
241
256
|
}
|
|
242
257
|
}
|
|
243
258
|
|
|
244
259
|
const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
|
|
245
|
-
if (numberedItems
|
|
260
|
+
if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {
|
|
261
|
+
return sanitizeForFilename(numberedItems[1]);
|
|
262
|
+
}
|
|
246
263
|
}
|
|
247
264
|
|
|
248
|
-
if (summary && summary.length > 5 && summary !== 'Session completed.') {
|
|
265
|
+
if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {
|
|
249
266
|
const cleanSummary = summary
|
|
250
267
|
.replace(/[^\w\s-]/g, ' ')
|
|
251
268
|
.trim()
|
|
252
269
|
.split(/\s+/)
|
|
253
270
|
.slice(0, 5)
|
|
254
271
|
.join(' ');
|
|
255
|
-
if (cleanSummary.length > 3
|
|
272
|
+
if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {
|
|
273
|
+
return sanitizeForFilename(cleanSummary);
|
|
274
|
+
}
|
|
256
275
|
}
|
|
257
276
|
|
|
258
277
|
return '';
|