@tekmidian/pai 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +72 -1
- package/README.md +107 -3
- package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
- package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
- package/dist/cli/index.mjs +1897 -1569
- package/dist/cli/index.mjs.map +1 -1
- package/dist/clusters-JIDQW65f.mjs +201 -0
- package/dist/clusters-JIDQW65f.mjs.map +1 -0
- package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
- package/dist/config-BuhHWyOK.mjs.map +1 -0
- package/dist/daemon/index.mjs +12 -9
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
- package/dist/daemon-D3hYb5_C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +4597 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
- package/dist/db-BtuN768f.mjs.map +1 -0
- package/dist/db-DdUperSl.mjs +110 -0
- package/dist/db-DdUperSl.mjs.map +1 -0
- package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
- package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
- package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
- package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
- package/dist/helpers-BEST-4Gx.mjs +420 -0
- package/dist/helpers-BEST-4Gx.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +19 -4
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/capture-session-summary.mjs +38 -0
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +6 -12
- package/dist/hooks/cleanup-session-files.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +105 -111
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +26 -17
- package/dist/hooks/initialize-session.mjs.map +4 -4
- package/dist/hooks/inject-observations.mjs +220 -0
- package/dist/hooks/inject-observations.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +18 -2
- package/dist/hooks/load-core-context.mjs.map +4 -4
- package/dist/hooks/load-project-context.mjs +102 -97
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/observe.mjs +354 -0
- package/dist/hooks/observe.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +174 -90
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/sync-todo-to-md.mjs +31 -33
- package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
- package/dist/index.d.mts +32 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -9
- package/dist/indexer-D53l5d1U.mjs +1 -0
- package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
- package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
- package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
- package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
- package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
- package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
- package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
- package/dist/note-context-BK24bX8Y.mjs +126 -0
- package/dist/note-context-BK24bX8Y.mjs.map +1 -0
- package/dist/postgres-CKf-EDtS.mjs +846 -0
- package/dist/postgres-CKf-EDtS.mjs.map +1 -0
- package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
- package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
- package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
- package/dist/search-DC1qhkKn.mjs.map +1 -0
- package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
- package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
- package/dist/state-C6_vqz7w.mjs +102 -0
- package/dist/state-C6_vqz7w.mjs.map +1 -0
- package/dist/stop-words-BaMEGVeY.mjs +326 -0
- package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
- package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
- package/dist/sync-BOsnEj2-.mjs.map +1 -0
- package/dist/themes-BvYF0W8T.mjs +148 -0
- package/dist/themes-BvYF0W8T.mjs.map +1 -0
- package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
- package/dist/tools-DcaJlYDN.mjs.map +1 -0
- package/dist/trace-CRx9lPuc.mjs +137 -0
- package/dist/trace-CRx9lPuc.mjs.map +1 -0
- package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
- package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
- package/dist/zettelkasten-cdajbnPr.mjs +708 -0
- package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
- package/package.json +1 -2
- package/src/hooks/ts/capture-all-events.ts +6 -0
- package/src/hooks/ts/lib/project-utils/index.ts +50 -0
- package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
- package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
- package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
- package/src/hooks/ts/lib/project-utils.ts +40 -999
- package/src/hooks/ts/post-tool-use/observe.ts +327 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
- package/src/hooks/ts/session-start/initialize-session.ts +7 -1
- package/src/hooks/ts/session-start/inject-observations.ts +254 -0
- package/src/hooks/ts/session-start/load-core-context.ts +7 -0
- package/src/hooks/ts/session-start/load-project-context.ts +8 -1
- package/src/hooks/ts/stop/stop-hook.ts +28 -0
- package/templates/claude-md.template.md +7 -74
- package/templates/skills/user/.gitkeep +0 -0
- package/dist/chunker-CbnBe0s0.mjs +0 -191
- package/dist/chunker-CbnBe0s0.mjs.map +0 -1
- package/dist/config-Cf92lGX_.mjs.map +0 -1
- package/dist/daemon-D9evGlgR.mjs.map +0 -1
- package/dist/db-4lSqLFb8.mjs.map +0 -1
- package/dist/db-Dp8VXIMR.mjs +0 -212
- package/dist/db-Dp8VXIMR.mjs.map +0 -1
- package/dist/indexer-CMPOiY1r.mjs.map +0 -1
- package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
- package/dist/mcp/index.d.mts +0 -1
- package/dist/mcp/index.mjs +0 -500
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/postgres-FXrHDPcE.mjs +0 -358
- package/dist/postgres-FXrHDPcE.mjs.map +0 -1
- package/dist/schemas-BFIgGntb.mjs +0 -3405
- package/dist/schemas-BFIgGntb.mjs.map +0 -1
- package/dist/search-_oHfguA5.mjs.map +0 -1
- package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
- package/dist/tools-DV_lsiCc.mjs.map +0 -1
- package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
- package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
- package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
- package/templates/README.md +0 -181
- package/templates/skills/createskill-skill.template.md +0 -78
- package/templates/skills/history-system.template.md +0 -371
- package/templates/skills/hook-system.template.md +0 -913
- package/templates/skills/sessions-skill.template.md +0 -102
- package/templates/skills/skill-system.template.md +0 -214
- package/templates/skills/terminal-tabs.template.md +0 -120
- package/templates/templates.md +0 -20
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/hooks/ts/post-tool-use/sync-todo-to-md.ts", "../../src/hooks/ts/lib/project-utils.ts", "../../src/hooks/ts/lib/pai-paths.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 * project-utils.ts - Shared utilities for project context management\n *\n * Provides:\n * - Path encoding (matching Claude Code's scheme)\n * - ntfy.sh notifications (mandatory, synchronous)\n * - Session notes management\n * - Session token calculation\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// Import from pai-paths which handles .env loading and path resolution\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 * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with - (hidden directories become --name)\n *\n * This matches Claude Code's internal encoding to ensure Notes\n * are stored in the same project directory as transcripts.\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-') // Slashes become dashes\n .replace(/\\./g, '-') // Dots also become dashes\n .replace(/ /g, '-'); // Spaces become dashes (matches Claude Code native encoding)\n}\n\n/**\n * Get the project directory for a given working directory\n */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/**\n * Get the Notes directory for a project (central location)\n */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory - check local first, fallback to central\n * DOES NOT create the directory - just finds the right location\n *\n * Logic:\n * - If cwd itself IS a Notes directory \u2192 use it directly\n * - If local Notes/ exists \u2192 use it (can be checked into git)\n * - Otherwise \u2192 use central ~/.claude/projects/.../Notes/\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n // FIRST: Check if cwd itself IS a Notes directory\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n // Check local locations\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes')\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n // Fallback to central location\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/**\n * Get the Sessions directory for a project (stores .jsonl transcripts)\n */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/**\n * Get the Sessions directory from a project directory path\n */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n/**\n * Check if WhatsApp (Whazaa) is configured as an enabled MCP server.\n *\n * Uses standard Claude Code config at ~/.claude/settings.json.\n * No PAI dependency \u2014 works for any Claude Code user with whazaa installed.\n */\nexport function isWhatsAppEnabled(): boolean {\n try {\n const { homedir } = require('os');\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('whazaa');\n } catch {\n return false;\n }\n}\n\n/**\n * Send push notification \u2014 WhatsApp-aware with ntfy fallback.\n *\n * When WhatsApp (Whazaa) is enabled in MCP config, ntfy is SKIPPED\n * because the AI sends WhatsApp messages directly via MCP. Sending both\n * would cause duplicate notifications.\n *\n * When WhatsApp is NOT configured, ntfy fires as the fallback channel.\n */\nexport async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {\n // Skip ntfy when WhatsApp is configured \u2014 the AI handles notifications via MCP\n if (isWhatsAppEnabled()) {\n console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);\n return true;\n }\n\n const topic = process.env.NTFY_TOPIC;\n\n if (!topic) {\n console.error('NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled');\n return false;\n }\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const response = await fetch(`https://ntfy.sh/${topic}`, {\n method: 'POST',\n body: message,\n headers: {\n 'Title': 'Claude Code',\n 'Priority': 'default'\n }\n });\n\n if (response.ok) {\n console.error(`ntfy.sh notification sent (WhatsApp inactive): \"${message}\"`);\n return true;\n } else {\n console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);\n }\n } catch (error) {\n console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);\n }\n\n // Wait before retry\n if (attempt < retries) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n\n console.error('ntfy.sh notification failed after all retries');\n return false;\n}\n\n/**\n * Ensure the Notes directory exists for a project\n * DEPRECATED: Use ensureNotesDirSmart() instead\n */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n *\n * This respects the user's choice:\n * - Projects with local Notes/ keep notes there (git-trackable)\n * - Other directories don't get cluttered with auto-created Notes/\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n\n if (found.isLocal) {\n // Local Notes/ exists - use it as-is\n return found;\n }\n\n // No local Notes/ - ensure central exists\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n\n return found;\n}\n\n/**\n * Ensure the Sessions directory exists for a project\n */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n\n return sessionsDir;\n}\n\n/**\n * Ensure the Sessions directory exists (from project dir path)\n */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory\n * @param projectDir - The project directory path\n * @param excludeFile - Optional filename to exclude (e.g., current active session)\n * @param silent - If true, suppress console output\n * Returns the number of files moved\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent: boolean = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) {\n return 0;\n }\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n // Match session files: uuid.jsonl or agent-*.jsonl\n // Skip the excluded file (typically the current active session)\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n\n try {\n renameSync(sourcePath, destPath);\n if (!silent) {\n console.error(`Moved ${file} \u2192 sessions/`);\n }\n movedCount++;\n } catch (error) {\n if (!silent) {\n console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n }\n\n return movedCount;\n}\n\n/**\n * Get the YYYY/MM subdirectory for the current month inside notesDir.\n * Creates the directory if it doesn't exist.\n */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.)\n * ALWAYS uses 4-digit format with space-dash-space separators\n * Format: NNNN - YYYY-MM-DD - Description.md\n * Numbers reset per month (each YYYY/MM directory has its own sequence).\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n // Match CORRECT format: \"0001 - \" (4-digit with space-dash-space)\n // Also match legacy formats for backwards compatibility when detecting max number\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/)) // Starts with 3-4 digits followed by separator\n .filter(f => f.endsWith('.md'))\n .sort();\n\n if (files.length === 0) {\n return '0001'; // Default to 4-digit\n }\n\n // Find the highest number across all formats\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n // ALWAYS return 4-digit format\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches in the current month's YYYY/MM subdirectory first,\n * then falls back to previous month (for sessions spanning month boundaries),\n * then falls back to flat notesDir for legacy notes.\n * Supports multiple formats for backwards compatibility:\n * - CORRECT: \"0001 - YYYY-MM-DD - Description.md\" (space-dash-space)\n * - Legacy: \"001_YYYY-MM-DD_description.md\" (underscores)\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) {\n return null;\n }\n\n // Helper: find latest session note in a directory\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n // 1. Check current month's YYYY/MM directory\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n // 2. Check previous month (for sessions spanning month boundaries)\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n // 3. Fallback: check flat notesDir (legacy notes not yet filed)\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note\n * CORRECT FORMAT: \"NNNN - YYYY-MM-DD - Description.md\"\n * - 4-digit zero-padded number\n * - Space-dash-space separators (NOT underscores)\n * - Title case description\n *\n * IMPORTANT: The initial description is just a PLACEHOLDER.\n * Claude MUST rename the file at session end with a meaningful description\n * based on the actual work done. Never leave it as \"New Session\" or project name.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD\n\n // Use \"New Session\" as placeholder - Claude MUST rename at session end!\n // The project name alone is NOT descriptive enough.\n const safeDescription = 'New Session';\n\n // CORRECT FORMAT: space-dash-space separators, filed into YYYY/MM subdirectory\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - ${safeDescription}.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/**\n * Append checkpoint to current session note\n */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n // Note vanished (cloud sync, cleanup, etc.) \u2014 recreate it\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n // Insert before \"## Next Steps\" if it exists, otherwise append\n const nextStepsIndex = content.indexOf('## Next Steps');\n let newContent: string;\n\n if (nextStepsIndex !== -1) {\n newContent = content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex);\n } else {\n newContent = content + checkpointText;\n }\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/**\n * Work item for session notes\n */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/**\n * Add work items to the \"Work Done\" section of a session note\n * This is the main way to capture what was accomplished in a session\n */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n // Build the work section content\n let workText = '';\n if (sectionTitle) {\n workText += `\\n### ${sectionTitle}\\n\\n`;\n }\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n // Find the Work Done section and insert after the comment/placeholder\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n // Fallback: insert before Next Steps\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Update the session note title to be more descriptive\n * Called when we know what work was done\n */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n // Update the H1 title\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n\n writeFileSync(notePath, content);\n\n // Also rename the file\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Sanitize a string for use in a filename (exported for use elsewhere)\n */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '') // Remove special chars\n .replace(/\\s+/g, '-') // Spaces to hyphens\n .replace(/-+/g, '-') // Collapse multiple hyphens\n .replace(/^-|-$/g, '') // Trim hyphens\n .substring(0, 50); // Limit length\n}\n\n/**\n * Extract a meaningful name from session note content\n * Looks at Work Done section and summary to generate a descriptive name\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n // Try to extract from Work Done section headers (### headings)\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n // Look for ### subheadings which typically describe what was done\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n // Use the first subheading, clean it up\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n // Look for bold text which often indicates key topics\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n // Look for numbered list items (1. Something)\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n // Fall back to summary if provided\n if (summary && summary.length > 5 && summary !== 'Session completed.') {\n // Take first meaningful phrase from summary\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n\n if (cleanSummary.length > 3) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename session note with a meaningful name\n * ALWAYS uses correct format: \"NNNN - YYYY-MM-DD - Description.md\"\n * Returns the new path, or original path if rename fails\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) {\n return notePath;\n }\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n // Parse existing filename - support multiple formats:\n // CORRECT: \"0001 - 2026-01-02 - Description.md\"\n // Legacy: \"001_2026-01-02_description.md\"\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n\n const match = correctMatch || legacyMatch;\n if (!match) {\n return notePath; // Can't parse, don't rename\n }\n\n const [, noteNumber, date] = match;\n\n // Convert to Title Case\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n // ALWAYS use correct format with 4-digit number\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n // Don't rename if name is the same\n if (newFilename === oldFilename) {\n return notePath;\n }\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/**\n * Finalize session note (mark as complete, add summary, rename with meaningful name)\n * IDEMPOTENT: Will only finalize once, subsequent calls are no-ops\n * Returns the final path (may be renamed)\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n // IDEMPOTENT CHECK: If already completed, don't modify again\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n // Update status\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n // Add completion timestamp (only if not already present)\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n // Add summary to Next Steps section (only if placeholder exists)\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n // Extract meaningful name and rename the file\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n const newPath = renameSessionNote(notePath, meaningfulName);\n return newPath;\n }\n\n return notePath;\n}\n\n/**\n * Calculate total tokens from a session .jsonl file\n */\nexport function calculateSessionTokens(jsonlPath: string): number {\n if (!existsSync(jsonlPath)) {\n return 0;\n }\n\n try {\n const content = readFileSync(jsonlPath, 'utf-8');\n const lines = content.trim().split('\\n');\n let totalTokens = 0;\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.message?.usage) {\n const usage = entry.message.usage;\n totalTokens += (usage.input_tokens || 0);\n totalTokens += (usage.output_tokens || 0);\n totalTokens += (usage.cache_creation_input_tokens || 0);\n totalTokens += (usage.cache_read_input_tokens || 0);\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return totalTokens;\n } catch (error) {\n console.error(`Error calculating tokens: ${error}`);\n return 0;\n }\n}\n\n/**\n * Find TODO.md - check local first, fallback to central\n */\nexport function findTodoPath(cwd: string): string {\n // Check local locations first\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md')\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return path;\n }\n }\n\n // Fallback to central location (inside Notes/)\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/**\n * Find CLAUDE.md - check local locations\n * Returns the FIRST found path (for backwards compatibility)\n */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations\n * Returns paths in priority order (most specific first):\n * 1. .claude/CLAUDE.md (project-specific config dir)\n * 2. CLAUDE.md (project root)\n * 3. Notes/CLAUDE.md (notes directory)\n * 4. Prompts/CLAUDE.md (prompts directory)\n *\n * All found files will be loaded and injected into context.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n // Priority order: most specific first\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md')\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n foundPaths.push(path);\n }\n }\n\n return foundPaths;\n}\n\n/**\n * Ensure TODO.md exists\n */\nexport function ensureTodoMd(cwd: string): string {\n const todoPath = findTodoPath(cwd);\n\n if (!existsSync(todoPath)) {\n // Ensure parent directory exists\n const parentDir = join(todoPath, '..');\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n\n const content = `# TODO\n\n## Current Session\n\n- [ ] (Tasks will be tracked here)\n\n## Backlog\n\n- [ ] (Future tasks)\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Created TODO.md: ${todoPath}`);\n }\n\n return todoPath;\n}\n\n/**\n * Task item for TODO.md\n */\nexport interface TodoItem {\n content: string;\n completed: boolean;\n}\n\n/**\n * Update TODO.md with current session tasks\n * Preserves the Backlog section\n * Ensures only ONE timestamp line at the end\n */\nexport function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {\n const todoPath = ensureTodoMd(cwd);\n const content = readFileSync(todoPath, 'utf-8');\n\n // Find Backlog section to preserve it (but strip any trailing timestamps/separators)\n const backlogMatch = content.match(/## Backlog[\\s\\S]*?(?=\\n---|\\n\\*Last updated|$)/);\n let backlogSection = backlogMatch ? backlogMatch[0].trim() : '## Backlog\\n\\n- [ ] (Future tasks)';\n\n // Format tasks\n const taskLines = tasks.length > 0\n ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\\n')\n : '- [ ] (No active tasks)';\n\n // Build new content with exactly ONE timestamp at the end\n const newContent = `# TODO\n\n## Current Session\n\n${taskLines}\n\n${sessionSummary ? `**Session Summary:** ${sessionSummary}\\n\\n` : ''}${backlogSection}\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;\n\n writeFileSync(todoPath, newContent);\n console.error(`Updated TODO.md: ${todoPath}`);\n}\n\n/**\n * Add a checkpoint entry to TODO.md (without replacing tasks)\n * Ensures only ONE timestamp line at the end\n * Works regardless of TODO.md structure \u2014 appends if no known section found\n */\nexport function addTodoCheckpoint(cwd: string, checkpoint: string): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove ALL existing timestamp lines and trailing separators\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n\n const checkpointText = `\\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\\n\\n`;\n\n // Try to insert before Backlog section\n const backlogIndex = content.indexOf('## Backlog');\n if (backlogIndex !== -1) {\n content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);\n } else {\n // No Backlog section \u2014 try before Continue section, or just append\n const continueIndex = content.indexOf('## Continue');\n if (continueIndex !== -1) {\n // Insert after the Continue section (find the next ## or ---)\n const afterContinue = content.indexOf('\\n---', continueIndex);\n if (afterContinue !== -1) {\n const insertAt = afterContinue + 4; // after \\n---\n content = content.substring(0, insertAt) + '\\n' + checkpointText + content.substring(insertAt);\n } else {\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n } else {\n // No known section \u2014 just append before the end\n content = content.trimEnd() + '\\n' + checkpointText;\n }\n }\n\n // Add exactly ONE timestamp at the end\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${new Date().toISOString()}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error(`Checkpoint added to TODO.md`);\n}\n\n/**\n * Update the ## Continue section at the top of TODO.md.\n * This mirrors \"pause session\" behavior \u2014 gives the next session a starting point.\n * Replaces any existing ## Continue section.\n */\nexport function updateTodoContinue(\n cwd: string,\n noteFilename: string,\n state: string | null,\n tokenDisplay: string\n): void {\n const todoPath = ensureTodoMd(cwd);\n let content = readFileSync(todoPath, 'utf-8');\n\n // Remove existing ## Continue section (from ## Continue to the first standalone --- line)\n content = content.replace(/## Continue\\n[\\s\\S]*?\\n---\\n+/, '');\n\n const now = new Date().toISOString();\n const stateLines = state\n ? state.split('\\n').filter(l => l.trim()).slice(0, 10).map(l => `> ${l}`).join('\\n')\n : `> Check the latest session note for details.`;\n\n const continueSection = `## Continue\n\n> **Last session:** ${noteFilename.replace('.md', '')}\n> **Paused at:** ${now}\n>\n${stateLines}\n\n---\n\n`;\n\n // Remove leading whitespace from content\n content = content.replace(/^\\s+/, '');\n\n // If content starts with # title, insert after it\n const titleMatch = content.match(/^(# [^\\n]+\\n+)/);\n if (titleMatch) {\n content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);\n } else {\n content = continueSection + content;\n }\n\n // Clean up trailing timestamps and add fresh one\n content = content.replace(/(\\n---\\s*)*(\\n\\*Last updated:.*\\*\\s*)+$/g, '');\n content = content.trimEnd() + `\\n\\n---\\n\\n*Last updated: ${now}*\\n`;\n\n writeFileSync(todoPath, content);\n console.error('TODO.md ## Continue section updated');\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
|
|
5
|
-
"mappings": ";;;AAYA,SAAS,iBAAAA,gBAAe,cAAAC,aAAY,gBAAAC,eAAc,aAAAC,kBAAiB;;;
|
|
6
|
-
"names": ["writeFileSync", "existsSync", "readFileSync", "mkdirSync", "existsSync", "
|
|
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 .filter(f => f.endsWith('.md'))\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"],
|
|
5
|
+
"mappings": "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",
|
|
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/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Database, Database as Database$1, Database as Database$2 } from "better-sqlite3";
|
|
2
2
|
|
|
3
3
|
//#region src/registry/schema.d.ts
|
|
4
|
-
declare const SCHEMA_VERSION =
|
|
5
|
-
declare const CREATE_TABLES_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n slug TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n root_path TEXT NOT NULL UNIQUE,\n encoded_dir TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL DEFAULT 'local'\n CHECK(type IN ('local','central','obsidian-linked','external')),\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('active','archived','migrating')),\n parent_id INTEGER,\n obsidian_link TEXT,\n claude_notes_dir TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n FOREIGN KEY (parent_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n number INTEGER NOT NULL,\n date TEXT NOT NULL,\n slug TEXT NOT NULL,\n title TEXT NOT NULL,\n filename TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'open'\n CHECK(status IN ('open','completed','compacted')),\n claude_session_id TEXT,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n closed_at INTEGER,\n UNIQUE (project_id, number),\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS project_tags (\n project_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (project_id, tag_id),\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS session_tags (\n session_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (session_id, tag_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS aliases (\n alias TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS compaction_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n session_id INTEGER,\n trigger TEXT NOT NULL\n CHECK(trigger IN ('precompact','manual','end-session')),\n files_written TEXT NOT NULL,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);\nCREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);\n";
|
|
4
|
+
declare const SCHEMA_VERSION = 4;
|
|
5
|
+
declare const CREATE_TABLES_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n slug TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n root_path TEXT NOT NULL UNIQUE,\n encoded_dir TEXT NOT NULL UNIQUE,\n type TEXT NOT NULL DEFAULT 'local'\n CHECK(type IN ('local','central','obsidian-linked','external')),\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('active','archived','migrating')),\n parent_id INTEGER,\n obsidian_link TEXT,\n claude_notes_dir TEXT,\n session_config TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n archived_at INTEGER,\n FOREIGN KEY (parent_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n number INTEGER NOT NULL,\n date TEXT NOT NULL,\n slug TEXT NOT NULL,\n title TEXT NOT NULL,\n filename TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'open'\n CHECK(status IN ('open','completed','compacted')),\n claude_session_id TEXT,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n closed_at INTEGER,\n UNIQUE (project_id, number),\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE\n);\n\nCREATE TABLE IF NOT EXISTS project_tags (\n project_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (project_id, tag_id),\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS session_tags (\n session_id INTEGER NOT NULL,\n tag_id INTEGER NOT NULL,\n PRIMARY KEY (session_id, tag_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (tag_id) REFERENCES tags(id)\n);\n\nCREATE TABLE IF NOT EXISTS aliases (\n alias TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS compaction_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL,\n session_id INTEGER,\n trigger TEXT NOT NULL\n CHECK(trigger IN ('precompact','manual','end-session')),\n files_written TEXT NOT NULL,\n token_count INTEGER,\n created_at INTEGER NOT NULL,\n FOREIGN KEY (project_id) REFERENCES projects(id),\n FOREIGN KEY (session_id) REFERENCES sessions(id)\n);\n\nCREATE TABLE IF NOT EXISTS links (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id INTEGER NOT NULL,\n target_project_id INTEGER NOT NULL,\n link_type TEXT NOT NULL DEFAULT 'related'\n CHECK(link_type IN ('related','follow-up','reference')),\n created_at INTEGER NOT NULL,\n UNIQUE (session_id, target_project_id),\n FOREIGN KEY (session_id) REFERENCES sessions(id),\n FOREIGN KEY (target_project_id) REFERENCES projects(id)\n);\n\nCREATE TABLE IF NOT EXISTS schema_version (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n\n-- Indexes\nCREATE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug);\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);\nCREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);\nCREATE INDEX IF NOT EXISTS idx_sessions_claude ON sessions(claude_session_id);\nCREATE INDEX IF NOT EXISTS idx_pc_project ON project_tags(project_id);\n";
|
|
6
6
|
/**
|
|
7
7
|
* Run the full DDL against an open database connection.
|
|
8
8
|
*
|
|
@@ -144,7 +144,7 @@ declare function readPaiMarker(projectRoot: string): {
|
|
|
144
144
|
declare function discoverPaiMarkers(searchDirs: string[]): PaiMarker[];
|
|
145
145
|
//#endregion
|
|
146
146
|
//#region src/memory/schema.d.ts
|
|
147
|
-
declare const FEDERATION_SCHEMA_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n
|
|
147
|
+
declare const FEDERATION_SCHEMA_SQL = "\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n";
|
|
148
148
|
/**
|
|
149
149
|
* Apply the full federation schema to an open database.
|
|
150
150
|
*
|
|
@@ -207,12 +207,13 @@ declare function estimateTokens(text: string): number;
|
|
|
207
207
|
*/
|
|
208
208
|
declare function chunkMarkdown(content: string, opts?: ChunkOptions): Chunk[];
|
|
209
209
|
//#endregion
|
|
210
|
-
//#region src/memory/indexer.d.ts
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
210
|
+
//#region src/memory/indexer/helpers.d.ts
|
|
211
|
+
/**
|
|
212
|
+
* Shared helpers for the PAI memory indexers.
|
|
213
|
+
*
|
|
214
|
+
* Contains utilities used by both the sync (SQLite) and async (StorageBackend)
|
|
215
|
+
* indexer paths: hashing, chunk ID generation, directory walking, and path guards.
|
|
216
|
+
*/
|
|
216
217
|
/**
|
|
217
218
|
* Classify a relative file path into one of the four memory tiers.
|
|
218
219
|
*
|
|
@@ -223,12 +224,34 @@ interface IndexResult {
|
|
|
223
224
|
* - anything in Notes/ → 'session'
|
|
224
225
|
*/
|
|
225
226
|
declare function detectTier(relativePath: string): "evergreen" | "daily" | "topic" | "session";
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/memory/indexer/types.d.ts
|
|
229
|
+
/**
|
|
230
|
+
* Shared types for the PAI memory indexer.
|
|
231
|
+
*/
|
|
232
|
+
interface IndexResult {
|
|
233
|
+
filesProcessed: number;
|
|
234
|
+
chunksCreated: number;
|
|
235
|
+
filesSkipped: number;
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/memory/indexer/sync.d.ts
|
|
226
239
|
/**
|
|
227
240
|
* Index a single file into the federation database.
|
|
228
241
|
*
|
|
229
242
|
* @returns true if the file was re-indexed (changed or new), false if skipped.
|
|
230
243
|
*/
|
|
231
244
|
declare function indexFile(db: Database, projectId: number, rootPath: string, relativePath: string, source: string, tier: string): boolean;
|
|
245
|
+
/**
|
|
246
|
+
* Index all memory, Notes, and content files for a single registered project.
|
|
247
|
+
*
|
|
248
|
+
* Scans:
|
|
249
|
+
* - {rootPath}/MEMORY.md → source='memory', tier='evergreen'
|
|
250
|
+
* - {rootPath}/memory/ → source='memory', tier from detectTier()
|
|
251
|
+
* - {rootPath}/Notes/ → source='notes', tier='session'
|
|
252
|
+
* - {rootPath}/**\/*.md → source='content', tier='topic' (all other .md files, recursive)
|
|
253
|
+
* - {claudeNotesDir}/ → source='notes', tier='session' (if set and different)
|
|
254
|
+
*/
|
|
232
255
|
declare function indexProject(db: Database, projectId: number, rootPath: string, claudeNotesDir?: string | null): Promise<IndexResult>;
|
|
233
256
|
/**
|
|
234
257
|
* Index all active projects registered in the registry DB.
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/registry/schema.ts","../src/registry/db.ts","../src/registry/migrate.ts","../src/registry/pai-marker.ts","../src/memory/schema.ts","../src/memory/db.ts","../src/memory/chunker.ts","../src/memory/indexer.ts","../src/memory/search.ts","../src/memory/reranker.ts"],"mappings":";;;cAgBa,cAAA;AAAA,cAEA,iBAAA;;;;;;ACYb;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/registry/schema.ts","../src/registry/db.ts","../src/registry/migrate.ts","../src/registry/pai-marker.ts","../src/memory/schema.ts","../src/memory/db.ts","../src/memory/chunker.ts","../src/memory/indexer/helpers.ts","../src/memory/indexer/types.ts","../src/memory/indexer/sync.ts","../src/memory/search.ts","../src/memory/reranker.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { a as initializeSchema, i as SCHEMA_VERSION, n as openRegistry, r as CREATE_TABLES_SQL } from "./db-
|
|
1
|
+
import { a as initializeSchema, i as SCHEMA_VERSION, n as openRegistry, r as CREATE_TABLES_SQL } from "./db-BtuN768f.mjs";
|
|
2
2
|
import "./utils-QSfKagcj.mjs";
|
|
3
3
|
import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, r as migrateFromJson } from "./migrate-jokLenje.mjs";
|
|
4
4
|
import { n as ensurePaiMarker, r as readPaiMarker, t as discoverPaiMarkers } from "./pai-marker-CXQPX2P6.mjs";
|
|
5
|
-
import { i as initializeFederationSchema, n as openFederation, r as FEDERATION_SCHEMA_SQL } from "./db-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { i as initializeFederationSchema, n as openFederation, r as FEDERATION_SCHEMA_SQL } from "./db-DdUperSl.mjs";
|
|
6
|
+
import { l as chunkMarkdown, r as detectTier, u as estimateTokens } from "./helpers-BEST-4Gx.mjs";
|
|
7
|
+
import { i as indexProject, n as indexAll, r as indexFile } from "./sync-BOsnEj2-.mjs";
|
|
8
8
|
import "./embeddings-DGRAPAYb.mjs";
|
|
9
|
-
import { n as populateSlugs, r as searchMemory, t as buildFtsQuery } from "./search-
|
|
10
|
-
import { n as rerankResults, t as configureRerankerModel } from "./reranker-
|
|
11
|
-
import "./config-Cf92lGX_.mjs";
|
|
12
|
-
import "./tools-DV_lsiCc.mjs";
|
|
13
|
-
import "./mcp/index.mjs";
|
|
9
|
+
import { n as populateSlugs, r as searchMemory, t as buildFtsQuery } from "./search-DC1qhkKn.mjs";
|
|
10
|
+
import { n as rerankResults, t as configureRerankerModel } from "./reranker-CMNZcfVx.mjs";
|
|
14
11
|
|
|
15
12
|
export { CREATE_TABLES_SQL, FEDERATION_SCHEMA_SQL, SCHEMA_VERSION, buildFtsQuery, chunkMarkdown, configureRerankerModel, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, rerankResults, searchMemory, slugify };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -1,132 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { basename, join, normalize, relative } from "node:path";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { a as parseSessionTitleChunk, c as yieldToEventLoop, f as sha256File, i as isPathTooBroadForContentScan, l as chunkMarkdown, n as chunkId, o as walkContentFiles, r as detectTier, s as walkMdFiles, t as INDEX_YIELD_EVERY } from "./helpers-BEST-4Gx.mjs";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, join, relative } from "node:path";
|
|
7
4
|
|
|
8
|
-
//#region src/memory/indexer
|
|
5
|
+
//#region src/memory/indexer/async.ts
|
|
9
6
|
/**
|
|
10
|
-
* Backend-aware indexer for PAI federation memory.
|
|
7
|
+
* Backend-aware async indexer for PAI federation memory.
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Provides the same functionality as sync.ts but writes through the
|
|
10
|
+
* StorageBackend interface instead of directly to better-sqlite3.
|
|
14
11
|
* Used when the daemon is configured with the Postgres backend.
|
|
15
12
|
*
|
|
16
|
-
* The SQLite path still uses
|
|
13
|
+
* The SQLite path still uses sync.ts directly (which is faster for SQLite
|
|
17
14
|
* due to synchronous transactions).
|
|
18
15
|
*/
|
|
19
|
-
const SESSION_TITLE_RE = /^(\d{4})\s*-\s*(\d{4}-\d{2}-\d{2})\s*-\s*(.+)\.md$/;
|
|
20
|
-
/**
|
|
21
|
-
* Parse a session title from a Notes filename.
|
|
22
|
-
* Format: "NNNN - YYYY-MM-DD - Descriptive Title.md"
|
|
23
|
-
* Returns a synthetic chunk text like "Session #0086 2026-02-23: Pai Daemon Background Service"
|
|
24
|
-
* or null if the filename doesn't match the expected pattern.
|
|
25
|
-
*/
|
|
26
|
-
function parseSessionTitleChunk(fileName) {
|
|
27
|
-
const m = SESSION_TITLE_RE.exec(fileName);
|
|
28
|
-
if (!m) return null;
|
|
29
|
-
const [, num, date, title] = m;
|
|
30
|
-
return `Session #${num} ${date}: ${title}`;
|
|
31
|
-
}
|
|
32
|
-
const MAX_FILES_PER_PROJECT = 5e3;
|
|
33
|
-
const MAX_WALK_DEPTH = 6;
|
|
34
|
-
const INDEX_YIELD_EVERY = 10;
|
|
35
|
-
/**
|
|
36
|
-
* Directories to ALWAYS skip, at any depth, during any directory walk.
|
|
37
|
-
* These are build artifacts, dependency trees, and VCS internals that
|
|
38
|
-
* should never be indexed regardless of where they appear in the tree.
|
|
39
|
-
*/
|
|
40
|
-
const ALWAYS_SKIP_DIRS = new Set([
|
|
41
|
-
".git",
|
|
42
|
-
"node_modules",
|
|
43
|
-
"vendor",
|
|
44
|
-
"Pods",
|
|
45
|
-
"dist",
|
|
46
|
-
"build",
|
|
47
|
-
"out",
|
|
48
|
-
"DerivedData",
|
|
49
|
-
".next",
|
|
50
|
-
".venv",
|
|
51
|
-
"venv",
|
|
52
|
-
"__pycache__",
|
|
53
|
-
".cache",
|
|
54
|
-
".bun"
|
|
55
|
-
]);
|
|
56
|
-
const ROOT_SCAN_SKIP_DIRS = new Set([
|
|
57
|
-
"memory",
|
|
58
|
-
"Notes",
|
|
59
|
-
".claude",
|
|
60
|
-
".DS_Store",
|
|
61
|
-
...ALWAYS_SKIP_DIRS
|
|
62
|
-
]);
|
|
63
|
-
const CONTENT_SCAN_SKIP_DIRS = new Set([
|
|
64
|
-
"Library",
|
|
65
|
-
"Applications",
|
|
66
|
-
"Music",
|
|
67
|
-
"Movies",
|
|
68
|
-
"Pictures",
|
|
69
|
-
"Desktop",
|
|
70
|
-
"Downloads",
|
|
71
|
-
"Public",
|
|
72
|
-
"coverage",
|
|
73
|
-
...ALWAYS_SKIP_DIRS
|
|
74
|
-
]);
|
|
75
|
-
function sha256File(content) {
|
|
76
|
-
return createHash("sha256").update(content).digest("hex");
|
|
77
|
-
}
|
|
78
|
-
function chunkId(projectId, path, chunkIndex, startLine, endLine) {
|
|
79
|
-
return createHash("sha256").update(`${projectId}:${path}:${chunkIndex}:${startLine}:${endLine}`).digest("hex");
|
|
80
|
-
}
|
|
81
|
-
function walkMdFiles(dir, acc, cap = MAX_FILES_PER_PROJECT, depth = 0) {
|
|
82
|
-
const results = acc ?? [];
|
|
83
|
-
if (!existsSync(dir)) return results;
|
|
84
|
-
if (results.length >= cap) return results;
|
|
85
|
-
if (depth > MAX_WALK_DEPTH) return results;
|
|
86
|
-
try {
|
|
87
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
88
|
-
if (results.length >= cap) break;
|
|
89
|
-
if (entry.isSymbolicLink()) continue;
|
|
90
|
-
if (ALWAYS_SKIP_DIRS.has(entry.name)) continue;
|
|
91
|
-
const full = join(dir, entry.name);
|
|
92
|
-
if (entry.isDirectory()) walkMdFiles(full, results, cap, depth + 1);
|
|
93
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) results.push(full);
|
|
94
|
-
}
|
|
95
|
-
} catch {}
|
|
96
|
-
return results;
|
|
97
|
-
}
|
|
98
|
-
function walkContentFiles(rootPath) {
|
|
99
|
-
if (!existsSync(rootPath)) return [];
|
|
100
|
-
const results = [];
|
|
101
|
-
try {
|
|
102
|
-
for (const entry of readdirSync(rootPath, { withFileTypes: true })) {
|
|
103
|
-
if (results.length >= MAX_FILES_PER_PROJECT) break;
|
|
104
|
-
if (entry.isSymbolicLink()) continue;
|
|
105
|
-
if (ROOT_SCAN_SKIP_DIRS.has(entry.name)) continue;
|
|
106
|
-
if (CONTENT_SCAN_SKIP_DIRS.has(entry.name)) continue;
|
|
107
|
-
const full = join(rootPath, entry.name);
|
|
108
|
-
if (entry.isDirectory()) walkMdFiles(full, results, MAX_FILES_PER_PROJECT);
|
|
109
|
-
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
110
|
-
if (entry.name !== "MEMORY.md") results.push(full);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch {}
|
|
114
|
-
return results;
|
|
115
|
-
}
|
|
116
|
-
function isPathTooBroadForContentScan(rootPath) {
|
|
117
|
-
const normalized = normalize(rootPath);
|
|
118
|
-
const home = homedir();
|
|
119
|
-
if (home.startsWith(normalized) || normalized === "/") return true;
|
|
120
|
-
if (normalized.startsWith(home)) {
|
|
121
|
-
const rel = normalized.slice(home.length).replace(/^\//, "");
|
|
122
|
-
if ((rel ? rel.split("/").length : 0) === 0) return true;
|
|
123
|
-
}
|
|
124
|
-
if (existsSync(join(normalized, ".git"))) return true;
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
function yieldToEventLoop() {
|
|
128
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
129
|
-
}
|
|
130
16
|
/**
|
|
131
17
|
* Index a single file through the StorageBackend interface.
|
|
132
18
|
* Returns true if the file was re-indexed (changed or new), false if skipped.
|
|
@@ -317,7 +203,7 @@ async function indexProjectWithBackend(backend, projectId, rootPath, claudeNotes
|
|
|
317
203
|
return result;
|
|
318
204
|
}
|
|
319
205
|
const EMBED_BATCH_SIZE = 50;
|
|
320
|
-
const EMBED_YIELD_EVERY =
|
|
206
|
+
const EMBED_YIELD_EVERY = 1;
|
|
321
207
|
/**
|
|
322
208
|
* Generate and store embeddings for all unembedded chunks via the StorageBackend.
|
|
323
209
|
*
|
|
@@ -330,12 +216,26 @@ const EMBED_YIELD_EVERY = 10;
|
|
|
330
216
|
*
|
|
331
217
|
* Returns the number of newly embedded chunks.
|
|
332
218
|
*/
|
|
333
|
-
async function embedChunksWithBackend(backend, shouldStop) {
|
|
219
|
+
async function embedChunksWithBackend(backend, shouldStop, projectNames) {
|
|
334
220
|
const { generateEmbedding, serializeEmbedding } = await import("./embeddings-DGRAPAYb.mjs").then((n) => n.i);
|
|
335
221
|
const rows = await backend.getUnembeddedChunkIds();
|
|
336
222
|
if (rows.length === 0) return 0;
|
|
337
223
|
const total = rows.length;
|
|
338
224
|
let embedded = 0;
|
|
225
|
+
const projectChunkCounts = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
const entry = projectChunkCounts.get(row.project_id);
|
|
228
|
+
if (entry) entry.count++;
|
|
229
|
+
else projectChunkCounts.set(row.project_id, {
|
|
230
|
+
count: 1,
|
|
231
|
+
samplePath: row.path
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const pName = (pid) => projectNames?.get(pid) ?? `project ${pid}`;
|
|
235
|
+
const projectSummary = Array.from(projectChunkCounts.entries()).map(([pid, { count, samplePath }]) => ` ${pName(pid)}: ${count} chunks (e.g. ${samplePath})`).join("\n");
|
|
236
|
+
process.stderr.write(`[pai-daemon] Embed pass: ${total} unembedded chunks across ${projectChunkCounts.size} project(s)\n${projectSummary}\n`);
|
|
237
|
+
let currentProjectId = -1;
|
|
238
|
+
let projectEmbedded = 0;
|
|
339
239
|
for (let i = 0; i < rows.length; i += EMBED_BATCH_SIZE) {
|
|
340
240
|
if (shouldStop?.()) {
|
|
341
241
|
process.stderr.write(`[pai-daemon] Embed pass cancelled after ${embedded}/${total} chunks (shutdown requested)\n`);
|
|
@@ -343,14 +243,24 @@ async function embedChunksWithBackend(backend, shouldStop) {
|
|
|
343
243
|
}
|
|
344
244
|
const batch = rows.slice(i, i + EMBED_BATCH_SIZE);
|
|
345
245
|
for (let j = 0; j < batch.length; j++) {
|
|
346
|
-
const { id, text } = batch[j];
|
|
246
|
+
const { id, text, project_id, path } = batch[j];
|
|
247
|
+
if (project_id !== currentProjectId) {
|
|
248
|
+
if (currentProjectId !== -1) process.stderr.write(`[pai-daemon] Finished ${pName(currentProjectId)}: ${projectEmbedded} chunks embedded\n`);
|
|
249
|
+
const info = projectChunkCounts.get(project_id);
|
|
250
|
+
process.stderr.write(`[pai-daemon] Embedding ${pName(project_id)} (${info?.count ?? "?"} chunks, starting at ${path})\n`);
|
|
251
|
+
currentProjectId = project_id;
|
|
252
|
+
projectEmbedded = 0;
|
|
253
|
+
}
|
|
347
254
|
if ((embedded + j) % EMBED_YIELD_EVERY === 0) await yieldToEventLoop();
|
|
348
255
|
const blob = serializeEmbedding(await generateEmbedding(text));
|
|
349
256
|
await backend.updateEmbedding(id, blob);
|
|
257
|
+
projectEmbedded++;
|
|
350
258
|
}
|
|
351
259
|
embedded += batch.length;
|
|
352
|
-
|
|
260
|
+
const lastChunk = batch[batch.length - 1];
|
|
261
|
+
process.stderr.write(`[pai-daemon] Embedded ${embedded}/${total} chunks (${pName(lastChunk.project_id)}: ${lastChunk.path})\n`);
|
|
353
262
|
}
|
|
263
|
+
if (currentProjectId !== -1) process.stderr.write(`[pai-daemon] Finished ${pName(currentProjectId)}: ${projectEmbedded} chunks embedded\n`);
|
|
354
264
|
return embedded;
|
|
355
265
|
}
|
|
356
266
|
async function indexAllWithBackend(backend, registryDb) {
|
|
@@ -375,4 +285,4 @@ async function indexAllWithBackend(backend, registryDb) {
|
|
|
375
285
|
|
|
376
286
|
//#endregion
|
|
377
287
|
export { embedChunksWithBackend, indexAllWithBackend };
|
|
378
|
-
//# sourceMappingURL=indexer-backend-
|
|
288
|
+
//# sourceMappingURL=indexer-backend-jcJFsmB4.mjs.map
|