@tekmidian/pai 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/ARCHITECTURE.md +16 -10
  2. package/README.md +46 -6
  3. package/dist/{auto-route-JjW3f7pV.mjs → auto-route-B5MSUJZK.mjs} +3 -3
  4. package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-B5MSUJZK.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +313 -43
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/{config-DELNqq3Z.mjs → config-B4brrHHE.mjs} +1 -1
  8. package/dist/{config-DELNqq3Z.mjs.map → config-B4brrHHE.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +7 -7
  10. package/dist/daemon-mcp/index.mjs +11 -4
  11. package/dist/daemon-mcp/index.mjs.map +1 -1
  12. package/dist/{daemon-CeTX4NpF.mjs → daemon-s868Paua.mjs} +12 -12
  13. package/dist/{daemon-CeTX4NpF.mjs.map → daemon-s868Paua.mjs.map} +1 -1
  14. package/dist/{detect-D7gPV3fQ.mjs → detect-CdaA48EI.mjs} +1 -1
  15. package/dist/{detect-D7gPV3fQ.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  16. package/dist/{detector-cYYhK2Mi.mjs → detector-Bp-2SM3x.mjs} +2 -2
  17. package/dist/{detector-cYYhK2Mi.mjs.map → detector-Bp-2SM3x.mjs.map} +1 -1
  18. package/dist/{factory-DZLvRf4m.mjs → factory-CeXQzlwn.mjs} +3 -3
  19. package/dist/{factory-DZLvRf4m.mjs.map → factory-CeXQzlwn.mjs.map} +1 -1
  20. package/dist/hooks/capture-all-events.mjs +238 -0
  21. package/dist/hooks/capture-all-events.mjs.map +7 -0
  22. package/dist/hooks/capture-session-summary.mjs +198 -0
  23. package/dist/hooks/capture-session-summary.mjs.map +7 -0
  24. package/dist/hooks/capture-tool-output.mjs +105 -0
  25. package/dist/hooks/capture-tool-output.mjs.map +7 -0
  26. package/dist/hooks/cleanup-session-files.mjs +129 -0
  27. package/dist/hooks/cleanup-session-files.mjs.map +7 -0
  28. package/dist/hooks/context-compression-hook.mjs +283 -0
  29. package/dist/hooks/context-compression-hook.mjs.map +7 -0
  30. package/dist/hooks/initialize-session.mjs +206 -0
  31. package/dist/hooks/initialize-session.mjs.map +7 -0
  32. package/dist/hooks/load-core-context.mjs +110 -0
  33. package/dist/hooks/load-core-context.mjs.map +7 -0
  34. package/dist/hooks/load-project-context.mjs +548 -0
  35. package/dist/hooks/load-project-context.mjs.map +7 -0
  36. package/dist/hooks/security-validator.mjs +159 -0
  37. package/dist/hooks/security-validator.mjs.map +7 -0
  38. package/dist/hooks/stop-hook.mjs +625 -0
  39. package/dist/hooks/stop-hook.mjs.map +7 -0
  40. package/dist/hooks/subagent-stop-hook.mjs +152 -0
  41. package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
  42. package/dist/hooks/sync-todo-to-md.mjs +322 -0
  43. package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
  44. package/dist/hooks/update-tab-on-action.mjs +90 -0
  45. package/dist/hooks/update-tab-on-action.mjs.map +7 -0
  46. package/dist/hooks/update-tab-titles.mjs +55 -0
  47. package/dist/hooks/update-tab-titles.mjs.map +7 -0
  48. package/dist/index.d.mts +29 -1
  49. package/dist/index.d.mts.map +1 -1
  50. package/dist/index.mjs +4 -3
  51. package/dist/{indexer-backend-BHztlJJg.mjs → indexer-backend-DQO-FqAI.mjs} +1 -1
  52. package/dist/{indexer-backend-BHztlJJg.mjs.map → indexer-backend-DQO-FqAI.mjs.map} +1 -1
  53. package/dist/{ipc-client-CLt2fNlC.mjs → ipc-client-CgSpwHDC.mjs} +1 -1
  54. package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-CgSpwHDC.mjs.map} +1 -1
  55. package/dist/mcp/index.mjs +15 -5
  56. package/dist/mcp/index.mjs.map +1 -1
  57. package/dist/{postgres-CRBe30Ag.mjs → postgres-CIxeqf_n.mjs} +1 -1
  58. package/dist/{postgres-CRBe30Ag.mjs.map → postgres-CIxeqf_n.mjs.map} +1 -1
  59. package/dist/reranker-D7bRAHi6.mjs +71 -0
  60. package/dist/reranker-D7bRAHi6.mjs.map +1 -0
  61. package/dist/{schemas-BY3Pjvje.mjs → schemas-BFIgGntb.mjs} +1 -1
  62. package/dist/{schemas-BY3Pjvje.mjs.map → schemas-BFIgGntb.mjs.map} +1 -1
  63. package/dist/{search-GK0ibTJy.mjs → search-_oHfguA5.mjs} +47 -4
  64. package/dist/search-_oHfguA5.mjs.map +1 -0
  65. package/dist/{sqlite-RyR8Up1v.mjs → sqlite-CymLKiDE.mjs} +2 -2
  66. package/dist/{sqlite-RyR8Up1v.mjs.map → sqlite-CymLKiDE.mjs.map} +1 -1
  67. package/dist/{tools-CUg0Lyg-.mjs → tools-Dx7GjOHd.mjs} +23 -14
  68. package/dist/tools-Dx7GjOHd.mjs.map +1 -0
  69. package/dist/{vault-indexer-Bo2aPSzP.mjs → vault-indexer-DXWs9pDn.mjs} +1 -1
  70. package/dist/{vault-indexer-Bo2aPSzP.mjs.map → vault-indexer-DXWs9pDn.mjs.map} +1 -1
  71. package/dist/{zettelkasten-Co-w0XSZ.mjs → zettelkasten-e-a4rW_6.mjs} +2 -2
  72. package/dist/{zettelkasten-Co-w0XSZ.mjs.map → zettelkasten-e-a4rW_6.mjs.map} +1 -1
  73. package/package.json +4 -2
  74. package/scripts/build-hooks.mjs +51 -0
  75. package/src/hooks/ts/capture-all-events.ts +179 -0
  76. package/src/hooks/ts/lib/detect-environment.ts +53 -0
  77. package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
  78. package/src/hooks/ts/lib/pai-paths.ts +124 -0
  79. package/src/hooks/ts/lib/project-utils.ts +914 -0
  80. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
  81. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
  82. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
  83. package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
  84. package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
  85. package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
  86. package/src/hooks/ts/session-start/initialize-session.ts +155 -0
  87. package/src/hooks/ts/session-start/load-core-context.ts +104 -0
  88. package/src/hooks/ts/session-start/load-project-context.ts +394 -0
  89. package/src/hooks/ts/stop/stop-hook.ts +407 -0
  90. package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
  91. package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
  92. package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
  93. package/tab-color-command.sh +24 -0
  94. package/templates/skills/createskill-skill.template.md +78 -0
  95. package/templates/skills/history-system.template.md +371 -0
  96. package/templates/skills/hook-system.template.md +913 -0
  97. package/templates/skills/sessions-skill.template.md +102 -0
  98. package/templates/skills/skill-system.template.md +214 -0
  99. package/templates/skills/terminal-tabs.template.md +120 -0
  100. package/dist/search-GK0ibTJy.mjs.map +0 -1
  101. package/dist/tools-CUg0Lyg-.mjs.map +0 -1
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * security-validator.ts - PreToolUse Security Validation Hook
5
+ *
6
+ * Fast pattern-based security validation for Bash commands.
7
+ * Blocks commands matching known attack patterns before execution.
8
+ *
9
+ * Design Principles:
10
+ * - Fast path: Most commands allowed with minimal processing
11
+ * - Pre-compiled regex patterns at module load
12
+ * - Only log/block on high-confidence attack detection
13
+ * - Fail open on errors (don't break legitimate work)
14
+ *
15
+ * CUSTOMIZATION REQUIRED:
16
+ * This template includes basic examples. Add your own security patterns
17
+ * based on your threat model and environment.
18
+ */
19
+
20
+ import { appendFileSync, mkdirSync, existsSync } from 'fs';
21
+ import { dirname } from 'path';
22
+
23
+ // ============================================================================
24
+ // ATTACK PATTERNS - CUSTOMIZE THESE FOR YOUR ENVIRONMENT
25
+ // ============================================================================
26
+
27
+ // Example: Reverse Shell Patterns (BLOCK - rarely legitimate)
28
+ const REVERSE_SHELL_PATTERNS: RegExp[] = [
29
+ /\/dev\/(tcp|udp)\/[0-9]/, // Bash TCP/UDP device
30
+ /bash\s+-i\s+>&?\s*\/dev\//, // Interactive bash redirect
31
+ // Add your own reverse shell patterns here
32
+ ];
33
+
34
+ // Example: Instruction Override (BLOCK - prompt injection)
35
+ const INSTRUCTION_OVERRIDE_PATTERNS: RegExp[] = [
36
+ /ignore\s+(all\s+)?previous\s+instructions?/i,
37
+ /disregard\s+(all\s+)?(prior|previous)\s+(instructions?|rules?)/i,
38
+ // Add your own prompt injection patterns here
39
+ ];
40
+
41
+ // Example: Catastrophic Deletion Patterns (BLOCK - filesystem destruction)
42
+ const CATASTROPHIC_DELETION_PATTERNS: RegExp[] = [
43
+ // Trailing tilde bypass
44
+ /\s+~\/?(\s*$|\s+)/, // Space then ~/ at end
45
+ /\brm\s+(-[rfivd]+\s+)*\S+\s+~\/?/, // rm something ~/
46
+
47
+ // Relative path recursive deletion
48
+ /\brm\s+(-[rfivd]+\s+)*\.\/\s*$/, // rm -rf ./
49
+ /\brm\s+(-[rfivd]+\s+)*\.\.\/\s*$/, // rm -rf ../
50
+
51
+ // Add your own dangerous deletion patterns here
52
+ ];
53
+
54
+ // Example: Dangerous File Operations (BLOCK - data destruction)
55
+ const DANGEROUS_FILE_OPS_PATTERNS: RegExp[] = [
56
+ /\bchmod\s+(-R\s+)?0{3,}/, // chmod 000
57
+ // Add your own dangerous file operation patterns here
58
+ ];
59
+
60
+ // OPTIONAL: Operations that require confirmation instead of blocking
61
+ const DANGEROUS_GIT_PATTERNS: RegExp[] = [
62
+ /\bgit\s+push\s+.*(-f\b|--force)/i, // git push --force
63
+ /\bgit\s+reset\s+--hard/i, // git reset --hard
64
+ // Add your own git safety patterns here
65
+ ];
66
+
67
+ // Combined patterns for fast iteration
68
+ const ALL_BLOCK_PATTERNS: { category: string; patterns: RegExp[] }[] = [
69
+ { category: 'reverse_shell', patterns: REVERSE_SHELL_PATTERNS },
70
+ { category: 'instruction_override', patterns: INSTRUCTION_OVERRIDE_PATTERNS },
71
+ { category: 'catastrophic_deletion', patterns: CATASTROPHIC_DELETION_PATTERNS },
72
+ { category: 'dangerous_file_ops', patterns: DANGEROUS_FILE_OPS_PATTERNS },
73
+ ];
74
+
75
+ const CONFIRM_PATTERNS: { category: string; patterns: RegExp[] }[] = [
76
+ { category: 'dangerous_git', patterns: DANGEROUS_GIT_PATTERNS },
77
+ ];
78
+
79
+ // ============================================================================
80
+ // TYPES
81
+ // ============================================================================
82
+
83
+ interface HookInput {
84
+ session_id: string;
85
+ tool_name: string;
86
+ tool_input: Record<string, unknown> | string;
87
+ }
88
+
89
+ interface HookOutput {
90
+ permissionDecision: 'allow' | 'deny';
91
+ additionalContext?: string;
92
+ feedback?: string;
93
+ }
94
+
95
+ // ============================================================================
96
+ // DETECTION LOGIC
97
+ // ============================================================================
98
+
99
+ interface DetectionResult {
100
+ blocked: boolean;
101
+ requiresConfirmation?: boolean;
102
+ category?: string;
103
+ pattern?: string;
104
+ }
105
+
106
+ function detectAttack(content: string): DetectionResult {
107
+ // First check for hard blocks
108
+ for (const { category, patterns } of ALL_BLOCK_PATTERNS) {
109
+ for (const pattern of patterns) {
110
+ if (pattern.test(content)) {
111
+ return { blocked: true, category, pattern: pattern.source };
112
+ }
113
+ }
114
+ }
115
+
116
+ // Then check for confirmation-required patterns
117
+ for (const { category, patterns } of CONFIRM_PATTERNS) {
118
+ for (const pattern of patterns) {
119
+ if (pattern.test(content)) {
120
+ return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };
121
+ }
122
+ }
123
+ }
124
+
125
+ return { blocked: false };
126
+ }
127
+
128
+ // ============================================================================
129
+ // ASYNC LOGGING (fire-and-forget on block only)
130
+ // ============================================================================
131
+
132
+ function logSecurityEvent(event: Record<string, unknown>): void {
133
+ const logPath = `${process.env.PAI_DIR || '~/.claude'}/history/security/security-events.jsonl`;
134
+ const entry = JSON.stringify({ timestamp: new Date().toISOString(), ...event }) + '\n';
135
+
136
+ try {
137
+ const dir = dirname(logPath);
138
+ if (!existsSync(dir)) {
139
+ mkdirSync(dir, { recursive: true });
140
+ }
141
+ appendFileSync(logPath, entry);
142
+ } catch {
143
+ // Silently fail - logging should never break the hook
144
+ }
145
+ }
146
+
147
+ // ============================================================================
148
+ // MAIN HOOK LOGIC
149
+ // ============================================================================
150
+
151
+ async function main(): Promise<void> {
152
+ let input: HookInput;
153
+
154
+ try {
155
+ // Read stdin
156
+ const chunks: Buffer[] = [];
157
+ const timeoutPromise = new Promise<Buffer[]>((_, reject) =>
158
+ setTimeout(() => reject(new Error('timeout')), 100)
159
+ );
160
+ const readPromise = (async () => {
161
+ for await (const chunk of process.stdin) {
162
+ chunks.push(chunk);
163
+ }
164
+ return chunks;
165
+ })();
166
+
167
+ let text = '';
168
+ try {
169
+ const result = await Promise.race([readPromise, timeoutPromise]);
170
+ text = Buffer.concat(result).toString('utf-8');
171
+ } catch {
172
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
173
+ return;
174
+ }
175
+
176
+ if (!text.trim()) {
177
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
178
+ return;
179
+ }
180
+
181
+ input = JSON.parse(text);
182
+ } catch {
183
+ // Parse error or timeout - fail open
184
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
185
+ return;
186
+ }
187
+
188
+ // Only validate Bash commands
189
+ if (input.tool_name !== 'Bash') {
190
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
191
+ return;
192
+ }
193
+
194
+ // Extract command string
195
+ const command = typeof input.tool_input === 'string'
196
+ ? input.tool_input
197
+ : (input.tool_input?.command as string) || '';
198
+
199
+ if (!command) {
200
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
201
+ return;
202
+ }
203
+
204
+ // Check all patterns
205
+ const result = detectAttack(command);
206
+
207
+ if (result.blocked) {
208
+ // Log and block
209
+ logSecurityEvent({
210
+ type: 'attack_blocked',
211
+ category: result.category,
212
+ pattern: result.pattern,
213
+ command: command.slice(0, 200), // Truncate for log
214
+ session_id: input.session_id,
215
+ });
216
+
217
+ const output: HookOutput = {
218
+ permissionDecision: 'deny',
219
+ additionalContext: `SECURITY: Blocked ${result.category} pattern`,
220
+ feedback: `This command matched a security pattern (${result.category}). If this is legitimate, please rephrase the command.`,
221
+ };
222
+
223
+ console.log(JSON.stringify(output));
224
+ process.exit(2); // Exit 2 = blocking error
225
+ }
226
+
227
+ if (result.requiresConfirmation) {
228
+ // Log warning and require confirmation
229
+ logSecurityEvent({
230
+ type: 'confirmation_required',
231
+ category: result.category,
232
+ pattern: result.pattern,
233
+ command: command.slice(0, 200),
234
+ session_id: input.session_id,
235
+ });
236
+
237
+ const output: HookOutput = {
238
+ permissionDecision: 'deny',
239
+ additionalContext: `DANGEROUS: ${result.category} operation requires confirmation`,
240
+ feedback: `This is a dangerous operation (${command.slice(0, 50)}...). This can cause data loss. If you're sure, explicitly confirm this command.`,
241
+ };
242
+
243
+ console.log(JSON.stringify(output));
244
+ process.exit(2); // Exit 2 = requires user confirmation
245
+ }
246
+
247
+ // Allow - no logging, immediate exit
248
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
249
+ }
250
+
251
+ // ============================================================================
252
+ // RUN
253
+ // ============================================================================
254
+
255
+ main().catch(() => {
256
+ // On any error, fail open
257
+ console.log(JSON.stringify({ permissionDecision: 'allow' }));
258
+ });
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SessionEnd Hook - Captures session summary for UOCS
5
+ *
6
+ * Generates a session summary document when a Claude Code session ends,
7
+ * documenting what was accomplished during the session.
8
+ */
9
+
10
+ import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';
13
+
14
+ interface SessionData {
15
+ conversation_id: string;
16
+ timestamp: string;
17
+ [key: string]: any;
18
+ }
19
+
20
+ async function main() {
21
+ try {
22
+ // Read input from stdin
23
+ const chunks: Buffer[] = [];
24
+ for await (const chunk of process.stdin) {
25
+ chunks.push(chunk);
26
+ }
27
+ const input = Buffer.concat(chunks).toString('utf-8');
28
+ if (!input || input.trim() === '') {
29
+ process.exit(0);
30
+ }
31
+
32
+ const data: SessionData = JSON.parse(input);
33
+
34
+ // Generate timestamp for filename
35
+ const now = new Date();
36
+ const timestamp = now.toISOString()
37
+ .replace(/:/g, '')
38
+ .replace(/\..+/, '')
39
+ .replace('T', '-'); // YYYY-MM-DD-HHMMSS
40
+
41
+ const yearMonth = timestamp.substring(0, 7); // YYYY-MM
42
+
43
+ // Try to extract session info from raw outputs
44
+ const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);
45
+
46
+ // Generate filename
47
+ const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;
48
+
49
+ // Ensure directory exists
50
+ const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);
51
+ if (!existsSync(sessionDir)) {
52
+ mkdirSync(sessionDir, { recursive: true });
53
+ }
54
+
55
+ // Generate session document
56
+ const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);
57
+
58
+ // Write session file
59
+ writeFileSync(join(sessionDir, filename), sessionDoc);
60
+
61
+ // Exit successfully
62
+ process.exit(0);
63
+ } catch (error) {
64
+ // Silent failure - don't disrupt workflow
65
+ console.error(`[UOCS] SessionEnd hook error: ${error}`);
66
+ process.exit(0);
67
+ }
68
+ }
69
+
70
+ async function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {
71
+ // Try to read raw outputs for this session
72
+ const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);
73
+
74
+ let filesChanged: string[] = [];
75
+ let commandsExecuted: string[] = [];
76
+ let toolsUsed: Set<string> = new Set();
77
+
78
+ try {
79
+ if (existsSync(rawOutputsDir)) {
80
+ const files = readdirSync(rawOutputsDir).filter(f => f.endsWith('.jsonl'));
81
+
82
+ for (const file of files) {
83
+ const filePath = join(rawOutputsDir, file);
84
+ const content = readFileSync(filePath, 'utf-8');
85
+ const lines = content.split('\n').filter(l => l.trim());
86
+
87
+ for (const line of lines) {
88
+ try {
89
+ const entry = JSON.parse(line);
90
+ if (entry.session === conversationId) {
91
+ toolsUsed.add(entry.tool);
92
+
93
+ // Extract file changes
94
+ if (entry.tool === 'Edit' || entry.tool === 'Write') {
95
+ if (entry.input?.file_path) {
96
+ filesChanged.push(entry.input.file_path);
97
+ }
98
+ }
99
+
100
+ // Extract bash commands
101
+ if (entry.tool === 'Bash' && entry.input?.command) {
102
+ commandsExecuted.push(entry.input.command);
103
+ }
104
+ }
105
+ } catch (e) {
106
+ // Skip invalid JSON lines
107
+ }
108
+ }
109
+ }
110
+ }
111
+ } catch (error) {
112
+ // Silent failure
113
+ }
114
+
115
+ return {
116
+ focus: 'general-work',
117
+ filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10
118
+ commandsExecuted: commandsExecuted.slice(0, 10), // Max 10
119
+ toolsUsed: Array.from(toolsUsed),
120
+ duration: 0 // Unknown
121
+ };
122
+ }
123
+
124
+ function formatSessionDocument(timestamp: string, data: SessionData, info: any): string {
125
+ const date = timestamp.substring(0, 10); // YYYY-MM-DD
126
+ const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS
127
+ const da = process.env.DA || 'PAI';
128
+
129
+ return `---
130
+ capture_type: SESSION
131
+ timestamp: ${new Date().toISOString()}
132
+ session_id: ${data.conversation_id}
133
+ duration_minutes: ${info.duration}
134
+ executor: ${da}
135
+ ---
136
+
137
+ # Session: ${info.focus}
138
+
139
+ **Date:** ${date}
140
+ **Time:** ${time}
141
+ **Session ID:** ${data.conversation_id}
142
+
143
+ ---
144
+
145
+ ## Session Overview
146
+
147
+ **Focus:** General development work
148
+ **Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}
149
+
150
+ ---
151
+
152
+ ## Tools Used
153
+
154
+ ${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\n') : '- None recorded'}
155
+
156
+ ---
157
+
158
+ ## Files Modified
159
+
160
+ ${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \`${f}\``).join('\n') : '- None recorded'}
161
+
162
+ **Total Files Changed:** ${info.filesChanged.length}
163
+
164
+ ---
165
+
166
+ ## Commands Executed
167
+
168
+ ${info.commandsExecuted.length > 0 ? '```bash\n' + info.commandsExecuted.join('\n') + '\n```' : 'None recorded'}
169
+
170
+ ---
171
+
172
+ ## Notes
173
+
174
+ This session summary was automatically generated by the UOCS SessionEnd hook.
175
+
176
+ For detailed tool outputs, see: \`\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\`
177
+
178
+ ---
179
+
180
+ **Session Outcome:** Completed
181
+ **Generated:** ${new Date().toISOString()}
182
+ `;
183
+ }
184
+
185
+ main();
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * initialize-session.ts
5
+ *
6
+ * Main session initialization hook that runs at the start of every Claude Code session.
7
+ *
8
+ * What it does:
9
+ * - Checks if this is a subagent session (skips for subagents)
10
+ * - Tests that stop-hook is properly configured
11
+ * - Sets initial terminal tab title
12
+ * - Sends ntfy.sh notification for global PAI system initialization
13
+ *
14
+ * Setup:
15
+ * 1. Set environment variables in settings.json:
16
+ * - DA: Your AI's name (e.g., "Kai", "Nova", "Assistant")
17
+ * - PAI_DIR: Path to your PAI directory (defaults to $HOME/.claude)
18
+ * 2. Ensure load-core-context.ts exists in hooks/session-start/ directory
19
+ * 3. Add all three SessionStart hooks to settings.json in this order:
20
+ * initialize-session.ts, load-core-context.ts, load-project-context.ts
21
+ */
22
+
23
+ import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
24
+ import { join } from 'path';
25
+ import { tmpdir } from 'os';
26
+ import { PAI_DIR } from '../lib/pai-paths';
27
+ import { sendNtfyNotification, isWhatsAppEnabled } from '../lib/project-utils';
28
+
29
+ // Debounce duration in milliseconds (prevents duplicate SessionStart events)
30
+ const DEBOUNCE_MS = 2000;
31
+ const LOCKFILE = join(tmpdir(), 'pai-session-start.lock');
32
+
33
+ /**
34
+ * Check if we're within the debounce window to prevent duplicate notifications
35
+ * from the IDE firing multiple SessionStart events
36
+ */
37
+ function shouldDebounce(): boolean {
38
+ try {
39
+ if (existsSync(LOCKFILE)) {
40
+ const lockContent = readFileSync(LOCKFILE, 'utf-8');
41
+ const lockTime = parseInt(lockContent, 10);
42
+ const now = Date.now();
43
+
44
+ if (now - lockTime < DEBOUNCE_MS) {
45
+ // Within debounce window, skip this notification
46
+ return true;
47
+ }
48
+ }
49
+
50
+ // Update lockfile with current timestamp
51
+ writeFileSync(LOCKFILE, Date.now().toString());
52
+ return false;
53
+ } catch (error) {
54
+ // If any error, just proceed (don't break session start)
55
+ try {
56
+ writeFileSync(LOCKFILE, Date.now().toString());
57
+ } catch {}
58
+ return false;
59
+ }
60
+ }
61
+
62
+ async function testStopHook() {
63
+ const stopHookPath = join(PAI_DIR, 'hooks/stop-hook.ts');
64
+
65
+ console.error('\nTesting stop-hook configuration...');
66
+
67
+ // Check if stop-hook exists
68
+ if (!existsSync(stopHookPath)) {
69
+ console.error('Stop-hook NOT FOUND at:', stopHookPath);
70
+ return false;
71
+ }
72
+
73
+ // Check if stop-hook is executable
74
+ try {
75
+ const stats = statSync(stopHookPath);
76
+ const isExecutable = (stats.mode & 0o111) !== 0;
77
+
78
+ if (!isExecutable) {
79
+ console.error('Stop-hook exists but is NOT EXECUTABLE');
80
+ return false;
81
+ }
82
+
83
+ console.error('Stop-hook found and is executable');
84
+
85
+ // Set initial tab title (customize with your AI's name via DA env var)
86
+ const daName = process.env.DA || 'AI Assistant';
87
+ const tabTitle = `${daName} Ready`;
88
+
89
+ process.stderr.write(`\x1b]0;${tabTitle}\x07`);
90
+ process.stderr.write(`\x1b]2;${tabTitle}\x07`);
91
+ process.stderr.write(`\x1b]30;${tabTitle}\x07`);
92
+ console.error(`Set initial tab title: "${tabTitle}"`);
93
+
94
+ return true;
95
+ } catch (e) {
96
+ console.error('Error checking stop-hook:', e);
97
+ return false;
98
+ }
99
+ }
100
+
101
+ async function main() {
102
+ try {
103
+ // Check if this is a subagent session - if so, exit silently
104
+ const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
105
+ const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||
106
+ process.env.CLAUDE_AGENT_TYPE !== undefined;
107
+
108
+ if (isSubagent) {
109
+ // This is a subagent session - exit silently without notification
110
+ console.error('Subagent session detected - skipping session initialization');
111
+ process.exit(0);
112
+ }
113
+
114
+ // Check debounce to prevent duplicate notifications
115
+ // (IDE extension can fire multiple SessionStart events)
116
+ if (shouldDebounce()) {
117
+ console.error('Debouncing duplicate SessionStart event');
118
+ process.exit(0);
119
+ }
120
+
121
+ // Check if WhatsApp (Whazaa) is configured as enabled MCP server
122
+ // Detection is config-based: reads enabledMcpjsonServers from settings.json
123
+ // No flag files — uses standard ~/.claude/settings.json
124
+ if (isWhatsAppEnabled()) {
125
+ console.error('WhatsApp (Whazaa) enabled in MCP config');
126
+ console.log(`<system-reminder>
127
+ WHATSAPP MODE ACTIVE — Whazaa MCP server is enabled. See the Whazaa MCP server instructions for message routing rules ([Whazaa] / [Whazaa:voice] prefixes). ntfy.sh is automatically skipped.
128
+ </system-reminder>`);
129
+ }
130
+
131
+ // Test stop-hook first (only for main sessions)
132
+ const stopHookOk = await testStopHook();
133
+
134
+ const daName = process.env.DA || 'AI Assistant';
135
+
136
+ if (!stopHookOk) {
137
+ console.error('\nSTOP-HOOK ISSUE DETECTED - Tab titles may not update automatically');
138
+ }
139
+
140
+ // Note: PAI core context loading is handled by load-core-context.ts hook
141
+ // which should run AFTER this hook in settings.json SessionStart hooks
142
+
143
+ // Send ntfy.sh notification (MANDATORY - never skip this)
144
+ // Note: load-project-context.ts also sends a project-specific notification
145
+ // This one is for the global PAI system initialization
146
+ await sendNtfyNotification(`${daName} ready`);
147
+
148
+ process.exit(0);
149
+ } catch (error) {
150
+ console.error('SessionStart hook error:', error);
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ main();
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * load-core-context.ts
5
+ *
6
+ * Automatically loads your CORE skill context at session start by reading and injecting
7
+ * the CORE SKILL.md file contents directly into Claude's context as a system-reminder.
8
+ *
9
+ * Purpose:
10
+ * - Read CORE SKILL.md file content
11
+ * - Output content as system-reminder for Claude to process
12
+ * - Ensure complete context (contacts, preferences, security, identity) available at session start
13
+ * - Bypass skill activation logic by directly injecting context
14
+ *
15
+ * Setup:
16
+ * 1. Customize your ${PAI_DIR}/skills/CORE/SKILL.md with your personal context
17
+ * 2. Add this hook to settings.json SessionStart hooks
18
+ * 3. Ensure PAI_DIR environment variable is set (defaults to $HOME/.claude)
19
+ *
20
+ * How it works:
21
+ * - Runs at the start of every Claude Code session
22
+ * - Skips execution for subagent sessions (they don't need CORE context)
23
+ * - Reads your CORE SKILL.md file
24
+ * - Injects content as <system-reminder> which Claude processes automatically
25
+ * - Gives your AI immediate access to your complete personal context
26
+ */
27
+
28
+ import { readFileSync, existsSync } from 'fs';
29
+ import { join } from 'path';
30
+ import { PAI_DIR, SKILLS_DIR } from '../lib/pai-paths';
31
+
32
+ async function main() {
33
+ try {
34
+ // Check if this is a subagent session - if so, exit silently
35
+ const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
36
+ const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||
37
+ process.env.CLAUDE_AGENT_TYPE !== undefined;
38
+
39
+ if (isSubagent) {
40
+ // Subagent sessions don't need CORE context loading
41
+ console.error('Subagent session - skipping CORE context loading');
42
+ process.exit(0);
43
+ }
44
+
45
+ // Get CORE skill path using PAI paths library
46
+ const coreSkillPath = join(SKILLS_DIR, 'CORE/SKILL.md');
47
+
48
+ // Verify CORE skill file exists
49
+ if (!existsSync(coreSkillPath)) {
50
+ console.error(`CORE skill not found at: ${coreSkillPath}`);
51
+ console.error(`Ensure CORE/SKILL.md exists or check PAI_DIR environment variable`);
52
+ process.exit(1);
53
+ }
54
+
55
+ console.error('Reading CORE context from skill file...');
56
+
57
+ // Read the CORE SKILL.md file content
58
+ let coreContent = readFileSync(coreSkillPath, 'utf-8');
59
+
60
+ // Perform Dynamic Variable Substitution
61
+ // This allows SKILL.md to be generic while the session is personalized
62
+ const daName = process.env.DA || 'PAI';
63
+ const daColor = process.env.DA_COLOR || 'blue';
64
+ const engineerName = process.env.ENGINEER_NAME || '';
65
+
66
+ // Replace placeholders {{DA}}, {{DA_COLOR}}, {{ENGINEER_NAME}}
67
+ coreContent = coreContent
68
+ .replace(/\{\{DA\}\}/g, daName)
69
+ .replace(/\{\{DA_COLOR\}\}/g, daColor)
70
+ .replace(/\{\{ENGINEER_NAME\}\}/g, engineerName);
71
+
72
+ console.error(`Read ${coreContent.length} characters from CORE SKILL.md (Personalized for ${engineerName} & ${daName})`);
73
+
74
+ // Determine the local timezone dynamically
75
+ const localTimeZone = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
76
+
77
+ // Output the CORE content as a system-reminder
78
+ // This will be injected into Claude's context at session start
79
+ const message = `<system-reminder>
80
+ PAI CORE CONTEXT (Auto-loaded at Session Start)
81
+
82
+ CURRENT DATE/TIME: ${new Date().toLocaleString('en-US', { timeZone: localTimeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' })}
83
+
84
+ The following context has been loaded from ${coreSkillPath}:
85
+
86
+ ---
87
+ ${coreContent}
88
+ ---
89
+
90
+ This context is now active for this session. Follow all instructions, preferences, and guidelines contained above.
91
+ </system-reminder>`;
92
+
93
+ // Write to stdout (will be captured by Claude Code)
94
+ console.log(message);
95
+
96
+ console.error('CORE context injected into session');
97
+ process.exit(0);
98
+ } catch (error) {
99
+ console.error('Error in load-core-context hook:', error);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ main();