@tekmidian/pai 0.6.5 → 0.6.6
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.
|
@@ -73,7 +73,17 @@ async function main() {
|
|
|
73
73
|
if (!input || input.trim() === "") {
|
|
74
74
|
process.exit(0);
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
if (!process.env.__PAI_HOOK_BG) {
|
|
77
|
+
const { spawn } = await import("child_process");
|
|
78
|
+
const child = spawn(process.execPath, [process.argv[1], "--background"], {
|
|
79
|
+
detached: true,
|
|
80
|
+
stdio: "ignore",
|
|
81
|
+
env: { ...process.env, __PAI_HOOK_BG: "1", __PAI_HOOK_INPUT: input }
|
|
82
|
+
});
|
|
83
|
+
child.unref();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
const data = JSON.parse(process.env.__PAI_HOOK_INPUT || input);
|
|
77
87
|
const now = /* @__PURE__ */ new Date();
|
|
78
88
|
const timestamp = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-");
|
|
79
89
|
const yearMonth = timestamp.substring(0, 7);
|
|
@@ -99,7 +109,10 @@ async function analyzeSession(conversationId, yearMonth) {
|
|
|
99
109
|
let toolsUsed = /* @__PURE__ */ new Set();
|
|
100
110
|
try {
|
|
101
111
|
if (existsSync2(rawOutputsDir)) {
|
|
102
|
-
const
|
|
112
|
+
const todayPrefix = (/* @__PURE__ */ new Date()).toISOString().substring(0, 10);
|
|
113
|
+
const files = readdirSync(rawOutputsDir).filter(
|
|
114
|
+
(f) => f.endsWith(".jsonl") && f.startsWith(todayPrefix)
|
|
115
|
+
);
|
|
103
116
|
for (const file of files) {
|
|
104
117
|
const filePath = join2(rawOutputsDir, file);
|
|
105
118
|
const content = readFileSync2(filePath, "utf-8");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/ts/session-end/capture-session-summary.ts", "../../src/hooks/ts/lib/pai-paths.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\n/**\n * SessionEnd Hook - Captures session summary for UOCS\n *\n * Generates a session summary document when a Claude Code session ends,\n * documenting what was accomplished during the session.\n */\n\nimport { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';\n\ninterface SessionData {\n conversation_id: string;\n timestamp: string;\n [key: string]: any;\n}\n\nasync function main() {\n try {\n // Read input from stdin\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 || input.trim() === '') {\n process.exit(0);\n }\n\n const data: SessionData = JSON.parse(input);\n\n // Generate timestamp for filename\n const now = new Date();\n const timestamp = now.toISOString()\n .replace(/:/g, '')\n .replace(/\\..+/, '')\n .replace('T', '-'); // YYYY-MM-DD-HHMMSS\n\n const yearMonth = timestamp.substring(0, 7); // YYYY-MM\n\n // Try to extract session info from raw outputs\n const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);\n\n // Generate filename\n const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;\n\n // Ensure directory exists\n const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);\n if (!existsSync(sessionDir)) {\n mkdirSync(sessionDir, { recursive: true });\n }\n\n // Generate session document\n const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);\n\n // Write session file\n writeFileSync(join(sessionDir, filename), sessionDoc);\n\n // Also store structured summary via daemon IPC for the observations system\n await storeStructuredSummary(data.conversation_id, sessionInfo);\n\n // Exit successfully\n process.exit(0);\n } catch (error) {\n // Silent failure - don't disrupt workflow\n console.error(`[UOCS] SessionEnd hook error: ${error}`);\n process.exit(0);\n }\n}\n\nasync function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {\n // Try to read raw outputs for this session\n const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);\n\n let filesChanged: string[] = [];\n let commandsExecuted: string[] = [];\n let toolsUsed: Set<string> = new Set();\n\n try {\n if (existsSync(rawOutputsDir)) {\n const files = readdirSync(rawOutputsDir).filter(f => f.endsWith('.jsonl'));\n\n for (const file of files) {\n const filePath = join(rawOutputsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.session === conversationId) {\n toolsUsed.add(entry.tool);\n\n // Extract file changes\n if (entry.tool === 'Edit' || entry.tool === 'Write') {\n if (entry.input?.file_path) {\n filesChanged.push(entry.input.file_path);\n }\n }\n\n // Extract bash commands\n if (entry.tool === 'Bash' && entry.input?.command) {\n commandsExecuted.push(entry.input.command);\n }\n }\n } catch (e) {\n // Skip invalid JSON lines\n }\n }\n }\n }\n } catch (error) {\n // Silent failure\n }\n\n return {\n focus: 'general-work',\n filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10\n commandsExecuted: commandsExecuted.slice(0, 10), // Max 10\n toolsUsed: Array.from(toolsUsed),\n duration: 0 // Unknown\n };\n}\n\nfunction formatSessionDocument(timestamp: string, data: SessionData, info: any): string {\n const date = timestamp.substring(0, 10); // YYYY-MM-DD\n const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS\n const da = process.env.DA || 'PAI';\n\n return `---\ncapture_type: SESSION\ntimestamp: ${new Date().toISOString()}\nsession_id: ${data.conversation_id}\nduration_minutes: ${info.duration}\nexecutor: ${da}\n---\n\n# Session: ${info.focus}\n\n**Date:** ${date}\n**Time:** ${time}\n**Session ID:** ${data.conversation_id}\n\n---\n\n## Session Overview\n\n**Focus:** General development work\n**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}\n\n---\n\n## Tools Used\n\n${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\\n') : '- None recorded'}\n\n---\n\n## Files Modified\n\n${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \\`${f}\\``).join('\\n') : '- None recorded'}\n\n**Total Files Changed:** ${info.filesChanged.length}\n\n---\n\n## Commands Executed\n\n${info.commandsExecuted.length > 0 ? '```bash\\n' + info.commandsExecuted.join('\\n') + '\\n```' : 'None recorded'}\n\n---\n\n## Notes\n\nThis session summary was automatically generated by the UOCS SessionEnd hook.\n\nFor detailed tool outputs, see: \\`\\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\\`\n\n---\n\n**Session Outcome:** Completed\n**Generated:** ${new Date().toISOString()}\n`;\n}\n\nasync function storeStructuredSummary(\n sessionId: string,\n info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }\n): Promise<void> {\n try {\n const cwd = process.cwd();\n const net = await import('net');\n\n await new Promise<void>((resolve, _reject) => {\n const client = net.createConnection('/tmp/pai.sock', () => {\n const msg = JSON.stringify({\n id: 1,\n method: 'session_summary_store',\n params: {\n session_id: sessionId,\n cwd,\n request: null, // We don't have the original request\n investigated: null,\n learned: null,\n completed: info.filesChanged.length > 0\n ? `Modified ${info.filesChanged.length} file(s): ${info.filesChanged.slice(0, 5).join(', ')}`\n : null,\n next_steps: null,\n observation_count: 0, // Will be filled by daemon from actual count\n }\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', () => { client.end(); resolve(); });\n client.on('error', () => resolve()); // Silent failure\n setTimeout(() => { client.destroy(); resolve(); }, 3000);\n });\n } catch {\n // Silent failure \u2014 don't disrupt session end\n }\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
|
|
5
|
-
"mappings": ";;;AASA,SAAS,eAAe,WAAW,cAAAA,aAAY,gBAAAC,eAAc,mBAAmB;AAChF,SAAS,QAAAC,aAAY;;;ACGrB,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAS,YAAY,oBAAoB;AAMzC,SAAS,cAAoB;AAE3B,QAAM,gBAAgB;AAAA,IACpB,QAAQ,QAAQ,IAAI,WAAW,IAAI,MAAM;AAAA,IACzC,QAAQ,QAAQ,GAAG,WAAW,MAAM;AAAA,EACtC;AAEA,aAAW,WAAW,eAAe;AACnC,QAAI,WAAW,OAAO,GAAG;AACvB,UAAI;AACF,cAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,mBAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,gBAAM,UAAU,KAAK,KAAK;AAE1B,cAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AAEzC,gBAAM,UAAU,QAAQ,QAAQ,GAAG;AACnC,cAAI,UAAU,GAAG;AACf,kBAAM,MAAM,QAAQ,UAAU,GAAG,OAAO,EAAE,KAAK;AAC/C,gBAAI,QAAQ,QAAQ,UAAU,UAAU,CAAC,EAAE,KAAK;AAGhD,gBAAK,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAC3C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,GAAI;AAClD,sBAAQ,MAAM,MAAM,GAAG,EAAE;AAAA,YAC3B;AAGA,oBAAQ,MAAM,QAAQ,WAAW,QAAQ,CAAC;AAC1C,oBAAQ,MAAM,QAAQ,cAAc,QAAQ,CAAC;AAG7C,gBAAI,QAAQ,IAAI,GAAG,MAAM,QAAW;AAClC,sBAAQ,IAAI,GAAG,IAAI;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAEA;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAGA,YAAY;AAQL,IAAM,UAAU,QAAQ,IAAI,UAC/B,QAAQ,QAAQ,IAAI,OAAO,IAC3B,QAAQ,QAAQ,GAAG,SAAS;AAKzB,IAAM,YAAY,KAAK,SAAS,OAAO;AACvC,IAAM,aAAa,KAAK,SAAS,QAAQ;AACzC,IAAM,aAAa,KAAK,SAAS,QAAQ;AACzC,IAAM,cAAc,KAAK,SAAS,SAAS;AAC3C,IAAM,eAAe,KAAK,SAAS,UAAU;AAMpD,SAAS,uBAA6B;AACpC,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,YAAQ,MAAM,2BAA2B,OAAO,EAAE;AAClD,YAAQ,MAAM,2DAA2D;AACzE,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAQ,MAAM,kCAAkC,SAAS,EAAE;AAC3D,YAAQ,MAAM,sCAAsC;AACpD,YAAQ,MAAM,uBAAuB,OAAO,EAAE;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAIA,qBAAqB;;;AD3FrB,eAAe,OAAO;AACpB,MAAI;
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\n/**\n * SessionEnd Hook - Captures session summary for UOCS\n *\n * Generates a session summary document when a Claude Code session ends,\n * documenting what was accomplished during the session.\n */\n\nimport { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';\n\ninterface SessionData {\n conversation_id: string;\n timestamp: string;\n [key: string]: any;\n}\n\nasync function main() {\n try {\n // Read input from stdin FIRST \u2014 this must complete before CC's abort signal fires.\n // Then fork the heavy work into a detached child so CC can't kill it.\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 || input.trim() === '') {\n process.exit(0);\n }\n\n // Fork: re-exec ourselves with --background flag and pipe the stdin data via env.\n // This detaches the heavy work (JSONL scan, IPC) from CC's abort signal.\n if (!process.env.__PAI_HOOK_BG) {\n const { spawn } = await import('child_process');\n const child = spawn(process.execPath, [process.argv[1], '--background'], {\n detached: true,\n stdio: 'ignore',\n env: { ...process.env, __PAI_HOOK_BG: '1', __PAI_HOOK_INPUT: input },\n });\n child.unref();\n process.exit(0); // Return immediately \u2014 CC sees success, abort signal is harmless\n }\n\n // Background mode: we're detached, safe from abort signals\n const data: SessionData = JSON.parse(process.env.__PAI_HOOK_INPUT || input);\n\n // Generate timestamp for filename\n const now = new Date();\n const timestamp = now.toISOString()\n .replace(/:/g, '')\n .replace(/\\..+/, '')\n .replace('T', '-'); // YYYY-MM-DD-HHMMSS\n\n const yearMonth = timestamp.substring(0, 7); // YYYY-MM\n\n // Try to extract session info from raw outputs\n const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);\n\n // Generate filename\n const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;\n\n // Ensure directory exists\n const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);\n if (!existsSync(sessionDir)) {\n mkdirSync(sessionDir, { recursive: true });\n }\n\n // Generate session document\n const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);\n\n // Write session file\n writeFileSync(join(sessionDir, filename), sessionDoc);\n\n // Also store structured summary via daemon IPC for the observations system\n await storeStructuredSummary(data.conversation_id, sessionInfo);\n\n // Exit successfully\n process.exit(0);\n } catch (error) {\n // Silent failure - don't disrupt workflow\n console.error(`[UOCS] SessionEnd hook error: ${error}`);\n process.exit(0);\n }\n}\n\nasync function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {\n // Try to read raw outputs for this session\n const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);\n\n let filesChanged: string[] = [];\n let commandsExecuted: string[] = [];\n let toolsUsed: Set<string> = new Set();\n\n try {\n if (existsSync(rawOutputsDir)) {\n // Only scan today's file \u2014 not the entire month (which can be 400MB+).\n // JSONL filenames are prefixed with YYYY-MM-DD.\n const todayPrefix = new Date().toISOString().substring(0, 10);\n const files = readdirSync(rawOutputsDir).filter(\n f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)\n );\n\n for (const file of files) {\n const filePath = join(rawOutputsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.session === conversationId) {\n toolsUsed.add(entry.tool);\n\n // Extract file changes\n if (entry.tool === 'Edit' || entry.tool === 'Write') {\n if (entry.input?.file_path) {\n filesChanged.push(entry.input.file_path);\n }\n }\n\n // Extract bash commands\n if (entry.tool === 'Bash' && entry.input?.command) {\n commandsExecuted.push(entry.input.command);\n }\n }\n } catch (e) {\n // Skip invalid JSON lines\n }\n }\n }\n }\n } catch (error) {\n // Silent failure\n }\n\n return {\n focus: 'general-work',\n filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10\n commandsExecuted: commandsExecuted.slice(0, 10), // Max 10\n toolsUsed: Array.from(toolsUsed),\n duration: 0 // Unknown\n };\n}\n\nfunction formatSessionDocument(timestamp: string, data: SessionData, info: any): string {\n const date = timestamp.substring(0, 10); // YYYY-MM-DD\n const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS\n const da = process.env.DA || 'PAI';\n\n return `---\ncapture_type: SESSION\ntimestamp: ${new Date().toISOString()}\nsession_id: ${data.conversation_id}\nduration_minutes: ${info.duration}\nexecutor: ${da}\n---\n\n# Session: ${info.focus}\n\n**Date:** ${date}\n**Time:** ${time}\n**Session ID:** ${data.conversation_id}\n\n---\n\n## Session Overview\n\n**Focus:** General development work\n**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}\n\n---\n\n## Tools Used\n\n${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\\n') : '- None recorded'}\n\n---\n\n## Files Modified\n\n${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \\`${f}\\``).join('\\n') : '- None recorded'}\n\n**Total Files Changed:** ${info.filesChanged.length}\n\n---\n\n## Commands Executed\n\n${info.commandsExecuted.length > 0 ? '```bash\\n' + info.commandsExecuted.join('\\n') + '\\n```' : 'None recorded'}\n\n---\n\n## Notes\n\nThis session summary was automatically generated by the UOCS SessionEnd hook.\n\nFor detailed tool outputs, see: \\`\\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\\`\n\n---\n\n**Session Outcome:** Completed\n**Generated:** ${new Date().toISOString()}\n`;\n}\n\nasync function storeStructuredSummary(\n sessionId: string,\n info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }\n): Promise<void> {\n try {\n const cwd = process.cwd();\n const net = await import('net');\n\n await new Promise<void>((resolve, _reject) => {\n const client = net.createConnection('/tmp/pai.sock', () => {\n const msg = JSON.stringify({\n id: 1,\n method: 'session_summary_store',\n params: {\n session_id: sessionId,\n cwd,\n request: null, // We don't have the original request\n investigated: null,\n learned: null,\n completed: info.filesChanged.length > 0\n ? `Modified ${info.filesChanged.length} file(s): ${info.filesChanged.slice(0, 5).join(', ')}`\n : null,\n next_steps: null,\n observation_count: 0, // Will be filled by daemon from actual count\n }\n }) + '\\n';\n client.write(msg);\n });\n\n client.on('data', () => { client.end(); resolve(); });\n client.on('error', () => resolve()); // Silent failure\n setTimeout(() => { client.destroy(); resolve(); }, 3000);\n });\n } catch {\n // Silent failure \u2014 don't disrupt session end\n }\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["existsSync", "readFileSync", "join", "join", "existsSync", "readFileSync", "resolve"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -19,7 +19,8 @@ interface SessionData {
|
|
|
19
19
|
|
|
20
20
|
async function main() {
|
|
21
21
|
try {
|
|
22
|
-
// Read input from stdin
|
|
22
|
+
// Read input from stdin FIRST — this must complete before CC's abort signal fires.
|
|
23
|
+
// Then fork the heavy work into a detached child so CC can't kill it.
|
|
23
24
|
const chunks: Buffer[] = [];
|
|
24
25
|
for await (const chunk of process.stdin) {
|
|
25
26
|
chunks.push(chunk);
|
|
@@ -29,7 +30,21 @@ async function main() {
|
|
|
29
30
|
process.exit(0);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Fork: re-exec ourselves with --background flag and pipe the stdin data via env.
|
|
34
|
+
// This detaches the heavy work (JSONL scan, IPC) from CC's abort signal.
|
|
35
|
+
if (!process.env.__PAI_HOOK_BG) {
|
|
36
|
+
const { spawn } = await import('child_process');
|
|
37
|
+
const child = spawn(process.execPath, [process.argv[1], '--background'], {
|
|
38
|
+
detached: true,
|
|
39
|
+
stdio: 'ignore',
|
|
40
|
+
env: { ...process.env, __PAI_HOOK_BG: '1', __PAI_HOOK_INPUT: input },
|
|
41
|
+
});
|
|
42
|
+
child.unref();
|
|
43
|
+
process.exit(0); // Return immediately — CC sees success, abort signal is harmless
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Background mode: we're detached, safe from abort signals
|
|
47
|
+
const data: SessionData = JSON.parse(process.env.__PAI_HOOK_INPUT || input);
|
|
33
48
|
|
|
34
49
|
// Generate timestamp for filename
|
|
35
50
|
const now = new Date();
|
|
@@ -80,7 +95,12 @@ async function analyzeSession(conversationId: string, yearMonth: string): Promis
|
|
|
80
95
|
|
|
81
96
|
try {
|
|
82
97
|
if (existsSync(rawOutputsDir)) {
|
|
83
|
-
|
|
98
|
+
// Only scan today's file — not the entire month (which can be 400MB+).
|
|
99
|
+
// JSONL filenames are prefixed with YYYY-MM-DD.
|
|
100
|
+
const todayPrefix = new Date().toISOString().substring(0, 10);
|
|
101
|
+
const files = readdirSync(rawOutputsDir).filter(
|
|
102
|
+
f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)
|
|
103
|
+
);
|
|
84
104
|
|
|
85
105
|
for (const file of files) {
|
|
86
106
|
const filePath = join(rawOutputsDir, file);
|