@tekmidian/pai 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +1 -1
- package/dist/daemon/index.mjs +1 -1
- package/dist/{daemon-DJoesjez.mjs → daemon-D8ZxcFhU.mjs} +615 -5
- package/dist/daemon-D8ZxcFhU.mjs.map +1 -0
- package/dist/hooks/context-compression-hook.mjs +140 -22
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +74 -4
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +70 -1
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
- package/dist/skills/Reconstruct/SKILL.md +232 -0
- package/package.json +1 -1
- package/plugins/productivity/plugin.json +1 -1
- package/plugins/productivity/skills/Reconstruct/SKILL.md +232 -0
- package/src/hooks/ts/lib/project-utils/index.ts +1 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +24 -2
- package/src/hooks/ts/lib/project-utils.ts +1 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +159 -37
- package/src/hooks/ts/session-start/load-project-context.ts +101 -3
- package/src/hooks/ts/stop/stop-hook.ts +66 -0
- package/dist/daemon-DJoesjez.mjs.map +0 -1
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/hooks/ts/session-start/load-project-context.ts
|
|
4
4
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync } from "fs";
|
|
5
|
-
import { join as join5, basename as basename3, dirname } from "path";
|
|
5
|
+
import { join as join5, basename as basename3, dirname, resolve as resolve2 } from "path";
|
|
6
|
+
import { homedir as homedir3 } from "os";
|
|
6
7
|
import { execSync } from "child_process";
|
|
7
8
|
|
|
8
9
|
// src/hooks/ts/lib/project-utils/paths.ts
|
|
@@ -177,7 +178,7 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
177
178
|
console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);
|
|
178
179
|
}
|
|
179
180
|
if (attempt < retries) {
|
|
180
|
-
await new Promise((
|
|
181
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
console.error("ntfy.sh notification failed after all retries");
|
|
@@ -298,6 +299,42 @@ function getRoutedNotesPath() {
|
|
|
298
299
|
}
|
|
299
300
|
return null;
|
|
300
301
|
}
|
|
302
|
+
var PROJECT_SIGNALS = [
|
|
303
|
+
".git",
|
|
304
|
+
"package.json",
|
|
305
|
+
"pubspec.yaml",
|
|
306
|
+
"Cargo.toml",
|
|
307
|
+
"go.mod",
|
|
308
|
+
"pyproject.toml",
|
|
309
|
+
"setup.py",
|
|
310
|
+
"build.gradle",
|
|
311
|
+
"pom.xml",
|
|
312
|
+
"composer.json",
|
|
313
|
+
"Gemfile",
|
|
314
|
+
"Makefile",
|
|
315
|
+
"CMakeLists.txt",
|
|
316
|
+
"tsconfig.json",
|
|
317
|
+
"CLAUDE.md",
|
|
318
|
+
join5("Notes", "PAI.md")
|
|
319
|
+
];
|
|
320
|
+
function hasProjectSignals(dir) {
|
|
321
|
+
for (const signal of PROJECT_SIGNALS) {
|
|
322
|
+
if (existsSync5(join5(dir, signal))) return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
function isGuardedPath(dir) {
|
|
327
|
+
const home = homedir3();
|
|
328
|
+
const resolved = resolve2(dir);
|
|
329
|
+
if (resolved === home) return true;
|
|
330
|
+
const parts = resolved.split("/").filter(Boolean);
|
|
331
|
+
if (parts.length < 3) return true;
|
|
332
|
+
const forbidden = ["/tmp", "/var", "/private/tmp", "/private/var/folders"];
|
|
333
|
+
for (const prefix of forbidden) {
|
|
334
|
+
if (resolved === prefix || resolved.startsWith(prefix + "/")) return true;
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
301
338
|
async function main() {
|
|
302
339
|
console.error("\nload-project-context.ts starting...");
|
|
303
340
|
if (isProbeSession()) {
|
|
@@ -470,9 +507,42 @@ Using existing session note: ${basename3(activeNotePath)}`);
|
|
|
470
507
|
if (raw) {
|
|
471
508
|
const detected = JSON.parse(raw);
|
|
472
509
|
if (detected.error === "no_match") {
|
|
473
|
-
|
|
510
|
+
let autoRegistered = false;
|
|
511
|
+
if (!isGuardedPath(cwd) && hasProjectSignals(cwd)) {
|
|
512
|
+
try {
|
|
513
|
+
execFileSync(paiBin, ["project", "add", cwd], {
|
|
514
|
+
encoding: "utf-8",
|
|
515
|
+
env: process.env
|
|
516
|
+
});
|
|
517
|
+
console.error(`PAI auto-registered project at: ${cwd}`);
|
|
518
|
+
try {
|
|
519
|
+
const raw2 = execFileSync(paiBin, ["project", "detect", "--json", cwd], {
|
|
520
|
+
encoding: "utf-8",
|
|
521
|
+
env: process.env
|
|
522
|
+
}).trim();
|
|
523
|
+
if (raw2) {
|
|
524
|
+
const detected2 = JSON.parse(raw2);
|
|
525
|
+
if (detected2.slug) {
|
|
526
|
+
const name2 = detected2.display_name || detected2.slug;
|
|
527
|
+
console.error(`PAI auto-registered: "${detected2.slug}" (${detected2.match_type})`);
|
|
528
|
+
paiProjectBlock = `PAI Project Registry: ${name2} (slug: ${detected2.slug}) [AUTO-REGISTERED]
|
|
529
|
+
Match: ${detected2.match_type ?? "exact"} | Sessions: 0`;
|
|
530
|
+
autoRegistered = true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch (detectErr) {
|
|
534
|
+
console.error("PAI auto-registration: project added but re-detect failed:", detectErr);
|
|
535
|
+
autoRegistered = true;
|
|
536
|
+
}
|
|
537
|
+
} catch (addErr) {
|
|
538
|
+
console.error("PAI auto-registration failed (project add):", addErr);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!autoRegistered) {
|
|
542
|
+
paiProjectBlock = `PAI Project Registry: No registered project matches this directory.
|
|
474
543
|
Run "pai project add ." to register this project, or use /route to tag the session.`;
|
|
475
|
-
|
|
544
|
+
console.error("PAI detect: no match for", cwd);
|
|
545
|
+
}
|
|
476
546
|
} else if (detected.slug) {
|
|
477
547
|
const name = detected.display_name || detected.slug;
|
|
478
548
|
const nameSlug = ` (slug: ${detected.slug})`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/ts/session-start/load-project-context.ts", "../../src/hooks/ts/lib/project-utils/paths.ts", "../../src/hooks/ts/lib/pai-paths.ts", "../../src/hooks/ts/lib/project-utils/notify.ts", "../../src/hooks/ts/lib/project-utils/session-notes.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\n/**\n * load-project-context.ts\n *\n * SessionStart hook that sets up project context:\n * - Checks for CLAUDE.md in various locations (Claude Code handles loading)\n * - Sets up Notes/ directory in ~/.claude/projects/{encoded-path}/\n * - Ensures TODO.md exists\n * - Sends ntfy.sh notification (mandatory)\n * - Displays session continuity info (like session-init.sh)\n *\n * This hook complements Claude Code's native CLAUDE.md loading by:\n * - Setting up the Notes infrastructure\n * - Showing the latest session note for continuity\n * - Sending ntfy.sh notifications\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from 'fs';\nimport { join, basename, dirname } from 'path';\nimport { execSync } from 'child_process';\nimport {\n PAI_DIR,\n findNotesDir,\n getProjectDir,\n getCurrentNotePath,\n createSessionNote,\n findTodoPath,\n findAllClaudeMdPaths,\n sendNtfyNotification,\n isProbeSession\n} from '../lib/project-utils';\n\n/**\n * Find the pai CLI binary path dynamically.\n * Tries `which pai` first, then common fallback locations.\n */\nfunction findPaiBinary(): string {\n try {\n return execSync('which pai', { encoding: 'utf-8' }).trim();\n } catch {\n // Fallback locations in order of preference\n const fallbacks = [\n '/usr/local/bin/pai',\n '/opt/homebrew/bin/pai',\n `${process.env.HOME}/.local/bin/pai`,\n ];\n for (const p of fallbacks) {\n if (existsSync(p)) return p;\n }\n }\n return 'pai'; // Last resort: rely on PATH at runtime\n}\n\n/**\n * Check session-routing.json for an active route.\n * Returns the routed Notes path if set, or null to use default behavior.\n */\nfunction getRoutedNotesPath(): string | null {\n const routingFile = join(PAI_DIR, 'session-routing.json');\n if (!existsSync(routingFile)) return null;\n\n try {\n const routing = JSON.parse(readFileSync(routingFile, 'utf-8'));\n const active = routing?.active_session;\n if (active?.notes_path) {\n return active.notes_path;\n }\n } catch {\n // Ignore parse errors\n }\n return null;\n}\n\ninterface HookInput {\n session_id: string;\n cwd: string;\n hook_event_name: string;\n}\n\nasync function main() {\n console.error('\\nload-project-context.ts starting...');\n\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n console.error('Probe session detected - skipping project context loading');\n process.exit(0);\n }\n\n // Read hook input from stdin\n let hookInput: HookInput | null = null;\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (input.trim()) {\n hookInput = JSON.parse(input);\n }\n } catch (error) {\n console.error('Could not parse hook input, using process.cwd()');\n }\n\n // Get current working directory\n const cwd = hookInput?.cwd || process.cwd();\n\n // Determine meaningful project name\n // If cwd is a Notes directory, use parent directory name instead\n let projectName = basename(cwd);\n if (projectName.toLowerCase() === 'notes') {\n projectName = basename(dirname(cwd));\n }\n\n console.error(`Working directory: ${cwd}`);\n console.error(`Project: ${projectName}`);\n\n // Check if this is a subagent session - skip for subagents\n const isSubagent = process.env.CLAUDE_AGENT_TYPE !== undefined ||\n (process.env.CLAUDE_PROJECT_DIR || '').includes('/.claude/agents/');\n\n if (isSubagent) {\n console.error('Subagent session - skipping project context setup');\n process.exit(0);\n }\n\n // 1. Find and READ all CLAUDE.md files - inject them into context\n // This ensures Claude actually processes the instructions, not just sees them in headers\n const claudeMdPaths = findAllClaudeMdPaths(cwd);\n const claudeMdContents: { path: string; content: string }[] = [];\n\n if (claudeMdPaths.length > 0) {\n console.error(`Found ${claudeMdPaths.length} CLAUDE.md file(s):`);\n for (const path of claudeMdPaths) {\n console.error(` - ${path}`);\n try {\n const content = readFileSync(path, 'utf-8');\n claudeMdContents.push({ path, content });\n console.error(` Read ${content.length} chars`);\n } catch (error) {\n console.error(` Could not read: ${error}`);\n }\n }\n } else {\n console.error('No CLAUDE.md found in project');\n console.error(' Consider creating one at ./CLAUDE.md or ./.claude/CLAUDE.md');\n }\n\n // 2. Find or create Notes directory\n // Priority:\n // 1. Active session routing (pai route <project>) \u2192 routed Obsidian path\n // 2. Local Notes/ in cwd \u2192 use it (git-trackable, e.g. symlink to Obsidian)\n // 3. Central ~/.claude/projects/.../Notes/ \u2192 fallback\n const routedPath = getRoutedNotesPath();\n let notesDir: string;\n\n if (routedPath) {\n // Routing is active - use the configured Obsidian Notes path\n const { mkdirSync } = await import('fs');\n if (!existsSync(routedPath)) {\n mkdirSync(routedPath, { recursive: true });\n console.error(`Created routed Notes: ${routedPath}`);\n } else {\n console.error(`Notes directory: ${routedPath} (routed via pai route)`);\n }\n notesDir = routedPath;\n } else {\n const notesInfo = findNotesDir(cwd);\n\n if (notesInfo.isLocal) {\n notesDir = notesInfo.path;\n console.error(`Notes directory: ${notesDir} (local)`);\n } else {\n // Create central Notes directory\n if (!existsSync(notesInfo.path)) {\n const { mkdirSync } = await import('fs');\n mkdirSync(notesInfo.path, { recursive: true });\n console.error(`Created central Notes: ${notesInfo.path}`);\n } else {\n console.error(`Notes directory: ${notesInfo.path} (central)`);\n }\n notesDir = notesInfo.path;\n }\n }\n\n // 3. Cleanup old .jsonl files from project root (move to sessions/)\n // Keep the newest one for potential resume, move older ones to sessions/\n const projectDir = getProjectDir(cwd);\n if (existsSync(projectDir)) {\n try {\n const files = readdirSync(projectDir);\n const jsonlFiles = files\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({\n name: f,\n path: join(projectDir, f),\n mtime: statSync(join(projectDir, f)).mtime.getTime()\n }))\n .sort((a, b) => b.mtime - a.mtime); // newest first\n\n if (jsonlFiles.length > 1) {\n const { mkdirSync, renameSync } = await import('fs');\n const sessionsDir = join(projectDir, 'sessions');\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n }\n\n // Move all except the newest\n for (let i = 1; i < jsonlFiles.length; i++) {\n const file = jsonlFiles[i];\n const destPath = join(sessionsDir, file.name);\n if (!existsSync(destPath)) {\n renameSync(file.path, destPath);\n console.error(`Moved old session: ${file.name} \u2192 sessions/`);\n }\n }\n }\n } catch (error) {\n console.error(`Could not cleanup old .jsonl files: ${error}`);\n }\n }\n\n // 4. Find or create TODO.md\n const todoPath = findTodoPath(cwd);\n const hasTodo = existsSync(todoPath);\n if (hasTodo) {\n console.error(`TODO.md: ${todoPath}`);\n } else {\n // Create TODO.md in the Notes directory\n const newTodoPath = join(notesDir, 'TODO.md');\n const { writeFileSync } = await import('fs');\n writeFileSync(newTodoPath, `# TODO\\n\\n## Offen\\n\\n- [ ] \\n\\n---\\n\\n*Created: ${new Date().toISOString()}*\\n`);\n console.error(`Created TODO.md: ${newTodoPath}`);\n }\n\n // 5. Check for existing note or create new one\n let activeNotePath: string | null = null;\n\n if (notesDir) { // notesDir is always set now (local or central)\n const currentNotePath = getCurrentNotePath(notesDir);\n\n // Only create a new note if there is truly no note at all.\n // A completed note is still used \u2014 it will be updated or continued.\n // This prevents duplicate notes at month boundaries and on every compaction.\n if (!currentNotePath) {\n // Defensive: ensure projectName is a usable string\n const safeProjectName = (typeof projectName === 'string' && projectName.trim().length > 0)\n ? projectName.trim()\n : 'Untitled Session';\n console.error('\\nNo previous session notes found - creating new one');\n activeNotePath = createSessionNote(notesDir, String(safeProjectName));\n console.error(`Created: ${basename(activeNotePath)}`);\n } else {\n activeNotePath = currentNotePath!;\n console.error(`\\nUsing existing session note: ${basename(activeNotePath)}`);\n // Show preview of current note\n try {\n const content = readFileSync(activeNotePath, 'utf-8');\n const lines = content.split('\\n').slice(0, 12);\n console.error('--- Current Note Preview ---');\n for (const line of lines) {\n console.error(line);\n }\n console.error('--- End Preview ---\\n');\n } catch {\n // Ignore read errors\n }\n }\n }\n\n // 6. Show TODO.md preview\n if (existsSync(todoPath)) {\n try {\n const todoContent = readFileSync(todoPath, 'utf-8');\n const todoLines = todoContent.split('\\n').filter(l => l.includes('[ ]')).slice(0, 5);\n if (todoLines.length > 0) {\n console.error('\\nOpen TODOs:');\n for (const line of todoLines) {\n console.error(` ${line.trim()}`);\n }\n }\n } catch {\n // Ignore read errors\n }\n }\n\n // 7. Send ntfy.sh notification (MANDATORY)\n await sendNtfyNotification(`Session started in ${projectName}`);\n\n // 7.5. Run pai project detect to identify the registered PAI project\n const paiBin = findPaiBinary();\n let paiProjectBlock = '';\n try {\n const { execFileSync } = await import('child_process');\n const raw = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n encoding: 'utf-8',\n env: process.env,\n }).trim();\n\n if (raw) {\n const detected = JSON.parse(raw) as {\n slug?: string;\n display_name?: string;\n root_path?: string;\n match_type?: string;\n relative_path?: string | null;\n session_count?: number;\n status?: string;\n error?: string;\n cwd?: string;\n };\n\n if (detected.error === 'no_match') {\n paiProjectBlock = `PAI Project Registry: No registered project matches this directory.\nRun \"pai project add .\" to register this project, or use /route to tag the session.`;\n console.error('PAI detect: no match for', cwd);\n } else if (detected.slug) {\n const name = detected.display_name || detected.slug;\n const nameSlug = ` (slug: ${detected.slug})`;\n const matchDesc = detected.match_type === 'exact'\n ? 'exact'\n : `parent (+${detected.relative_path ?? ''})`;\n const statusFlag = detected.status && detected.status !== 'active'\n ? ` [${detected.status.toUpperCase()}]`\n : '';\n paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}\nMatch: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== 'active' ? `\\nWARNING: Project status is \"${detected.status}\". Run: pai project health --fix` : ''}`;\n console.error(`PAI detect: matched \"${detected.slug}\" (${detected.match_type})`);\n }\n }\n } catch (e) {\n // Non-fatal \u2014 don't break session start if pai is unavailable\n console.error('pai project detect failed:', e);\n }\n\n // 8. Output system reminder with session info\n const reminder = `\n<system-reminder>\nPROJECT CONTEXT LOADED\n\nProject: ${projectName}\nWorking Directory: ${cwd}\n${notesDir ? `Notes Directory: ${notesDir}${routedPath ? ' (routed via pai route)' : ''}` : 'Notes: disabled (no local Notes/ directory)'}\n${hasTodo ? `TODO: ${todoPath}` : 'TODO: not found'}\n${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(', ')}` : 'No CLAUDE.md found'}\n${activeNotePath ? `Active Note: ${basename(activeNotePath)}` : ''}\n${routedPath ? `\\nNote Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ''}\n${paiProjectBlock ? `\\n${paiProjectBlock}` : ''}\nSession Commands:\n- \"pause session\" \u2192 Save checkpoint, update TODO, exit (no compact)\n- \"end session\" \u2192 Finalize note, commit if needed, start fresh next time\n- \"pai route clear\" \u2192 Clear note routing (in a new session)\n</system-reminder>\n`;\n\n // Output to stdout for Claude to receive\n console.log(reminder);\n\n // 9. INJECT CLAUDE.md contents as system-reminders\n // This ensures Claude actually reads and processes the instructions\n for (const { path, content } of claudeMdContents) {\n const claudeMdReminder = `\n<system-reminder>\nLOCAL CLAUDE.md LOADED (MANDATORY - READ AND FOLLOW)\n\nSource: ${path}\n\n${content}\n\n---\nTHE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.\n</system-reminder>\n`;\n console.log(claudeMdReminder);\n console.error(`Injected CLAUDE.md content from: ${path}`);\n }\n\n console.error('\\nProject context setup complete\\n');\n process.exit(0);\n}\n\nmain().catch(error => {\n console.error('load-project-context.ts error:', error);\n process.exit(0); // Don't block session start\n});\n", "/**\n * Path utilities \u2014 encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory.\n * Returns the number of files moved.\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n try {\n renameSync(sourcePath, destPath);\n if (!silent) console.error(`Moved ${file} \u2192 sessions/`);\n movedCount++;\n } catch (error) {\n if (!silent) console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n\n return movedCount;\n}\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n", "/**\n * Push notification helpers \u2014 WhatsApp-aware with ntfy.sh fallback.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\n/**\n * Check if a messaging MCP server (AIBroker, Whazaa, or Telex) is configured.\n * When any messaging server is active, the AI handles notifications via MCP\n * and ntfy is skipped to avoid duplicates.\n */\nexport function isWhatsAppEnabled(): boolean {\n try {\n const settingsPath = join(homedir(), '.claude', 'settings.json');\n if (!existsSync(settingsPath)) return false;\n\n const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n const enabled: string[] = settings.enabledMcpjsonServers || [];\n return enabled.includes('aibroker') || enabled.includes('whazaa') || enabled.includes('telex');\n } catch {\n return false;\n }\n}\n\n/**\n * Send push notification \u2014 WhatsApp-aware with ntfy fallback.\n *\n * When WhatsApp (Whazaa) is enabled in MCP config, ntfy is SKIPPED\n * because the AI sends WhatsApp messages directly via MCP.\n * When WhatsApp is NOT configured, ntfy fires as the fallback channel.\n */\nexport async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {\n if (isWhatsAppEnabled()) {\n console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);\n return true;\n }\n\n const topic = process.env.NTFY_TOPIC;\n\n if (!topic) {\n console.error('NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled');\n return false;\n }\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const response = await fetch(`https://ntfy.sh/${topic}`, {\n method: 'POST',\n body: message,\n headers: {\n 'Title': 'Claude Code',\n 'Priority': 'default',\n },\n });\n\n if (response.ok) {\n console.error(`ntfy.sh notification sent (WhatsApp inactive): \"${message}\"`);\n return true;\n } else {\n console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);\n }\n } catch (error) {\n console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);\n }\n\n if (attempt < retries) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n\n console.error('ntfy.sh notification failed after all retries');\n return false;\n}\n", "/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month \u2192 previous month \u2192 flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, and \"[object Object]\" artifacts.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.startsWith('/')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note \u2014 mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["existsSync", "readdirSync", "readFileSync", "join", "basename", "existsSync", "join", "join", "existsSync", "join", "existsSync", "join", "existsSync", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "join", "readFileSync", "basename", "mkdirSync", "readdirSync", "renameSync", "writeFileSync"]
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\n/**\n * load-project-context.ts\n *\n * SessionStart hook that sets up project context:\n * - Checks for CLAUDE.md in various locations (Claude Code handles loading)\n * - Sets up Notes/ directory in ~/.claude/projects/{encoded-path}/\n * - Ensures TODO.md exists\n * - Sends ntfy.sh notification (mandatory)\n * - Displays session continuity info (like session-init.sh)\n *\n * This hook complements Claude Code's native CLAUDE.md loading by:\n * - Setting up the Notes infrastructure\n * - Showing the latest session note for continuity\n * - Sending ntfy.sh notifications\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from 'fs';\nimport { join, basename, dirname, resolve } from 'path';\nimport { homedir } from 'os';\nimport { execSync } from 'child_process';\nimport {\n PAI_DIR,\n findNotesDir,\n getProjectDir,\n getCurrentNotePath,\n createSessionNote,\n findTodoPath,\n findAllClaudeMdPaths,\n sendNtfyNotification,\n isProbeSession\n} from '../lib/project-utils';\n\n/**\n * Find the pai CLI binary path dynamically.\n * Tries `which pai` first, then common fallback locations.\n */\nfunction findPaiBinary(): string {\n try {\n return execSync('which pai', { encoding: 'utf-8' }).trim();\n } catch {\n // Fallback locations in order of preference\n const fallbacks = [\n '/usr/local/bin/pai',\n '/opt/homebrew/bin/pai',\n `${process.env.HOME}/.local/bin/pai`,\n ];\n for (const p of fallbacks) {\n if (existsSync(p)) return p;\n }\n }\n return 'pai'; // Last resort: rely on PATH at runtime\n}\n\n/**\n * Check session-routing.json for an active route.\n * Returns the routed Notes path if set, or null to use default behavior.\n */\nfunction getRoutedNotesPath(): string | null {\n const routingFile = join(PAI_DIR, 'session-routing.json');\n if (!existsSync(routingFile)) return null;\n\n try {\n const routing = JSON.parse(readFileSync(routingFile, 'utf-8'));\n const active = routing?.active_session;\n if (active?.notes_path) {\n return active.notes_path;\n }\n } catch {\n // Ignore parse errors\n }\n return null;\n}\n\n/**\n * Project signals that indicate a directory is a real project root.\n */\nconst PROJECT_SIGNALS = [\n '.git',\n 'package.json',\n 'pubspec.yaml',\n 'Cargo.toml',\n 'go.mod',\n 'pyproject.toml',\n 'setup.py',\n 'build.gradle',\n 'pom.xml',\n 'composer.json',\n 'Gemfile',\n 'Makefile',\n 'CMakeLists.txt',\n 'tsconfig.json',\n 'CLAUDE.md',\n join('Notes', 'PAI.md'),\n];\n\n/**\n * Returns true if the given directory looks like a project root.\n * Checks for the presence of well-known project signal files/dirs.\n */\nfunction hasProjectSignals(dir: string): boolean {\n for (const signal of PROJECT_SIGNALS) {\n if (existsSync(join(dir, signal))) return true;\n }\n return false;\n}\n\n/**\n * Returns true if the directory should NOT be auto-registered.\n * Guards: home directory, shallow paths, temp directories.\n */\nfunction isGuardedPath(dir: string): boolean {\n const home = homedir();\n const resolved = resolve(dir);\n\n // Never register the home directory itself\n if (resolved === home) return true;\n\n // Depth guard: require at least 3 path segments beyond root\n // e.g. /Users/i052341/foo is depth 3 on macOS \u2014 reject it\n const parts = resolved.split('/').filter(Boolean);\n if (parts.length < 3) return true;\n\n // Temp/system directories\n const forbidden = ['/tmp', '/var', '/private/tmp', '/private/var/folders'];\n for (const prefix of forbidden) {\n if (resolved === prefix || resolved.startsWith(prefix + '/')) return true;\n }\n\n return false;\n}\n\ninterface HookInput {\n session_id: string;\n cwd: string;\n hook_event_name: string;\n}\n\nasync function main() {\n console.error('\\nload-project-context.ts starting...');\n\n // Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)\n if (isProbeSession()) {\n console.error('Probe session detected - skipping project context loading');\n process.exit(0);\n }\n\n // Read hook input from stdin\n let hookInput: HookInput | null = null;\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (input.trim()) {\n hookInput = JSON.parse(input);\n }\n } catch (error) {\n console.error('Could not parse hook input, using process.cwd()');\n }\n\n // Get current working directory\n const cwd = hookInput?.cwd || process.cwd();\n\n // Determine meaningful project name\n // If cwd is a Notes directory, use parent directory name instead\n let projectName = basename(cwd);\n if (projectName.toLowerCase() === 'notes') {\n projectName = basename(dirname(cwd));\n }\n\n console.error(`Working directory: ${cwd}`);\n console.error(`Project: ${projectName}`);\n\n // Check if this is a subagent session - skip for subagents\n const isSubagent = process.env.CLAUDE_AGENT_TYPE !== undefined ||\n (process.env.CLAUDE_PROJECT_DIR || '').includes('/.claude/agents/');\n\n if (isSubagent) {\n console.error('Subagent session - skipping project context setup');\n process.exit(0);\n }\n\n // 1. Find and READ all CLAUDE.md files - inject them into context\n // This ensures Claude actually processes the instructions, not just sees them in headers\n const claudeMdPaths = findAllClaudeMdPaths(cwd);\n const claudeMdContents: { path: string; content: string }[] = [];\n\n if (claudeMdPaths.length > 0) {\n console.error(`Found ${claudeMdPaths.length} CLAUDE.md file(s):`);\n for (const path of claudeMdPaths) {\n console.error(` - ${path}`);\n try {\n const content = readFileSync(path, 'utf-8');\n claudeMdContents.push({ path, content });\n console.error(` Read ${content.length} chars`);\n } catch (error) {\n console.error(` Could not read: ${error}`);\n }\n }\n } else {\n console.error('No CLAUDE.md found in project');\n console.error(' Consider creating one at ./CLAUDE.md or ./.claude/CLAUDE.md');\n }\n\n // 2. Find or create Notes directory\n // Priority:\n // 1. Active session routing (pai route <project>) \u2192 routed Obsidian path\n // 2. Local Notes/ in cwd \u2192 use it (git-trackable, e.g. symlink to Obsidian)\n // 3. Central ~/.claude/projects/.../Notes/ \u2192 fallback\n const routedPath = getRoutedNotesPath();\n let notesDir: string;\n\n if (routedPath) {\n // Routing is active - use the configured Obsidian Notes path\n const { mkdirSync } = await import('fs');\n if (!existsSync(routedPath)) {\n mkdirSync(routedPath, { recursive: true });\n console.error(`Created routed Notes: ${routedPath}`);\n } else {\n console.error(`Notes directory: ${routedPath} (routed via pai route)`);\n }\n notesDir = routedPath;\n } else {\n const notesInfo = findNotesDir(cwd);\n\n if (notesInfo.isLocal) {\n notesDir = notesInfo.path;\n console.error(`Notes directory: ${notesDir} (local)`);\n } else {\n // Create central Notes directory\n if (!existsSync(notesInfo.path)) {\n const { mkdirSync } = await import('fs');\n mkdirSync(notesInfo.path, { recursive: true });\n console.error(`Created central Notes: ${notesInfo.path}`);\n } else {\n console.error(`Notes directory: ${notesInfo.path} (central)`);\n }\n notesDir = notesInfo.path;\n }\n }\n\n // 3. Cleanup old .jsonl files from project root (move to sessions/)\n // Keep the newest one for potential resume, move older ones to sessions/\n const projectDir = getProjectDir(cwd);\n if (existsSync(projectDir)) {\n try {\n const files = readdirSync(projectDir);\n const jsonlFiles = files\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({\n name: f,\n path: join(projectDir, f),\n mtime: statSync(join(projectDir, f)).mtime.getTime()\n }))\n .sort((a, b) => b.mtime - a.mtime); // newest first\n\n if (jsonlFiles.length > 1) {\n const { mkdirSync, renameSync } = await import('fs');\n const sessionsDir = join(projectDir, 'sessions');\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n }\n\n // Move all except the newest\n for (let i = 1; i < jsonlFiles.length; i++) {\n const file = jsonlFiles[i];\n const destPath = join(sessionsDir, file.name);\n if (!existsSync(destPath)) {\n renameSync(file.path, destPath);\n console.error(`Moved old session: ${file.name} \u2192 sessions/`);\n }\n }\n }\n } catch (error) {\n console.error(`Could not cleanup old .jsonl files: ${error}`);\n }\n }\n\n // 4. Find or create TODO.md\n const todoPath = findTodoPath(cwd);\n const hasTodo = existsSync(todoPath);\n if (hasTodo) {\n console.error(`TODO.md: ${todoPath}`);\n } else {\n // Create TODO.md in the Notes directory\n const newTodoPath = join(notesDir, 'TODO.md');\n const { writeFileSync } = await import('fs');\n writeFileSync(newTodoPath, `# TODO\\n\\n## Offen\\n\\n- [ ] \\n\\n---\\n\\n*Created: ${new Date().toISOString()}*\\n`);\n console.error(`Created TODO.md: ${newTodoPath}`);\n }\n\n // 5. Check for existing note or create new one\n let activeNotePath: string | null = null;\n\n if (notesDir) { // notesDir is always set now (local or central)\n const currentNotePath = getCurrentNotePath(notesDir);\n\n // Only create a new note if there is truly no note at all.\n // A completed note is still used \u2014 it will be updated or continued.\n // This prevents duplicate notes at month boundaries and on every compaction.\n if (!currentNotePath) {\n // Defensive: ensure projectName is a usable string\n const safeProjectName = (typeof projectName === 'string' && projectName.trim().length > 0)\n ? projectName.trim()\n : 'Untitled Session';\n console.error('\\nNo previous session notes found - creating new one');\n activeNotePath = createSessionNote(notesDir, String(safeProjectName));\n console.error(`Created: ${basename(activeNotePath)}`);\n } else {\n activeNotePath = currentNotePath!;\n console.error(`\\nUsing existing session note: ${basename(activeNotePath)}`);\n // Show preview of current note\n try {\n const content = readFileSync(activeNotePath, 'utf-8');\n const lines = content.split('\\n').slice(0, 12);\n console.error('--- Current Note Preview ---');\n for (const line of lines) {\n console.error(line);\n }\n console.error('--- End Preview ---\\n');\n } catch {\n // Ignore read errors\n }\n }\n }\n\n // 6. Show TODO.md preview\n if (existsSync(todoPath)) {\n try {\n const todoContent = readFileSync(todoPath, 'utf-8');\n const todoLines = todoContent.split('\\n').filter(l => l.includes('[ ]')).slice(0, 5);\n if (todoLines.length > 0) {\n console.error('\\nOpen TODOs:');\n for (const line of todoLines) {\n console.error(` ${line.trim()}`);\n }\n }\n } catch {\n // Ignore read errors\n }\n }\n\n // 7. Send ntfy.sh notification (MANDATORY)\n await sendNtfyNotification(`Session started in ${projectName}`);\n\n // 7.5. Run pai project detect to identify the registered PAI project\n const paiBin = findPaiBinary();\n let paiProjectBlock = '';\n try {\n const { execFileSync } = await import('child_process');\n const raw = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n encoding: 'utf-8',\n env: process.env,\n }).trim();\n\n if (raw) {\n const detected = JSON.parse(raw) as {\n slug?: string;\n display_name?: string;\n root_path?: string;\n match_type?: string;\n relative_path?: string | null;\n session_count?: number;\n status?: string;\n error?: string;\n cwd?: string;\n };\n\n if (detected.error === 'no_match') {\n // Attempt auto-registration if the directory looks like a real project\n let autoRegistered = false;\n\n if (!isGuardedPath(cwd) && hasProjectSignals(cwd)) {\n try {\n execFileSync(paiBin, ['project', 'add', cwd], {\n encoding: 'utf-8',\n env: process.env,\n });\n console.error(`PAI auto-registered project at: ${cwd}`);\n\n // Re-run detect to get the proper detection result\n try {\n const raw2 = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {\n encoding: 'utf-8',\n env: process.env,\n }).trim();\n\n if (raw2) {\n const detected2 = JSON.parse(raw2) as typeof detected;\n if (detected2.slug) {\n const name2 = detected2.display_name || detected2.slug;\n console.error(`PAI auto-registered: \"${detected2.slug}\" (${detected2.match_type})`);\n paiProjectBlock = `PAI Project Registry: ${name2} (slug: ${detected2.slug}) [AUTO-REGISTERED]\nMatch: ${detected2.match_type ?? 'exact'} | Sessions: 0`;\n autoRegistered = true;\n }\n }\n } catch (detectErr) {\n console.error('PAI auto-registration: project added but re-detect failed:', detectErr);\n autoRegistered = true; // project IS registered, just can't load context\n }\n } catch (addErr) {\n console.error('PAI auto-registration failed (project add):', addErr);\n }\n }\n\n if (!autoRegistered) {\n paiProjectBlock = `PAI Project Registry: No registered project matches this directory.\nRun \"pai project add .\" to register this project, or use /route to tag the session.`;\n console.error('PAI detect: no match for', cwd);\n }\n } else if (detected.slug) {\n const name = detected.display_name || detected.slug;\n const nameSlug = ` (slug: ${detected.slug})`;\n const matchDesc = detected.match_type === 'exact'\n ? 'exact'\n : `parent (+${detected.relative_path ?? ''})`;\n const statusFlag = detected.status && detected.status !== 'active'\n ? ` [${detected.status.toUpperCase()}]`\n : '';\n paiProjectBlock = `PAI Project Registry: ${name}${statusFlag}${nameSlug}\nMatch: ${matchDesc} | Sessions: ${detected.session_count ?? 0}${detected.status && detected.status !== 'active' ? `\\nWARNING: Project status is \"${detected.status}\". Run: pai project health --fix` : ''}`;\n console.error(`PAI detect: matched \"${detected.slug}\" (${detected.match_type})`);\n }\n }\n } catch (e) {\n // Non-fatal \u2014 don't break session start if pai is unavailable\n console.error('pai project detect failed:', e);\n }\n\n // 8. Output system reminder with session info\n const reminder = `\n<system-reminder>\nPROJECT CONTEXT LOADED\n\nProject: ${projectName}\nWorking Directory: ${cwd}\n${notesDir ? `Notes Directory: ${notesDir}${routedPath ? ' (routed via pai route)' : ''}` : 'Notes: disabled (no local Notes/ directory)'}\n${hasTodo ? `TODO: ${todoPath}` : 'TODO: not found'}\n${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(', ')}` : 'No CLAUDE.md found'}\n${activeNotePath ? `Active Note: ${basename(activeNotePath)}` : ''}\n${routedPath ? `\\nNote Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ''}\n${paiProjectBlock ? `\\n${paiProjectBlock}` : ''}\nSession Commands:\n- \"pause session\" \u2192 Save checkpoint, update TODO, exit (no compact)\n- \"end session\" \u2192 Finalize note, commit if needed, start fresh next time\n- \"pai route clear\" \u2192 Clear note routing (in a new session)\n</system-reminder>\n`;\n\n // Output to stdout for Claude to receive\n console.log(reminder);\n\n // 9. INJECT CLAUDE.md contents as system-reminders\n // This ensures Claude actually reads and processes the instructions\n for (const { path, content } of claudeMdContents) {\n const claudeMdReminder = `\n<system-reminder>\nLOCAL CLAUDE.md LOADED (MANDATORY - READ AND FOLLOW)\n\nSource: ${path}\n\n${content}\n\n---\nTHE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.\n</system-reminder>\n`;\n console.log(claudeMdReminder);\n console.error(`Injected CLAUDE.md content from: ${path}`);\n }\n\n console.error('\\nProject context setup complete\\n');\n process.exit(0);\n}\n\nmain().catch(error => {\n console.error('load-project-context.ts error:', error);\n process.exit(0); // Don't block session start\n});\n", "/**\n * Path utilities \u2014 encoding, Notes/Sessions directory discovery and creation.\n */\n\nimport { existsSync, mkdirSync, readdirSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\nimport { PAI_DIR } from '../pai-paths.js';\n\n// Re-export PAI_DIR for consumers\nexport { PAI_DIR };\nexport const PROJECTS_DIR = join(PAI_DIR, 'projects');\n\n/**\n * Directories known to be automated health-check / probe sessions.\n * Hooks should exit early for these to avoid registry clutter and wasted work.\n */\nconst PROBE_CWD_PATTERNS = [\n '/CodexBar/ClaudeProbe',\n '/ClaudeProbe',\n];\n\n/**\n * Check if the current working directory belongs to a probe/health-check session.\n * Returns true if hooks should skip this session entirely.\n */\nexport function isProbeSession(cwd?: string): boolean {\n const dir = cwd || process.cwd();\n return PROBE_CWD_PATTERNS.some(pattern => dir.includes(pattern));\n}\n\n/**\n * Encode a path the same way Claude Code does:\n * - Replace / with -\n * - Replace . with -\n * - Replace space with -\n */\nexport function encodePath(path: string): string {\n return path\n .replace(/\\//g, '-')\n .replace(/\\./g, '-')\n .replace(/ /g, '-');\n}\n\n/** Get the project directory for a given working directory. */\nexport function getProjectDir(cwd: string): string {\n const encoded = encodePath(cwd);\n return join(PROJECTS_DIR, encoded);\n}\n\n/** Get the Notes directory for a project (central location). */\nexport function getNotesDir(cwd: string): string {\n return join(getProjectDir(cwd), 'Notes');\n}\n\n/**\n * Find Notes directory \u2014 checks local first, falls back to central.\n * Does NOT create the directory.\n */\nexport function findNotesDir(cwd: string): { path: string; isLocal: boolean } {\n const cwdBasename = basename(cwd).toLowerCase();\n if (cwdBasename === 'notes' && existsSync(cwd)) {\n return { path: cwd, isLocal: true };\n }\n\n const localPaths = [\n join(cwd, 'Notes'),\n join(cwd, 'notes'),\n join(cwd, '.claude', 'Notes'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) {\n return { path, isLocal: true };\n }\n }\n\n return { path: getNotesDir(cwd), isLocal: false };\n}\n\n/** Get the sessions/ directory for a project (stores .jsonl transcripts). */\nexport function getSessionsDir(cwd: string): string {\n return join(getProjectDir(cwd), 'sessions');\n}\n\n/** Get the sessions/ directory from a project directory path. */\nexport function getSessionsDirFromProjectDir(projectDir: string): string {\n return join(projectDir, 'sessions');\n}\n\n// ---------------------------------------------------------------------------\n// Directory creation helpers\n// ---------------------------------------------------------------------------\n\n/** Ensure the Notes directory exists for a project. @deprecated Use ensureNotesDirSmart() */\nexport function ensureNotesDir(cwd: string): string {\n const notesDir = getNotesDir(cwd);\n if (!existsSync(notesDir)) {\n mkdirSync(notesDir, { recursive: true });\n console.error(`Created Notes directory: ${notesDir}`);\n }\n return notesDir;\n}\n\n/**\n * Smart Notes directory handling:\n * - If local Notes/ exists \u2192 use it (don't create anything new)\n * - If no local Notes/ \u2192 ensure central exists and use that\n */\nexport function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {\n const found = findNotesDir(cwd);\n if (found.isLocal) return found;\n if (!existsSync(found.path)) {\n mkdirSync(found.path, { recursive: true });\n console.error(`Created central Notes directory: ${found.path}`);\n }\n return found;\n}\n\n/** Ensure the sessions/ directory exists for a project. */\nexport function ensureSessionsDir(cwd: string): string {\n const sessionsDir = getSessionsDir(cwd);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/** Ensure the sessions/ directory exists (from project dir path). */\nexport function ensureSessionsDirFromProjectDir(projectDir: string): string {\n const sessionsDir = getSessionsDirFromProjectDir(projectDir);\n if (!existsSync(sessionsDir)) {\n mkdirSync(sessionsDir, { recursive: true });\n console.error(`Created sessions directory: ${sessionsDir}`);\n }\n return sessionsDir;\n}\n\n/**\n * Move all .jsonl session files from project root to sessions/ subdirectory.\n * Returns the number of files moved.\n */\nexport function moveSessionFilesToSessionsDir(\n projectDir: string,\n excludeFile?: string,\n silent = false\n): number {\n const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);\n\n if (!existsSync(projectDir)) return 0;\n\n const files = readdirSync(projectDir);\n let movedCount = 0;\n\n for (const file of files) {\n if (file.endsWith('.jsonl') && file !== excludeFile) {\n const sourcePath = join(projectDir, file);\n const destPath = join(sessionsDir, file);\n try {\n renameSync(sourcePath, destPath);\n if (!silent) console.error(`Moved ${file} \u2192 sessions/`);\n movedCount++;\n } catch (error) {\n if (!silent) console.error(`Could not move ${file}: ${error}`);\n }\n }\n }\n\n return movedCount;\n}\n\n// ---------------------------------------------------------------------------\n// CLAUDE.md / TODO.md discovery\n// ---------------------------------------------------------------------------\n\n/** Find TODO.md \u2014 check local first, fallback to central. */\nexport function findTodoPath(cwd: string): string {\n const localPaths = [\n join(cwd, 'TODO.md'),\n join(cwd, 'notes', 'TODO.md'),\n join(cwd, 'Notes', 'TODO.md'),\n join(cwd, '.claude', 'TODO.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) return path;\n }\n\n return join(getNotesDir(cwd), 'TODO.md');\n}\n\n/** Find CLAUDE.md \u2014 returns the FIRST found path. */\nexport function findClaudeMdPath(cwd: string): string | null {\n const paths = findAllClaudeMdPaths(cwd);\n return paths.length > 0 ? paths[0] : null;\n}\n\n/**\n * Find ALL CLAUDE.md files in local locations in priority order.\n */\nexport function findAllClaudeMdPaths(cwd: string): string[] {\n const foundPaths: string[] = [];\n\n const localPaths = [\n join(cwd, '.claude', 'CLAUDE.md'),\n join(cwd, 'CLAUDE.md'),\n join(cwd, 'Notes', 'CLAUDE.md'),\n join(cwd, 'notes', 'CLAUDE.md'),\n join(cwd, 'Prompts', 'CLAUDE.md'),\n join(cwd, 'prompts', 'CLAUDE.md'),\n ];\n\n for (const path of localPaths) {\n if (existsSync(path)) foundPaths.push(path);\n }\n\n return foundPaths;\n}\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n", "/**\n * Push notification helpers \u2014 WhatsApp-aware with ntfy.sh fallback.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\n/**\n * Check if a messaging MCP server (AIBroker, Whazaa, or Telex) is configured.\n * When any messaging server is active, the AI handles notifications via MCP\n * and ntfy is skipped to avoid duplicates.\n */\nexport function isWhatsAppEnabled(): boolean {\n try {\n const settingsPath = join(homedir(), '.claude', 'settings.json');\n if (!existsSync(settingsPath)) return false;\n\n const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\n const enabled: string[] = settings.enabledMcpjsonServers || [];\n return enabled.includes('aibroker') || enabled.includes('whazaa') || enabled.includes('telex');\n } catch {\n return false;\n }\n}\n\n/**\n * Send push notification \u2014 WhatsApp-aware with ntfy fallback.\n *\n * When WhatsApp (Whazaa) is enabled in MCP config, ntfy is SKIPPED\n * because the AI sends WhatsApp messages directly via MCP.\n * When WhatsApp is NOT configured, ntfy fires as the fallback channel.\n */\nexport async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {\n if (isWhatsAppEnabled()) {\n console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);\n return true;\n }\n\n const topic = process.env.NTFY_TOPIC;\n\n if (!topic) {\n console.error('NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled');\n return false;\n }\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n const response = await fetch(`https://ntfy.sh/${topic}`, {\n method: 'POST',\n body: message,\n headers: {\n 'Title': 'Claude Code',\n 'Priority': 'default',\n },\n });\n\n if (response.ok) {\n console.error(`ntfy.sh notification sent (WhatsApp inactive): \"${message}\"`);\n return true;\n } else {\n console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);\n }\n } catch (error) {\n console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);\n }\n\n if (attempt < retries) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n\n console.error('ntfy.sh notification failed after all retries');\n return false;\n}\n", "/**\n * Session note creation, editing, checkpointing, renaming, and finalization.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join, basename } from 'path';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */\nfunction getMonthDir(notesDir: string): string {\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const monthDir = join(notesDir, year, month);\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n return monthDir;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Get the next note number (4-digit format: 0001, 0002, etc.).\n * Numbers are scoped per YYYY/MM directory.\n */\nexport function getNextNoteNumber(notesDir: string): string {\n const monthDir = getMonthDir(notesDir);\n\n const files = readdirSync(monthDir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-]/))\n .sort();\n\n if (files.length === 0) return '0001';\n\n let maxNumber = 0;\n for (const file of files) {\n const digitMatch = file.match(/^(\\d+)/);\n if (digitMatch) {\n const num = parseInt(digitMatch[1], 10);\n if (num > maxNumber) maxNumber = num;\n }\n }\n\n return String(maxNumber + 1).padStart(4, '0');\n}\n\n/**\n * Get the current (latest) note file path, or null if none exists.\n * Searches current month \u2192 previous month \u2192 flat notesDir (legacy).\n */\nexport function getCurrentNotePath(notesDir: string): string | null {\n if (!existsSync(notesDir)) return null;\n\n const findLatestIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n const files = readdirSync(dir)\n .filter(f => f.match(/^\\d{3,4}[\\s_-].*\\.md$/))\n .sort((a, b) => {\n const numA = parseInt(a.match(/^(\\d+)/)?.[1] || '0', 10);\n const numB = parseInt(b.match(/^(\\d+)/)?.[1] || '0', 10);\n return numA - numB;\n });\n if (files.length === 0) return null;\n return join(dir, files[files.length - 1]);\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const currentMonthDir = join(notesDir, year, month);\n const found = findLatestIn(currentMonthDir);\n if (found) return found;\n\n const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const prevYear = String(prevDate.getFullYear());\n const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');\n const prevMonthDir = join(notesDir, prevYear, prevMonth);\n const prevFound = findLatestIn(prevMonthDir);\n if (prevFound) return prevFound;\n\n return findLatestIn(notesDir);\n}\n\n/**\n * Create a new session note.\n * Format: \"NNNN - YYYY-MM-DD - New Session.md\" filed into YYYY/MM subdirectory.\n * Claude MUST rename at session end with a meaningful description.\n */\nexport function createSessionNote(notesDir: string, description: string): string {\n const noteNumber = getNextNoteNumber(notesDir);\n const date = new Date().toISOString().split('T')[0];\n const monthDir = getMonthDir(notesDir);\n const filename = `${noteNumber} - ${date} - New Session.md`;\n const filepath = join(monthDir, filename);\n\n const content = `# Session ${noteNumber}: ${description}\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;\n\n writeFileSync(filepath, content);\n console.error(`Created session note: ${filename}`);\n\n return filepath;\n}\n\n/** Append a checkpoint to the current session note. */\nexport function appendCheckpoint(notePath: string, checkpoint: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found, recreating: ${notePath}`);\n try {\n const parentDir = join(notePath, '..');\n if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });\n const noteFilename = basename(notePath);\n const numberMatch = noteFilename.match(/^(\\d+)/);\n const noteNumber = numberMatch ? numberMatch[1] : '0000';\n const date = new Date().toISOString().split('T')[0];\n const content = `# Session ${noteNumber}: Recovered\\n\\n**Date:** ${date}\\n**Status:** In Progress\\n\\n---\\n\\n## Work Done\\n\\n<!-- PAI will add completed work here during session -->\\n\\n---\\n\\n## Next Steps\\n\\n<!-- To be filled at session end -->\\n\\n---\\n\\n**Tags:** #Session\\n`;\n writeFileSync(notePath, content);\n console.error(`Recreated session note: ${noteFilename}`);\n } catch (err) {\n console.error(`Failed to recreate note: ${err}`);\n return;\n }\n }\n\n const content = readFileSync(notePath, 'utf-8');\n const timestamp = new Date().toISOString();\n const checkpointText = `\\n### Checkpoint ${timestamp}\\n\\n${checkpoint}\\n`;\n\n const nextStepsIndex = content.indexOf('## Next Steps');\n const newContent = nextStepsIndex !== -1\n ? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)\n : content + checkpointText;\n\n writeFileSync(notePath, newContent);\n console.error(`Checkpoint added to: ${basename(notePath)}`);\n}\n\n/** Work item for session notes. */\nexport interface WorkItem {\n title: string;\n details?: string[];\n completed?: boolean;\n}\n\n/** Add work items to the \"Work Done\" section of a session note. */\nexport function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n let workText = '';\n if (sectionTitle) workText += `\\n### ${sectionTitle}\\n\\n`;\n\n for (const item of workItems) {\n const checkbox = item.completed !== false ? '[x]' : '[ ]';\n workText += `- ${checkbox} **${item.title}**\\n`;\n if (item.details && item.details.length > 0) {\n for (const detail of item.details) {\n workText += ` - ${detail}\\n`;\n }\n }\n }\n\n const workDoneMatch = content.match(/## Work Done\\n\\n(<!-- .*? -->)?/);\n if (workDoneMatch) {\n const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;\n content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);\n } else {\n const nextStepsIndex = content.indexOf('## Next Steps');\n if (nextStepsIndex !== -1) {\n content = content.substring(0, nextStepsIndex) + workText + '\\n' + content.substring(nextStepsIndex);\n }\n }\n\n writeFileSync(notePath, content);\n console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);\n}\n\n/**\n * Check if a candidate title is meaningless / garbage.\n * Public wrapper around the internal filter for use by other hooks.\n */\nexport function isMeaningfulTitle(text: string): boolean {\n return !isMeaninglessCandidate(text);\n}\n\n/** Sanitize a string for use in a filename. */\nexport function sanitizeForFilename(str: string): string {\n return str\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 50);\n}\n\n/**\n * Return true if the candidate string should be rejected as a meaningful name.\n * Rejects file paths, shebangs, timestamps, system noise, XML tags, hashes, etc.\n */\nfunction isMeaninglessCandidate(text: string): boolean {\n const t = text.trim();\n if (!t) return true;\n if (t.length < 5) return true; // too short to be meaningful\n if (t.startsWith('/') || t.startsWith('~')) return true; // file path\n if (t.startsWith('#!')) return true; // shebang\n if (t.includes('[object Object]')) return true; // serialization artifact\n if (/^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+-]+)?$/.test(t)) return true; // ISO timestamp\n if (/^\\d{1,2}:\\d{2}(:\\d{2})?(\\s*(AM|PM))?$/i.test(t)) return true; // time-only\n if (/^<[a-z-]+[\\s/>]/i.test(t)) return true; // XML/HTML tags (<task-notification>, etc.)\n if (/^[0-9a-f]{10,}$/i.test(t)) return true; // hex hash strings\n if (/^Exit code \\d+/i.test(t)) return true; // exit code messages\n if (/^Error:/i.test(t)) return true; // error messages\n if (/^This session is being continued/i.test(t)) return true; // continuation boilerplate\n if (/^\\(Bash completed/i.test(t)) return true; // bash output noise\n if (/^Task Notification$/i.test(t)) return true; // literal \"Task Notification\"\n if (/^New Session$/i.test(t)) return true; // placeholder title\n if (/^Recovered Session$/i.test(t)) return true; // placeholder title\n if (/^Continued Session$/i.test(t)) return true; // placeholder title\n if (/^Untitled Session$/i.test(t)) return true; // placeholder title\n if (/^Context Compression$/i.test(t)) return true; // compression artifact\n if (/^[A-Fa-f0-9]{8,}\\s+Output$/i.test(t)) return true; // hash + \"Output\" pattern\n return false;\n}\n\n/**\n * Extract a meaningful name from session note content and summary.\n * Looks at Work Done section headers, bold text, and summary.\n */\nexport function extractMeaningfulName(noteContent: string, summary: string): string {\n const workDoneMatch = noteContent.match(/## Work Done\\n\\n([\\s\\S]*?)(?=\\n---|\\n## Next)/);\n\n if (workDoneMatch) {\n const workDoneSection = workDoneMatch[1];\n\n const subheadings = workDoneSection.match(/### ([^\\n]+)/g);\n if (subheadings && subheadings.length > 0) {\n const firstHeading = subheadings[0].replace('### ', '').trim();\n if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {\n return sanitizeForFilename(firstHeading);\n }\n }\n\n const boldMatches = workDoneSection.match(/\\*\\*([^*]+)\\*\\*/g);\n if (boldMatches && boldMatches.length > 0) {\n const firstBold = boldMatches[0].replace(/\\*\\*/g, '').trim();\n if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {\n return sanitizeForFilename(firstBold);\n }\n }\n\n const numberedItems = workDoneSection.match(/^\\d+\\.\\s+\\*\\*([^*]+)\\*\\*/m);\n if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {\n return sanitizeForFilename(numberedItems[1]);\n }\n }\n\n if (summary && summary.length > 5 && summary !== 'Session completed.' && !isMeaninglessCandidate(summary)) {\n const cleanSummary = summary\n .replace(/[^\\w\\s-]/g, ' ')\n .trim()\n .split(/\\s+/)\n .slice(0, 5)\n .join(' ');\n if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {\n return sanitizeForFilename(cleanSummary);\n }\n }\n\n return '';\n}\n\n/**\n * Rename a session note with a meaningful name.\n * Always uses \"NNNN - YYYY-MM-DD - Description.md\" format.\n * Returns the new path, or original path if rename fails.\n */\nexport function renameSessionNote(notePath: string, meaningfulName: string): string {\n if (!meaningfulName || !existsSync(notePath)) return notePath;\n\n const dir = join(notePath, '..');\n const oldFilename = basename(notePath);\n\n const correctMatch = oldFilename.match(/^(\\d{3,4}) - (\\d{4}-\\d{2}-\\d{2}) - .*\\.md$/);\n const legacyMatch = oldFilename.match(/^(\\d{3,4})_(\\d{4}-\\d{2}-\\d{2})_.*\\.md$/);\n const match = correctMatch || legacyMatch;\n if (!match) return notePath;\n\n const [, noteNumber, date] = match;\n\n const titleCaseName = meaningfulName\n .split(/[\\s_-]+/)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n .trim();\n\n const paddedNumber = noteNumber.padStart(4, '0');\n const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;\n const newPath = join(dir, newFilename);\n\n if (newFilename === oldFilename) return notePath;\n\n try {\n renameSync(notePath, newPath);\n console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);\n return newPath;\n } catch (error) {\n console.error(`Could not rename note: ${error}`);\n return notePath;\n }\n}\n\n/** Update the session note's H1 title and rename the file. */\nexport function updateSessionNoteTitle(notePath: string, newTitle: string): void {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n content = content.replace(/^# Session \\d+:.*$/m, (match) => {\n const sessionNum = match.match(/Session (\\d+)/)?.[1] || '';\n return `# Session ${sessionNum}: ${newTitle}`;\n });\n writeFileSync(notePath, content);\n renameSessionNote(notePath, sanitizeForFilename(newTitle));\n}\n\n/**\n * Finalize session note \u2014 mark as complete, add summary, rename with meaningful name.\n * IDEMPOTENT: subsequent calls are no-ops if already finalized.\n * Returns the final path (may be renamed).\n */\nexport function finalizeSessionNote(notePath: string, summary: string): string {\n if (!existsSync(notePath)) {\n console.error(`Note file not found: ${notePath}`);\n return notePath;\n }\n\n let content = readFileSync(notePath, 'utf-8');\n\n if (content.includes('**Status:** Completed')) {\n console.error(`Note already finalized: ${basename(notePath)}`);\n return notePath;\n }\n\n content = content.replace('**Status:** In Progress', '**Status:** Completed');\n\n if (!content.includes('**Completed:**')) {\n const completionTime = new Date().toISOString();\n content = content.replace(\n '---\\n\\n## Work Done',\n `**Completed:** ${completionTime}\\n\\n---\\n\\n## Work Done`\n );\n }\n\n const nextStepsMatch = content.match(/## Next Steps\\n\\n(<!-- .*? -->)/);\n if (nextStepsMatch) {\n content = content.replace(\n nextStepsMatch[0],\n `## Next Steps\\n\\n${summary || 'Session completed.'}`\n );\n }\n\n writeFileSync(notePath, content);\n console.error(`Session note finalized: ${basename(notePath)}`);\n\n const meaningfulName = extractMeaningfulName(content, summary);\n if (meaningfulName) {\n return renameSessionNote(notePath, meaningfulName);\n }\n\n return notePath;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["existsSync", "readdirSync", "readFileSync", "join", "basename", "resolve", "homedir", "existsSync", "join", "join", "existsSync", "join", "existsSync", "join", "existsSync", "existsSync", "readFileSync", "join", "homedir", "resolve", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "renameSync", "join", "basename", "existsSync", "join", "readFileSync", "homedir", "resolve", "basename", "mkdirSync", "readdirSync", "renameSync", "writeFileSync"]
|
|
7
7
|
}
|
package/dist/hooks/stop-hook.mjs
CHANGED
|
@@ -271,11 +271,25 @@ function sanitizeForFilename(str) {
|
|
|
271
271
|
function isMeaninglessCandidate(text) {
|
|
272
272
|
const t = text.trim();
|
|
273
273
|
if (!t) return true;
|
|
274
|
-
if (t.
|
|
274
|
+
if (t.length < 5) return true;
|
|
275
|
+
if (t.startsWith("/") || t.startsWith("~")) return true;
|
|
275
276
|
if (t.startsWith("#!")) return true;
|
|
276
277
|
if (t.includes("[object Object]")) return true;
|
|
277
278
|
if (/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/.test(t)) return true;
|
|
278
279
|
if (/^\d{1,2}:\d{2}(:\d{2})?(\s*(AM|PM))?$/i.test(t)) return true;
|
|
280
|
+
if (/^<[a-z-]+[\s/>]/i.test(t)) return true;
|
|
281
|
+
if (/^[0-9a-f]{10,}$/i.test(t)) return true;
|
|
282
|
+
if (/^Exit code \d+/i.test(t)) return true;
|
|
283
|
+
if (/^Error:/i.test(t)) return true;
|
|
284
|
+
if (/^This session is being continued/i.test(t)) return true;
|
|
285
|
+
if (/^\(Bash completed/i.test(t)) return true;
|
|
286
|
+
if (/^Task Notification$/i.test(t)) return true;
|
|
287
|
+
if (/^New Session$/i.test(t)) return true;
|
|
288
|
+
if (/^Recovered Session$/i.test(t)) return true;
|
|
289
|
+
if (/^Continued Session$/i.test(t)) return true;
|
|
290
|
+
if (/^Untitled Session$/i.test(t)) return true;
|
|
291
|
+
if (/^Context Compression$/i.test(t)) return true;
|
|
292
|
+
if (/^[A-Fa-f0-9]{8,}\s+Output$/i.test(t)) return true;
|
|
279
293
|
return false;
|
|
280
294
|
}
|
|
281
295
|
function extractMeaningfulName(noteContent, summary) {
|
|
@@ -533,6 +547,60 @@ function enqueueWithDaemon(payload) {
|
|
|
533
547
|
}, DAEMON_TIMEOUT_MS);
|
|
534
548
|
});
|
|
535
549
|
}
|
|
550
|
+
function enqueueSessionSummaryWithDaemon(payload) {
|
|
551
|
+
return new Promise((resolve2) => {
|
|
552
|
+
let done = false;
|
|
553
|
+
let buffer = "";
|
|
554
|
+
let timer = null;
|
|
555
|
+
function finish(ok) {
|
|
556
|
+
if (done) return;
|
|
557
|
+
done = true;
|
|
558
|
+
if (timer !== null) {
|
|
559
|
+
clearTimeout(timer);
|
|
560
|
+
timer = null;
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
client.destroy();
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
resolve2(ok);
|
|
567
|
+
}
|
|
568
|
+
const client = connect(DAEMON_SOCKET, () => {
|
|
569
|
+
const msg = JSON.stringify({
|
|
570
|
+
id: randomUUID(),
|
|
571
|
+
method: "work_queue_enqueue",
|
|
572
|
+
params: {
|
|
573
|
+
type: "session-summary",
|
|
574
|
+
priority: 4,
|
|
575
|
+
payload: {
|
|
576
|
+
cwd: payload.cwd,
|
|
577
|
+
force: true
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}) + "\n";
|
|
581
|
+
client.write(msg);
|
|
582
|
+
});
|
|
583
|
+
client.on("data", (chunk) => {
|
|
584
|
+
buffer += chunk.toString();
|
|
585
|
+
const nl = buffer.indexOf("\n");
|
|
586
|
+
if (nl === -1) return;
|
|
587
|
+
const line = buffer.slice(0, nl);
|
|
588
|
+
try {
|
|
589
|
+
const response = JSON.parse(line);
|
|
590
|
+
if (response.ok) {
|
|
591
|
+
console.error(`STOP-HOOK: Session summary enqueued (id=${response.result?.id}).`);
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
finish(true);
|
|
596
|
+
});
|
|
597
|
+
client.on("error", () => finish(false));
|
|
598
|
+
client.on("end", () => {
|
|
599
|
+
if (!done) finish(false);
|
|
600
|
+
});
|
|
601
|
+
timer = setTimeout(() => finish(false), DAEMON_TIMEOUT_MS);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
536
604
|
function extractWorkFromTranscript(lines) {
|
|
537
605
|
const workItems = [];
|
|
538
606
|
const seenSummaries = /* @__PURE__ */ new Set();
|
|
@@ -782,6 +850,7 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
|
|
|
782
850
|
console.error("STOP-HOOK: Using direct execution fallback.");
|
|
783
851
|
await executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery);
|
|
784
852
|
}
|
|
853
|
+
await enqueueSessionSummaryWithDaemon({ cwd });
|
|
785
854
|
console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
786
855
|
`);
|
|
787
856
|
}
|