@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.
- package/ARCHITECTURE.md +16 -10
- package/README.md +46 -6
- package/dist/{auto-route-JjW3f7pV.mjs → auto-route-B5MSUJZK.mjs} +3 -3
- package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-B5MSUJZK.mjs.map} +1 -1
- package/dist/cli/index.mjs +313 -43
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{config-DELNqq3Z.mjs → config-B4brrHHE.mjs} +1 -1
- package/dist/{config-DELNqq3Z.mjs.map → config-B4brrHHE.mjs.map} +1 -1
- package/dist/daemon/index.mjs +7 -7
- package/dist/daemon-mcp/index.mjs +11 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{daemon-CeTX4NpF.mjs → daemon-s868Paua.mjs} +12 -12
- package/dist/{daemon-CeTX4NpF.mjs.map → daemon-s868Paua.mjs.map} +1 -1
- package/dist/{detect-D7gPV3fQ.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-D7gPV3fQ.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-cYYhK2Mi.mjs → detector-Bp-2SM3x.mjs} +2 -2
- package/dist/{detector-cYYhK2Mi.mjs.map → detector-Bp-2SM3x.mjs.map} +1 -1
- package/dist/{factory-DZLvRf4m.mjs → factory-CeXQzlwn.mjs} +3 -3
- package/dist/{factory-DZLvRf4m.mjs.map → factory-CeXQzlwn.mjs.map} +1 -1
- package/dist/hooks/capture-all-events.mjs +238 -0
- package/dist/hooks/capture-all-events.mjs.map +7 -0
- package/dist/hooks/capture-session-summary.mjs +198 -0
- package/dist/hooks/capture-session-summary.mjs.map +7 -0
- package/dist/hooks/capture-tool-output.mjs +105 -0
- package/dist/hooks/capture-tool-output.mjs.map +7 -0
- package/dist/hooks/cleanup-session-files.mjs +129 -0
- package/dist/hooks/cleanup-session-files.mjs.map +7 -0
- package/dist/hooks/context-compression-hook.mjs +283 -0
- package/dist/hooks/context-compression-hook.mjs.map +7 -0
- package/dist/hooks/initialize-session.mjs +206 -0
- package/dist/hooks/initialize-session.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +110 -0
- package/dist/hooks/load-core-context.mjs.map +7 -0
- package/dist/hooks/load-project-context.mjs +548 -0
- package/dist/hooks/load-project-context.mjs.map +7 -0
- package/dist/hooks/security-validator.mjs +159 -0
- package/dist/hooks/security-validator.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +625 -0
- package/dist/hooks/stop-hook.mjs.map +7 -0
- package/dist/hooks/subagent-stop-hook.mjs +152 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
- package/dist/hooks/sync-todo-to-md.mjs +322 -0
- package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
- package/dist/hooks/update-tab-on-action.mjs +90 -0
- package/dist/hooks/update-tab-on-action.mjs.map +7 -0
- package/dist/hooks/update-tab-titles.mjs +55 -0
- package/dist/hooks/update-tab-titles.mjs.map +7 -0
- package/dist/index.d.mts +29 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -3
- package/dist/{indexer-backend-BHztlJJg.mjs → indexer-backend-DQO-FqAI.mjs} +1 -1
- package/dist/{indexer-backend-BHztlJJg.mjs.map → indexer-backend-DQO-FqAI.mjs.map} +1 -1
- package/dist/{ipc-client-CLt2fNlC.mjs → ipc-client-CgSpwHDC.mjs} +1 -1
- package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-CgSpwHDC.mjs.map} +1 -1
- package/dist/mcp/index.mjs +15 -5
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{postgres-CRBe30Ag.mjs → postgres-CIxeqf_n.mjs} +1 -1
- package/dist/{postgres-CRBe30Ag.mjs.map → postgres-CIxeqf_n.mjs.map} +1 -1
- package/dist/reranker-D7bRAHi6.mjs +71 -0
- package/dist/reranker-D7bRAHi6.mjs.map +1 -0
- package/dist/{schemas-BY3Pjvje.mjs → schemas-BFIgGntb.mjs} +1 -1
- package/dist/{schemas-BY3Pjvje.mjs.map → schemas-BFIgGntb.mjs.map} +1 -1
- package/dist/{search-GK0ibTJy.mjs → search-_oHfguA5.mjs} +47 -4
- package/dist/search-_oHfguA5.mjs.map +1 -0
- package/dist/{sqlite-RyR8Up1v.mjs → sqlite-CymLKiDE.mjs} +2 -2
- package/dist/{sqlite-RyR8Up1v.mjs.map → sqlite-CymLKiDE.mjs.map} +1 -1
- package/dist/{tools-CUg0Lyg-.mjs → tools-Dx7GjOHd.mjs} +23 -14
- package/dist/tools-Dx7GjOHd.mjs.map +1 -0
- package/dist/{vault-indexer-Bo2aPSzP.mjs → vault-indexer-DXWs9pDn.mjs} +1 -1
- package/dist/{vault-indexer-Bo2aPSzP.mjs.map → vault-indexer-DXWs9pDn.mjs.map} +1 -1
- package/dist/{zettelkasten-Co-w0XSZ.mjs → zettelkasten-e-a4rW_6.mjs} +2 -2
- package/dist/{zettelkasten-Co-w0XSZ.mjs.map → zettelkasten-e-a4rW_6.mjs.map} +1 -1
- package/package.json +4 -2
- package/scripts/build-hooks.mjs +51 -0
- package/src/hooks/ts/capture-all-events.ts +179 -0
- package/src/hooks/ts/lib/detect-environment.ts +53 -0
- package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
- package/src/hooks/ts/lib/pai-paths.ts +124 -0
- package/src/hooks/ts/lib/project-utils.ts +914 -0
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
- package/src/hooks/ts/session-start/initialize-session.ts +155 -0
- package/src/hooks/ts/session-start/load-core-context.ts +104 -0
- package/src/hooks/ts/session-start/load-project-context.ts +394 -0
- package/src/hooks/ts/stop/stop-hook.ts +407 -0
- package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
- package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
- package/tab-color-command.sh +24 -0
- package/templates/skills/createskill-skill.template.md +78 -0
- package/templates/skills/history-system.template.md +371 -0
- package/templates/skills/hook-system.template.md +913 -0
- package/templates/skills/sessions-skill.template.md +102 -0
- package/templates/skills/skill-system.template.md +214 -0
- package/templates/skills/terminal-tabs.template.md +120 -0
- package/dist/search-GK0ibTJy.mjs.map +0 -1
- 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();
|