@tekmidian/pai 0.5.7 → 0.6.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 +72 -1
- package/README.md +87 -1
- package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
- package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
- package/dist/cli/index.mjs +1482 -1628
- package/dist/cli/index.mjs.map +1 -1
- package/dist/clusters-JIDQW65f.mjs +201 -0
- package/dist/clusters-JIDQW65f.mjs.map +1 -0
- package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
- package/dist/config-BuhHWyOK.mjs.map +1 -0
- package/dist/daemon/index.mjs +11 -8
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-2ND5WO2j.mjs → daemon-D3hYb5_C.mjs} +669 -218
- package/dist/daemon-D3hYb5_C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +4597 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/db-DdUperSl.mjs +110 -0
- package/dist/db-DdUperSl.mjs.map +1 -0
- package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
- package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
- package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
- package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
- package/dist/helpers-BEST-4Gx.mjs +420 -0
- package/dist/helpers-BEST-4Gx.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +2 -2
- package/dist/hooks/capture-all-events.mjs.map +3 -3
- package/dist/hooks/capture-session-summary.mjs +38 -0
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +6 -12
- package/dist/hooks/cleanup-session-files.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +93 -104
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +14 -11
- package/dist/hooks/initialize-session.mjs.map +4 -4
- package/dist/hooks/inject-observations.mjs +220 -0
- package/dist/hooks/inject-observations.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +2 -2
- package/dist/hooks/load-core-context.mjs.map +3 -3
- package/dist/hooks/load-project-context.mjs +90 -91
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/observe.mjs +354 -0
- package/dist/hooks/observe.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +94 -107
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/sync-todo-to-md.mjs +31 -33
- package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
- package/dist/index.d.mts +30 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -8
- package/dist/indexer-D53l5d1U.mjs +1 -0
- package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
- package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
- package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
- package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
- package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
- package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
- package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
- package/dist/note-context-BK24bX8Y.mjs +126 -0
- package/dist/note-context-BK24bX8Y.mjs.map +1 -0
- package/dist/postgres-CKf-EDtS.mjs +846 -0
- package/dist/postgres-CKf-EDtS.mjs.map +1 -0
- package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
- package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
- package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
- package/dist/search-DC1qhkKn.mjs.map +1 -0
- package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
- package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
- package/dist/state-C6_vqz7w.mjs +102 -0
- package/dist/state-C6_vqz7w.mjs.map +1 -0
- package/dist/stop-words-BaMEGVeY.mjs +326 -0
- package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
- package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
- package/dist/sync-BOsnEj2-.mjs.map +1 -0
- package/dist/themes-BvYF0W8T.mjs +148 -0
- package/dist/themes-BvYF0W8T.mjs.map +1 -0
- package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
- package/dist/tools-DcaJlYDN.mjs.map +1 -0
- package/dist/trace-CRx9lPuc.mjs +137 -0
- package/dist/trace-CRx9lPuc.mjs.map +1 -0
- package/dist/{vault-indexer-k-kUlaZ-.mjs → vault-indexer-Bi2cRmn7.mjs} +134 -132
- package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
- package/dist/zettelkasten-cdajbnPr.mjs +708 -0
- package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
- package/package.json +1 -2
- package/src/hooks/ts/lib/project-utils/index.ts +50 -0
- package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
- package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
- package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
- package/src/hooks/ts/lib/project-utils.ts +40 -1018
- package/src/hooks/ts/post-tool-use/observe.ts +327 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
- package/src/hooks/ts/session-start/inject-observations.ts +254 -0
- package/dist/chunker-CbnBe0s0.mjs +0 -191
- package/dist/chunker-CbnBe0s0.mjs.map +0 -1
- package/dist/config-Cf92lGX_.mjs.map +0 -1
- package/dist/daemon-2ND5WO2j.mjs.map +0 -1
- package/dist/db-Dp8VXIMR.mjs +0 -212
- package/dist/db-Dp8VXIMR.mjs.map +0 -1
- package/dist/indexer-CMPOiY1r.mjs.map +0 -1
- package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
- package/dist/mcp/index.d.mts +0 -1
- package/dist/mcp/index.mjs +0 -500
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/postgres-FXrHDPcE.mjs +0 -358
- package/dist/postgres-FXrHDPcE.mjs.map +0 -1
- package/dist/schemas-BFIgGntb.mjs +0 -3405
- package/dist/schemas-BFIgGntb.mjs.map +0 -1
- package/dist/search-_oHfguA5.mjs.map +0 -1
- package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
- package/dist/tools-DV_lsiCc.mjs.map +0 -1
- package/dist/vault-indexer-k-kUlaZ-.mjs.map +0 -1
- package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
- package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
- package/templates/README.md +0 -181
- package/templates/skills/CORE/Aesthetic.md +0 -333
- package/templates/skills/CORE/CONSTITUTION.md +0 -1502
- package/templates/skills/CORE/HistorySystem.md +0 -427
- package/templates/skills/CORE/HookSystem.md +0 -1082
- package/templates/skills/CORE/Prompting.md +0 -509
- package/templates/skills/CORE/ProsodyAgentTemplate.md +0 -53
- package/templates/skills/CORE/ProsodyGuide.md +0 -416
- package/templates/skills/CORE/SKILL.md +0 -741
- package/templates/skills/CORE/SkillSystem.md +0 -213
- package/templates/skills/CORE/TerminalTabs.md +0 -119
- package/templates/skills/CORE/VOICE.md +0 -106
- package/templates/skills/createskill-skill.template.md +0 -78
- package/templates/skills/history-system.template.md +0 -371
- package/templates/skills/hook-system.template.md +0 -913
- package/templates/skills/sessions-skill.template.md +0 -102
- package/templates/skills/skill-system.template.md +0 -214
- package/templates/skills/terminal-tabs.template.md +0 -120
- package/templates/templates.md +0 -20
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook - Observation Capture
|
|
4
|
+
*
|
|
5
|
+
* Classifies each tool call as a structured observation and sends it to the
|
|
6
|
+
* PAI daemon via IPC. Fire-and-forget with a 5-second timeout. Never blocks
|
|
7
|
+
* Claude Code (always exits 0).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { connect } from 'net';
|
|
11
|
+
import { sha256 } from '../../../utils/hash.js';
|
|
12
|
+
import { basename } from 'path';
|
|
13
|
+
import { isProbeSession } from '../lib/project-utils.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
interface HookData {
|
|
20
|
+
session_id: string;
|
|
21
|
+
tool_name: string;
|
|
22
|
+
tool_input: Record<string, unknown>;
|
|
23
|
+
tool_response?: unknown;
|
|
24
|
+
cwd?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ObservationType = 'change' | 'discovery' | 'decision' | 'feature';
|
|
28
|
+
|
|
29
|
+
interface Observation {
|
|
30
|
+
type: ObservationType;
|
|
31
|
+
title: string;
|
|
32
|
+
narrative: string;
|
|
33
|
+
tool_name: string;
|
|
34
|
+
tool_input_summary: string;
|
|
35
|
+
files_read: string[];
|
|
36
|
+
files_modified: string[];
|
|
37
|
+
concepts: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Tools to skip entirely
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const SKIP_TOOLS = new Set([
|
|
45
|
+
'ToolSearch',
|
|
46
|
+
'AskUserQuestion',
|
|
47
|
+
'EnterPlanMode',
|
|
48
|
+
'ExitPlanMode',
|
|
49
|
+
'Skill',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Inline IPC sender — avoids importing src/daemon/ipc-client.ts at build time
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function sendToDaemon(method: string, params: Record<string, unknown>): Promise<void> {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const timeout = setTimeout(() => { resolve(); }, 5000);
|
|
59
|
+
try {
|
|
60
|
+
const socket = connect('/tmp/pai.sock', () => {
|
|
61
|
+
const req = JSON.stringify({ id: Date.now().toString(), method, params }) + '\n';
|
|
62
|
+
socket.write(req);
|
|
63
|
+
socket.on('data', () => { clearTimeout(timeout); socket.destroy(); resolve(); });
|
|
64
|
+
socket.on('error', () => { clearTimeout(timeout); resolve(); });
|
|
65
|
+
});
|
|
66
|
+
socket.on('error', () => { clearTimeout(timeout); resolve(); });
|
|
67
|
+
} catch {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Concept extraction from file paths
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
const SKIP_SEGMENTS = new Set([
|
|
79
|
+
'src', 'dist', 'lib', 'bin', 'test', 'tests', 'spec', 'specs',
|
|
80
|
+
'node_modules', 'Users', 'home', 'usr', 'var', 'tmp', 'etc',
|
|
81
|
+
'hooks', 'scripts', 'config', 'configs', 'assets', 'static',
|
|
82
|
+
'public', 'private', 'build', 'out', 'output', 'generated',
|
|
83
|
+
'ts', 'js', 'mjs', 'cjs',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
function extractConcepts(paths: string[]): string[] {
|
|
87
|
+
const concepts = new Set<string>();
|
|
88
|
+
for (const p of paths) {
|
|
89
|
+
const segments = p.split('/').filter(Boolean);
|
|
90
|
+
for (const seg of segments) {
|
|
91
|
+
// Drop extensions
|
|
92
|
+
const clean = seg.replace(/\.[^.]+$/, '');
|
|
93
|
+
if (clean.length > 2 && !SKIP_SEGMENTS.has(clean) && !/^\d+$/.test(clean)) {
|
|
94
|
+
concepts.add(clean);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Array.from(concepts).slice(0, 10);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Inline classifier
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function str(v: unknown): string {
|
|
106
|
+
if (typeof v === 'string') return v;
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function truncate(s: string, len: number): string {
|
|
111
|
+
return s.length > len ? s.slice(0, len) : s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function classify(toolName: string, toolInput: Record<string, unknown>): Observation | null {
|
|
115
|
+
if (SKIP_TOOLS.has(toolName)) return null;
|
|
116
|
+
|
|
117
|
+
let type: ObservationType = 'discovery';
|
|
118
|
+
let title = '';
|
|
119
|
+
let narrative = '';
|
|
120
|
+
let tool_input_summary = '';
|
|
121
|
+
const files_read: string[] = [];
|
|
122
|
+
const files_modified: string[] = [];
|
|
123
|
+
|
|
124
|
+
switch (toolName) {
|
|
125
|
+
case 'Edit':
|
|
126
|
+
case 'MultiEdit': {
|
|
127
|
+
const fp = str(toolInput.file_path);
|
|
128
|
+
const name = fp ? basename(fp) : 'file';
|
|
129
|
+
type = 'change';
|
|
130
|
+
title = `Modified ${name}`;
|
|
131
|
+
narrative = fp ? `Edited ${fp}` : 'Edited a file';
|
|
132
|
+
tool_input_summary = fp;
|
|
133
|
+
files_modified.push(...(fp ? [fp] : []));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'Write':
|
|
138
|
+
case 'NotebookEdit': {
|
|
139
|
+
const fp = str(toolInput.file_path);
|
|
140
|
+
const name = fp ? basename(fp) : 'file';
|
|
141
|
+
type = 'change';
|
|
142
|
+
title = `Created ${name}`;
|
|
143
|
+
narrative = fp ? `Wrote ${fp}` : 'Wrote a file';
|
|
144
|
+
tool_input_summary = fp;
|
|
145
|
+
files_modified.push(...(fp ? [fp] : []));
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case 'Read': {
|
|
150
|
+
const fp = str(toolInput.file_path);
|
|
151
|
+
const name = fp ? basename(fp) : 'file';
|
|
152
|
+
type = 'discovery';
|
|
153
|
+
title = `Read ${name}`;
|
|
154
|
+
narrative = fp ? `Read ${fp}` : 'Read a file';
|
|
155
|
+
tool_input_summary = fp;
|
|
156
|
+
files_read.push(...(fp ? [fp] : []));
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'Grep': {
|
|
161
|
+
const pattern = str(toolInput.pattern);
|
|
162
|
+
type = 'discovery';
|
|
163
|
+
title = `Searched for '${truncate(pattern, 40)}'`;
|
|
164
|
+
narrative = `Grep search: ${pattern}`;
|
|
165
|
+
tool_input_summary = pattern;
|
|
166
|
+
const gPath = str(toolInput.path || toolInput.file_path);
|
|
167
|
+
if (gPath) files_read.push(gPath);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'Glob': {
|
|
172
|
+
const pattern = str(toolInput.pattern);
|
|
173
|
+
type = 'discovery';
|
|
174
|
+
title = `Found files: ${truncate(pattern, 40)}`;
|
|
175
|
+
narrative = `Glob pattern: ${pattern}`;
|
|
176
|
+
tool_input_summary = pattern;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'Bash': {
|
|
181
|
+
const cmd = str(toolInput.command);
|
|
182
|
+
const cmdLower = cmd.toLowerCase();
|
|
183
|
+
|
|
184
|
+
if (/git\s+commit/.test(cmdLower)) {
|
|
185
|
+
type = 'decision';
|
|
186
|
+
// Try to extract the commit message after -m "..."
|
|
187
|
+
const mMatch = cmd.match(/-m\s+["']([^"']+)/);
|
|
188
|
+
const msg = mMatch ? mMatch[1] : cmd;
|
|
189
|
+
title = `Committed: ${truncate(msg, 60)}`;
|
|
190
|
+
narrative = `Git commit: ${msg}`;
|
|
191
|
+
tool_input_summary = truncate(cmd, 120);
|
|
192
|
+
} else if (/git\s+push/.test(cmdLower)) {
|
|
193
|
+
type = 'decision';
|
|
194
|
+
title = 'Pushed to remote';
|
|
195
|
+
narrative = `Git push: ${truncate(cmd, 80)}`;
|
|
196
|
+
tool_input_summary = truncate(cmd, 120);
|
|
197
|
+
} else if (/\b(jest|vitest|pytest|bun\s+test|npm\s+test|yarn\s+test|pnpm\s+test|node\s+--test)\b/.test(cmdLower)) {
|
|
198
|
+
type = 'feature';
|
|
199
|
+
title = 'Ran tests';
|
|
200
|
+
narrative = `Test run: ${truncate(cmd, 80)}`;
|
|
201
|
+
tool_input_summary = truncate(cmd, 120);
|
|
202
|
+
} else if (/\b(build|compile|bun\s+run\s+build|tsc|esbuild|webpack|vite\s+build)\b/.test(cmdLower)) {
|
|
203
|
+
type = 'feature';
|
|
204
|
+
title = 'Built project';
|
|
205
|
+
narrative = `Build: ${truncate(cmd, 80)}`;
|
|
206
|
+
tool_input_summary = truncate(cmd, 120);
|
|
207
|
+
} else {
|
|
208
|
+
type = 'discovery';
|
|
209
|
+
title = `Ran: ${truncate(cmd, 60)}`;
|
|
210
|
+
narrative = `Bash: ${truncate(cmd, 120)}`;
|
|
211
|
+
tool_input_summary = truncate(cmd, 120);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'Task': {
|
|
217
|
+
const prompt = str(toolInput.prompt || toolInput.description);
|
|
218
|
+
type = 'discovery';
|
|
219
|
+
title = `Delegated: ${truncate(prompt, 60)}`;
|
|
220
|
+
narrative = `Spawned agent: ${truncate(prompt, 200)}`;
|
|
221
|
+
tool_input_summary = truncate(prompt, 200);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'WebFetch': {
|
|
226
|
+
const url = str(toolInput.url);
|
|
227
|
+
type = 'discovery';
|
|
228
|
+
title = `Fetched: ${truncate(url, 60)}`;
|
|
229
|
+
narrative = `Web fetch: ${url}`;
|
|
230
|
+
tool_input_summary = url;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'WebSearch': {
|
|
235
|
+
const query = str(toolInput.query);
|
|
236
|
+
type = 'discovery';
|
|
237
|
+
title = `Searched web: ${truncate(query, 60)}`;
|
|
238
|
+
narrative = `Web search: ${query}`;
|
|
239
|
+
tool_input_summary = query;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
default: {
|
|
244
|
+
// mcp__* tools and anything else
|
|
245
|
+
if (toolName.startsWith('mcp__')) {
|
|
246
|
+
type = 'discovery';
|
|
247
|
+
title = `MCP: ${toolName}`;
|
|
248
|
+
narrative = `Called MCP tool ${toolName}`;
|
|
249
|
+
tool_input_summary = toolName;
|
|
250
|
+
} else {
|
|
251
|
+
// Generic fallback — still capture rather than skip
|
|
252
|
+
type = 'discovery';
|
|
253
|
+
title = `Tool: ${toolName}`;
|
|
254
|
+
narrative = `Called ${toolName}`;
|
|
255
|
+
tool_input_summary = JSON.stringify(toolInput).slice(0, 120);
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const allPaths = [...files_read, ...files_modified];
|
|
262
|
+
const concepts = extractConcepts(allPaths);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
type,
|
|
266
|
+
title,
|
|
267
|
+
narrative,
|
|
268
|
+
tool_name: toolName,
|
|
269
|
+
tool_input_summary,
|
|
270
|
+
files_read,
|
|
271
|
+
files_modified,
|
|
272
|
+
concepts,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Main
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
async function main() {
|
|
281
|
+
try {
|
|
282
|
+
// Read stdin
|
|
283
|
+
const chunks: Buffer[] = [];
|
|
284
|
+
for await (const chunk of process.stdin) {
|
|
285
|
+
chunks.push(chunk as Buffer);
|
|
286
|
+
}
|
|
287
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
288
|
+
if (!raw) process.exit(0);
|
|
289
|
+
|
|
290
|
+
const hookData: HookData = JSON.parse(raw);
|
|
291
|
+
|
|
292
|
+
// Skip probe/health-check sessions
|
|
293
|
+
if (isProbeSession(hookData.cwd)) process.exit(0);
|
|
294
|
+
|
|
295
|
+
// Skip uninteresting tools
|
|
296
|
+
if (SKIP_TOOLS.has(hookData.tool_name)) process.exit(0);
|
|
297
|
+
|
|
298
|
+
// Classify
|
|
299
|
+
const obs = classify(hookData.tool_name, hookData.tool_input);
|
|
300
|
+
if (!obs) process.exit(0);
|
|
301
|
+
|
|
302
|
+
// Content-hash dedup key
|
|
303
|
+
const hash = sha256(hookData.session_id + hookData.tool_name + obs.title).slice(0, 16);
|
|
304
|
+
|
|
305
|
+
// Fire-and-forget to daemon
|
|
306
|
+
await sendToDaemon('observation_store', {
|
|
307
|
+
session_id: hookData.session_id,
|
|
308
|
+
type: obs.type,
|
|
309
|
+
title: obs.title,
|
|
310
|
+
narrative: obs.narrative,
|
|
311
|
+
tool_name: obs.tool_name,
|
|
312
|
+
tool_input_summary: obs.tool_input_summary,
|
|
313
|
+
files_read: obs.files_read,
|
|
314
|
+
files_modified: obs.files_modified,
|
|
315
|
+
concepts: obs.concepts,
|
|
316
|
+
content_hash: hash,
|
|
317
|
+
cwd: hookData.cwd ?? '',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
process.exit(0);
|
|
321
|
+
} catch {
|
|
322
|
+
// Never block Claude Code
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
main();
|
|
@@ -58,6 +58,9 @@ async function main() {
|
|
|
58
58
|
// Write session file
|
|
59
59
|
writeFileSync(join(sessionDir, filename), sessionDoc);
|
|
60
60
|
|
|
61
|
+
// Also store structured summary via daemon IPC for the observations system
|
|
62
|
+
await storeStructuredSummary(data.conversation_id, sessionInfo);
|
|
63
|
+
|
|
61
64
|
// Exit successfully
|
|
62
65
|
process.exit(0);
|
|
63
66
|
} catch (error) {
|
|
@@ -182,4 +185,42 @@ For detailed tool outputs, see: \`\${PAI_DIR}/History/raw-outputs/${timestamp.su
|
|
|
182
185
|
`;
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
async function storeStructuredSummary(
|
|
189
|
+
sessionId: string,
|
|
190
|
+
info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
try {
|
|
193
|
+
const cwd = process.cwd();
|
|
194
|
+
const net = await import('net');
|
|
195
|
+
|
|
196
|
+
await new Promise<void>((resolve, _reject) => {
|
|
197
|
+
const client = net.createConnection('/tmp/pai.sock', () => {
|
|
198
|
+
const msg = JSON.stringify({
|
|
199
|
+
id: 1,
|
|
200
|
+
method: 'session_summary_store',
|
|
201
|
+
params: {
|
|
202
|
+
session_id: sessionId,
|
|
203
|
+
cwd,
|
|
204
|
+
request: null, // We don't have the original request
|
|
205
|
+
investigated: null,
|
|
206
|
+
learned: null,
|
|
207
|
+
completed: info.filesChanged.length > 0
|
|
208
|
+
? `Modified ${info.filesChanged.length} file(s): ${info.filesChanged.slice(0, 5).join(', ')}`
|
|
209
|
+
: null,
|
|
210
|
+
next_steps: null,
|
|
211
|
+
observation_count: 0, // Will be filled by daemon from actual count
|
|
212
|
+
}
|
|
213
|
+
}) + '\n';
|
|
214
|
+
client.write(msg);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
client.on('data', () => { client.end(); resolve(); });
|
|
218
|
+
client.on('error', () => resolve()); // Silent failure
|
|
219
|
+
setTimeout(() => { client.destroy(); resolve(); }, 3000);
|
|
220
|
+
});
|
|
221
|
+
} catch {
|
|
222
|
+
// Silent failure — don't disrupt session end
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
185
226
|
main();
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* inject-observations.ts
|
|
5
|
+
*
|
|
6
|
+
* SessionStart hook that injects recent project observation context into Claude's
|
|
7
|
+
* session as a <system-reminder>. Provides progressive disclosure of recent activity
|
|
8
|
+
* so Claude has immediate awareness of what has been happening in this project.
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. Read session data (session_id, cwd) from stdin
|
|
12
|
+
* 2. Call daemon via IPC: observation_recent with { cwd, limit: 25 }
|
|
13
|
+
* (daemon resolves project_id from cwd internally via registry lookup)
|
|
14
|
+
* 3. Format as progressive disclosure context block
|
|
15
|
+
* 4. Output to stdout as <system-reminder> (injected into session by Claude Code)
|
|
16
|
+
*
|
|
17
|
+
* Silent on any failure — never blocks session start.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { connect } from 'net';
|
|
21
|
+
import { randomUUID } from 'crypto';
|
|
22
|
+
import { isProbeSession } from '../lib/project-utils.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Types
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
interface HookData {
|
|
29
|
+
session_id?: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
hook_event_name?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ObservationRow {
|
|
35
|
+
id: number;
|
|
36
|
+
session_id: string;
|
|
37
|
+
project_id: number | null;
|
|
38
|
+
project_slug: string | null;
|
|
39
|
+
type: string;
|
|
40
|
+
title: string;
|
|
41
|
+
narrative: string | null;
|
|
42
|
+
created_at: string; // ISO string after JSON serialization
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ObservationRecentResult {
|
|
46
|
+
rows: ObservationRow[];
|
|
47
|
+
project_slug?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Inline IPC client — mirrors the pattern in observe.ts
|
|
52
|
+
// Hooks can't import from src/daemon/ at runtime, so we inline this.
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
async function callDaemon(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timeout = setTimeout(() => reject(new Error('timeout')), 5000);
|
|
58
|
+
let buffer = '';
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const socket = connect('/tmp/pai.sock', () => {
|
|
62
|
+
socket.write(JSON.stringify({ id: randomUUID(), method, params }) + '\n');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
socket.on('data', (chunk: Buffer) => {
|
|
66
|
+
buffer += chunk.toString();
|
|
67
|
+
const nl = buffer.indexOf('\n');
|
|
68
|
+
if (nl !== -1) {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
try {
|
|
71
|
+
const response = JSON.parse(buffer.slice(0, nl));
|
|
72
|
+
socket.destroy();
|
|
73
|
+
if (response.ok) resolve(response.result);
|
|
74
|
+
else reject(new Error(response.error ?? 'daemon error'));
|
|
75
|
+
} catch (e) {
|
|
76
|
+
socket.destroy();
|
|
77
|
+
reject(e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
socket.on('error', () => {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
reject(new Error('daemon unavailable'));
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
reject(new Error('daemon unavailable'));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Time formatting
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function timeAgo(date: Date): string {
|
|
98
|
+
const diffMs = Date.now() - date.getTime();
|
|
99
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
100
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
101
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
102
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
103
|
+
|
|
104
|
+
if (diffSec < 60) return 'just now';
|
|
105
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
106
|
+
if (diffHour < 24) return `${diffHour}h ago`;
|
|
107
|
+
if (diffDay < 7) return `${diffDay}d ago`;
|
|
108
|
+
|
|
109
|
+
// Older than a week: show date string
|
|
110
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Type label
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
function typeLabel(type: string): string {
|
|
118
|
+
switch (type) {
|
|
119
|
+
case 'change': return '[change]';
|
|
120
|
+
case 'discovery': return '[discovery]';
|
|
121
|
+
case 'decision': return '[decision]';
|
|
122
|
+
case 'bugfix': return '[bugfix]';
|
|
123
|
+
case 'feature': return '[feature]';
|
|
124
|
+
case 'refactor': return '[refactor]';
|
|
125
|
+
default: return `[${type}]`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Truncate
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
function truncate(s: string, maxLen: number): string {
|
|
134
|
+
return s.length > maxLen ? s.slice(0, maxLen - 1) + '\u2026' : s;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Format observations as progressive disclosure context
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function formatContext(
|
|
142
|
+
projectSlug: string,
|
|
143
|
+
observations: ObservationRow[]
|
|
144
|
+
): string {
|
|
145
|
+
if (observations.length === 0) return '';
|
|
146
|
+
|
|
147
|
+
// Count distinct sessions
|
|
148
|
+
const sessionSet = new Set(observations.map(o => o.session_id));
|
|
149
|
+
const sessionCount = sessionSet.size;
|
|
150
|
+
|
|
151
|
+
// Most recent observation
|
|
152
|
+
const newest = new Date(observations[0].created_at);
|
|
153
|
+
const lastActivity = timeAgo(newest);
|
|
154
|
+
|
|
155
|
+
// Timeline: show most recent 15, keep titles to 80 chars
|
|
156
|
+
const timelineObs = observations.slice(0, 15);
|
|
157
|
+
const timeline = timelineObs
|
|
158
|
+
.map(o => {
|
|
159
|
+
const t = timeAgo(new Date(o.created_at));
|
|
160
|
+
const label = typeLabel(o.type);
|
|
161
|
+
const title = truncate(o.title, 80);
|
|
162
|
+
return `- [${t}] ${label} ${title}`;
|
|
163
|
+
})
|
|
164
|
+
.join('\n');
|
|
165
|
+
|
|
166
|
+
const showingNote = observations.length > 15
|
|
167
|
+
? `(showing most recent 15 of ${observations.length}, use observation_search for more)`
|
|
168
|
+
: `(showing ${observations.length} observation${observations.length !== 1 ? 's' : ''})`;
|
|
169
|
+
|
|
170
|
+
const lines: string[] = [
|
|
171
|
+
`<system-reminder>`,
|
|
172
|
+
`OBSERVATION CONTEXT (auto-injected)`,
|
|
173
|
+
``,
|
|
174
|
+
`## Recent Activity (${projectSlug})`,
|
|
175
|
+
`${observations.length} observations across ${sessionCount} session${sessionCount !== 1 ? 's' : ''} | Last activity: ${lastActivity}`,
|
|
176
|
+
``,
|
|
177
|
+
`### Recent Timeline`,
|
|
178
|
+
timeline,
|
|
179
|
+
showingNote,
|
|
180
|
+
`</system-reminder>`,
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Main
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
try {
|
|
192
|
+
// Skip probe/health-check sessions
|
|
193
|
+
if (isProbeSession()) {
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Skip subagent sessions — they don't need observation context
|
|
198
|
+
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
|
|
199
|
+
const isSubagent = claudeProjectDir.includes('/.claude/agents/') ||
|
|
200
|
+
process.env.CLAUDE_AGENT_TYPE !== undefined;
|
|
201
|
+
if (isSubagent) {
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Read hook data from stdin
|
|
206
|
+
let hookData: HookData = {};
|
|
207
|
+
try {
|
|
208
|
+
const chunks: Buffer[] = [];
|
|
209
|
+
for await (const chunk of process.stdin) {
|
|
210
|
+
chunks.push(chunk as Buffer);
|
|
211
|
+
}
|
|
212
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
213
|
+
if (raw) {
|
|
214
|
+
hookData = JSON.parse(raw);
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
// Non-fatal — fall back to process.cwd()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const cwd = hookData.cwd || process.cwd();
|
|
221
|
+
|
|
222
|
+
// Fetch recent observations for this cwd — daemon resolves the project internally
|
|
223
|
+
let observations: ObservationRow[];
|
|
224
|
+
let projectSlug: string;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const result = await callDaemon('observation_recent', { cwd, limit: 25 }) as ObservationRecentResult;
|
|
228
|
+
observations = result?.rows ?? [];
|
|
229
|
+
projectSlug = result?.project_slug ?? '';
|
|
230
|
+
} catch {
|
|
231
|
+
// Daemon unavailable or Postgres not configured — silent exit
|
|
232
|
+
process.exit(0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!observations || observations.length === 0 || !projectSlug) {
|
|
236
|
+
// No data or no matching project — nothing to inject
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Format and output
|
|
241
|
+
const context = formatContext(projectSlug, observations);
|
|
242
|
+
if (context) {
|
|
243
|
+
// Output to stdout — Claude Code captures this and injects into session context
|
|
244
|
+
console.log(context);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process.exit(0);
|
|
248
|
+
} catch {
|
|
249
|
+
// Never block session start
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
main();
|