@tekmidian/pai 0.6.4 → 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.
- package/dist/cli/index.mjs +55 -37
- package/dist/cli/index.mjs.map +1 -1
- package/dist/hooks/capture-session-summary.mjs +15 -2
- package/dist/hooks/capture-session-summary.mjs.map +2 -2
- package/package.json +2 -2
- package/scripts/build-hooks.mjs +115 -2
- package/src/hooks/ts/session-end/capture-session-summary.ts +23 -3
- package/statusline-command.sh +6 -2
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekmidian/pai",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"pai-daemon-mcp": "dist/daemon-mcp/index.mjs"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
|
-
"build": "tsdown && node scripts/build-hooks.mjs && node scripts/build-skill-stubs.mjs --sync",
|
|
51
|
+
"build": "tsdown && node scripts/build-hooks.mjs --sync && node scripts/build-skill-stubs.mjs --sync",
|
|
52
52
|
"dev": "tsdown --watch",
|
|
53
53
|
"test": "vitest",
|
|
54
54
|
"lint": "tsc --noEmit",
|
package/scripts/build-hooks.mjs
CHANGED
|
@@ -4,14 +4,33 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Each hook is fully self-contained — lib/ dependencies are inlined.
|
|
6
6
|
* Output: dist/hooks/<name>.mjs with #!/usr/bin/env node shebang.
|
|
7
|
+
*
|
|
8
|
+
* With --sync: also creates/updates symlinks (or copies on Windows) from
|
|
9
|
+
* ~/.claude/Hooks/ and ~/.claude/ to the built/source files. This ensures
|
|
10
|
+
* that `bun run build` is the only step needed to deploy hook updates.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { buildSync } from "esbuild";
|
|
10
|
-
import {
|
|
11
|
-
|
|
14
|
+
import {
|
|
15
|
+
readdirSync,
|
|
16
|
+
statSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
symlinkSync,
|
|
21
|
+
lstatSync,
|
|
22
|
+
readlinkSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
copyFileSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "fs";
|
|
28
|
+
import { join, resolve, basename } from "path";
|
|
29
|
+
import { homedir, platform } from "os";
|
|
12
30
|
|
|
13
31
|
const HOOKS_SRC = "src/hooks/ts";
|
|
14
32
|
const HOOKS_OUT = "dist/hooks";
|
|
33
|
+
const doSync = process.argv.includes("--sync");
|
|
15
34
|
|
|
16
35
|
// Collect all .ts entry points (skip lib/ — those are bundled into each hook)
|
|
17
36
|
function collectEntryPoints(dir) {
|
|
@@ -49,3 +68,97 @@ for (const entry of entryPoints) {
|
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
console.log(`✔ ${entryPoints.length} hooks built to ${HOOKS_OUT}/`);
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// --sync: Symlink (or copy on Windows) all deployable files to ~/.claude/
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
if (doSync) {
|
|
77
|
+
const useSymlinks = platform() !== "win32";
|
|
78
|
+
const claudeDir = join(homedir(), ".claude");
|
|
79
|
+
const hooksTarget = join(claudeDir, "Hooks");
|
|
80
|
+
mkdirSync(hooksTarget, { recursive: true });
|
|
81
|
+
|
|
82
|
+
let created = 0;
|
|
83
|
+
let updated = 0;
|
|
84
|
+
let current = 0;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ensure `target` is a symlink (or copy on Windows) pointing to `source`.
|
|
88
|
+
* Replaces stale symlinks and plain-file copies with correct symlinks.
|
|
89
|
+
* Never overwrites non-symlink, non-PAI files (user's own scripts).
|
|
90
|
+
*/
|
|
91
|
+
function syncFile(source, target) {
|
|
92
|
+
const absSource = resolve(source);
|
|
93
|
+
|
|
94
|
+
if (!existsSync(absSource)) {
|
|
95
|
+
console.warn(` ⚠ Source not found: ${source}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check existing target (lstat doesn't follow symlinks)
|
|
100
|
+
let isUpdate = false;
|
|
101
|
+
try {
|
|
102
|
+
const stat = lstatSync(target);
|
|
103
|
+
if (stat.isSymbolicLink()) {
|
|
104
|
+
if (resolve(readlinkSync(target)) === absSource) {
|
|
105
|
+
current++;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
unlinkSync(target);
|
|
109
|
+
isUpdate = true;
|
|
110
|
+
} else if (stat.isFile()) {
|
|
111
|
+
unlinkSync(target);
|
|
112
|
+
isUpdate = true;
|
|
113
|
+
} else {
|
|
114
|
+
return; // Directory or something unexpected — don't touch
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Target doesn't exist — fresh install
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (useSymlinks) {
|
|
121
|
+
symlinkSync(absSource, target);
|
|
122
|
+
} else {
|
|
123
|
+
copyFileSync(absSource, target);
|
|
124
|
+
chmodSync(target, 0o755);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isUpdate) {
|
|
128
|
+
updated++;
|
|
129
|
+
} else {
|
|
130
|
+
created++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. TypeScript hooks: dist/hooks/*.mjs → ~/.claude/Hooks/*.mjs
|
|
135
|
+
const mjsFiles = readdirSync(HOOKS_OUT).filter((f) => f.endsWith(".mjs"));
|
|
136
|
+
for (const filename of mjsFiles) {
|
|
137
|
+
syncFile(join(HOOKS_OUT, filename), join(hooksTarget, filename));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 2. Shell hooks: src/hooks/*.sh → ~/.claude/Hooks/pai-*.sh
|
|
141
|
+
const shellHooks = [
|
|
142
|
+
["src/hooks/pre-compact.sh", "pai-pre-compact.sh"],
|
|
143
|
+
["src/hooks/session-stop.sh", "pai-session-stop.sh"],
|
|
144
|
+
];
|
|
145
|
+
for (const [src, destName] of shellHooks) {
|
|
146
|
+
if (existsSync(src)) {
|
|
147
|
+
syncFile(src, join(hooksTarget, destName));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Root scripts: statusline + tab-color → ~/.claude/
|
|
152
|
+
const rootScripts = ["statusline-command.sh", "tab-color-command.sh"];
|
|
153
|
+
for (const script of rootScripts) {
|
|
154
|
+
if (existsSync(script)) {
|
|
155
|
+
syncFile(script, join(claudeDir, script));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parts = [];
|
|
160
|
+
if (created > 0) parts.push(`${created} created`);
|
|
161
|
+
if (updated > 0) parts.push(`${updated} updated`);
|
|
162
|
+
if (current > 0) parts.push(`${current} current`);
|
|
163
|
+
console.log(`✔ Hook symlinks synced: ${parts.join(", ")}`);
|
|
164
|
+
}
|
|
@@ -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);
|
package/statusline-command.sh
CHANGED
|
@@ -406,11 +406,15 @@ if [ -f "$usage_cache" ]; then
|
|
|
406
406
|
elapsed_secs=$((window_secs - remaining_secs))
|
|
407
407
|
# Expected usage if spending linearly: elapsed/total * 100
|
|
408
408
|
expected_pct=$(( elapsed_secs * 100 / window_secs ))
|
|
409
|
-
# Daily pace: actual spend
|
|
409
|
+
# Daily pace: actual spend/day vs dynamic budget
|
|
410
|
+
# Budget = remaining capacity / remaining days (not static 100/7)
|
|
410
411
|
elapsed_days_x10=$((elapsed_secs * 10 / 86400))
|
|
411
412
|
[ "$elapsed_days_x10" -lt 1 ] && elapsed_days_x10=1
|
|
412
413
|
spend_per_day=$((seven_day_int * 10 / elapsed_days_x10))
|
|
413
|
-
|
|
414
|
+
remaining_days_x10=$((remaining_secs * 10 / 86400))
|
|
415
|
+
[ "$remaining_days_x10" -lt 1 ] && remaining_days_x10=1
|
|
416
|
+
remaining_budget=$((100 - seven_day_int))
|
|
417
|
+
budget_per_day=$((remaining_budget * 10 / remaining_days_x10))
|
|
414
418
|
# Color: green = under budget, orange = near budget, red = over budget
|
|
415
419
|
overspend=$((spend_per_day - budget_per_day))
|
|
416
420
|
if [ "$overspend" -le -3 ] 2>/dev/null; then
|