@tekmidian/pai 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +1 -1
- package/dist/daemon/index.mjs +1 -1
- package/dist/{daemon-D3hYb5_C.mjs → daemon-DuGlDnV7.mjs} +861 -4
- package/dist/daemon-DuGlDnV7.mjs.map +1 -0
- package/dist/hooks/context-compression-hook.mjs +58 -22
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +78 -27
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +220 -125
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
- package/dist/skills/Reconstruct/SKILL.md +232 -0
- package/package.json +1 -1
- package/plugins/productivity/plugin.json +1 -1
- package/plugins/productivity/skills/Reconstruct/SKILL.md +232 -0
- package/src/hooks/ts/lib/project-utils/index.ts +1 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +46 -5
- package/src/hooks/ts/lib/project-utils.ts +1 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +60 -37
- package/src/hooks/ts/session-start/load-project-context.ts +110 -28
- package/src/hooks/ts/stop/stop-hook.ts +259 -199
- package/dist/daemon-D3hYb5_C.mjs.map +0 -1
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { join, basename, dirname } from 'path';
|
|
5
|
+
import { connect } from 'net';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
5
7
|
import {
|
|
6
8
|
sendNtfyNotification,
|
|
7
9
|
getCurrentNotePath,
|
|
@@ -14,15 +16,148 @@ import {
|
|
|
14
16
|
WorkItem
|
|
15
17
|
} from '../lib/project-utils';
|
|
16
18
|
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const DAEMON_SOCKET = process.env.PAI_SOCKET ?? '/tmp/pai.sock';
|
|
24
|
+
const DAEMON_TIMEOUT_MS = 3_000;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helper: safely convert Claude content (string | Block[]) to plain text
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function contentToText(content: any): string {
|
|
31
|
+
if (typeof content === 'string') return content;
|
|
32
|
+
if (Array.isArray(content)) {
|
|
33
|
+
return content
|
|
34
|
+
.map((c) => {
|
|
35
|
+
if (typeof c === 'string') return c;
|
|
36
|
+
if (c?.text) return c.text;
|
|
37
|
+
if (c?.content) return String(c.content);
|
|
38
|
+
return '';
|
|
39
|
+
})
|
|
40
|
+
.join(' ')
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helper: extract COMPLETED: line from the last assistant response
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function extractCompletedMessage(lines: string[]): string {
|
|
51
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
52
|
+
try {
|
|
53
|
+
const entry = JSON.parse(lines[i]);
|
|
54
|
+
if (entry.type === 'assistant' && entry.message?.content) {
|
|
55
|
+
const content = contentToText(entry.message.content);
|
|
56
|
+
const m = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
57
|
+
if (m) {
|
|
58
|
+
return m[1].trim().replace(/\*+/g, '').replace(/\[.*?\]/g, '').trim();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Skip invalid JSON
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Daemon IPC relay — fast path
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Try to enqueue work with the daemon over its Unix socket.
|
|
74
|
+
* Returns true on success, false if the daemon is unreachable.
|
|
75
|
+
* Times out after DAEMON_TIMEOUT_MS so the hook doesn't block.
|
|
76
|
+
*/
|
|
77
|
+
function enqueueWithDaemon(payload: {
|
|
78
|
+
transcriptPath: string;
|
|
79
|
+
cwd: string;
|
|
80
|
+
message: string;
|
|
81
|
+
}): Promise<boolean> {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
let done = false;
|
|
84
|
+
let buffer = '';
|
|
85
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
+
|
|
87
|
+
function finish(ok: boolean): void {
|
|
88
|
+
if (done) return;
|
|
89
|
+
done = true;
|
|
90
|
+
if (timer !== null) { clearTimeout(timer); timer = null; }
|
|
91
|
+
try { client.destroy(); } catch { /* ignore */ }
|
|
92
|
+
resolve(ok);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const client = connect(DAEMON_SOCKET, () => {
|
|
96
|
+
const msg = JSON.stringify({
|
|
97
|
+
id: randomUUID(),
|
|
98
|
+
method: 'work_queue_enqueue',
|
|
99
|
+
params: {
|
|
100
|
+
type: 'session-end',
|
|
101
|
+
priority: 2,
|
|
102
|
+
payload: {
|
|
103
|
+
transcriptPath: payload.transcriptPath,
|
|
104
|
+
cwd: payload.cwd,
|
|
105
|
+
message: payload.message,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}) + '\n';
|
|
109
|
+
client.write(msg);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
client.on('data', (chunk: Buffer) => {
|
|
113
|
+
buffer += chunk.toString();
|
|
114
|
+
const nl = buffer.indexOf('\n');
|
|
115
|
+
if (nl === -1) return;
|
|
116
|
+
const line = buffer.slice(0, nl);
|
|
117
|
+
try {
|
|
118
|
+
const response = JSON.parse(line) as { ok: boolean; error?: string };
|
|
119
|
+
if (response.ok) {
|
|
120
|
+
console.error(`STOP-HOOK: Work enqueued with daemon (id=${(response as any).result?.id}).`);
|
|
121
|
+
finish(true);
|
|
122
|
+
} else {
|
|
123
|
+
console.error(`STOP-HOOK: Daemon rejected enqueue: ${response.error}`);
|
|
124
|
+
finish(false);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
finish(false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
client.on('error', (e: NodeJS.ErrnoException) => {
|
|
132
|
+
if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {
|
|
133
|
+
console.error('STOP-HOOK: Daemon not running — falling back to direct execution.');
|
|
134
|
+
} else {
|
|
135
|
+
console.error(`STOP-HOOK: Daemon socket error: ${e.message}`);
|
|
136
|
+
}
|
|
137
|
+
finish(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
client.on('end', () => { if (!done) finish(false); });
|
|
141
|
+
|
|
142
|
+
timer = setTimeout(() => {
|
|
143
|
+
console.error(`STOP-HOOK: Daemon timeout after ${DAEMON_TIMEOUT_MS}ms — falling back.`);
|
|
144
|
+
finish(false);
|
|
145
|
+
}, DAEMON_TIMEOUT_MS);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Direct execution — fallback path (original stop-hook logic)
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
17
153
|
/**
|
|
18
|
-
* Extract work items from transcript for session note
|
|
19
|
-
* Looks for SUMMARY, ACTIONS, RESULTS sections in assistant responses
|
|
154
|
+
* Extract work items from transcript for session note.
|
|
155
|
+
* Looks for SUMMARY, ACTIONS, RESULTS sections in assistant responses.
|
|
20
156
|
*/
|
|
21
157
|
function extractWorkFromTranscript(lines: string[]): WorkItem[] {
|
|
22
158
|
const workItems: WorkItem[] = [];
|
|
23
159
|
const seenSummaries = new Set<string>();
|
|
24
160
|
|
|
25
|
-
// Process all assistant messages to find work summaries
|
|
26
161
|
for (const line of lines) {
|
|
27
162
|
try {
|
|
28
163
|
const entry = JSON.parse(line);
|
|
@@ -36,16 +171,14 @@ function extractWorkFromTranscript(lines: string[]): WorkItem[] {
|
|
|
36
171
|
if (summary && !seenSummaries.has(summary) && summary.length > 5) {
|
|
37
172
|
seenSummaries.add(summary);
|
|
38
173
|
|
|
39
|
-
// Try to extract details from ACTIONS
|
|
174
|
+
// Try to extract details from ACTIONS section
|
|
40
175
|
const details: string[] = [];
|
|
41
|
-
|
|
42
176
|
const actionsMatch = content.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
|
|
43
177
|
if (actionsMatch) {
|
|
44
|
-
// Extract bullet points or numbered items
|
|
45
178
|
const actionLines = actionsMatch[1].split('\n')
|
|
46
179
|
.map(l => l.replace(/^[-*•]\s*/, '').replace(/^\d+\.\s*/, '').trim())
|
|
47
180
|
.filter(l => l.length > 3 && l.length < 100);
|
|
48
|
-
details.push(...actionLines.slice(0, 3));
|
|
181
|
+
details.push(...actionLines.slice(0, 3));
|
|
49
182
|
}
|
|
50
183
|
|
|
51
184
|
workItems.push({
|
|
@@ -59,13 +192,10 @@ function extractWorkFromTranscript(lines: string[]): WorkItem[] {
|
|
|
59
192
|
// Also look for COMPLETED: lines as backup
|
|
60
193
|
const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
61
194
|
if (completedMatch && workItems.length === 0) {
|
|
62
|
-
const completed = completedMatch[1].trim().replace(/\*+/g, '');
|
|
195
|
+
const completed = completedMatch[1].trim().replace(/\*+/g, '').replace(/\[.*?\]/g, '');
|
|
63
196
|
if (completed && !seenSummaries.has(completed) && completed.length > 5) {
|
|
64
197
|
seenSummaries.add(completed);
|
|
65
|
-
workItems.push({
|
|
66
|
-
title: completed,
|
|
67
|
-
completed: true
|
|
68
|
-
});
|
|
198
|
+
workItems.push({ title: completed, completed: true });
|
|
69
199
|
}
|
|
70
200
|
}
|
|
71
201
|
}
|
|
@@ -78,10 +208,9 @@ function extractWorkFromTranscript(lines: string[]): WorkItem[] {
|
|
|
78
208
|
}
|
|
79
209
|
|
|
80
210
|
/**
|
|
81
|
-
* Generate 4-word tab title summarizing what was done
|
|
211
|
+
* Generate 4-word tab title summarizing what was done.
|
|
82
212
|
*/
|
|
83
213
|
function generateTabTitle(prompt: string, completedLine?: string): string {
|
|
84
|
-
// If we have a completed line, try to use it for a better summary
|
|
85
214
|
if (completedLine) {
|
|
86
215
|
const cleanCompleted = completedLine
|
|
87
216
|
.replace(/\*+/g, '')
|
|
@@ -89,23 +218,18 @@ function generateTabTitle(prompt: string, completedLine?: string): string {
|
|
|
89
218
|
.replace(/COMPLETED:\s*/gi, '')
|
|
90
219
|
.trim();
|
|
91
220
|
|
|
92
|
-
// Extract meaningful words from the completed line
|
|
93
221
|
const completedWords = cleanCompleted.split(/\s+/)
|
|
94
222
|
.filter(word => word.length > 2 &&
|
|
95
223
|
!['the', 'and', 'but', 'for', 'are', 'with', 'his', 'her', 'this', 'that', 'you', 'can', 'will', 'have', 'been', 'your', 'from', 'they', 'were', 'said', 'what', 'them', 'just', 'told', 'how', 'does', 'into', 'about', 'completed'].includes(word.toLowerCase()))
|
|
96
224
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
|
97
225
|
|
|
98
226
|
if (completedWords.length >= 2) {
|
|
99
|
-
// Build a 4-word summary from completed line
|
|
100
227
|
const summary = completedWords.slice(0, 4);
|
|
101
|
-
while (summary.length < 4)
|
|
102
|
-
summary.push('Done');
|
|
103
|
-
}
|
|
228
|
+
while (summary.length < 4) summary.push('Done');
|
|
104
229
|
return summary.slice(0, 4).join(' ');
|
|
105
230
|
}
|
|
106
231
|
}
|
|
107
232
|
|
|
108
|
-
// Fall back to parsing the prompt
|
|
109
233
|
const cleanPrompt = prompt.replace(/[^\w\s]/g, ' ').trim();
|
|
110
234
|
const words = cleanPrompt.split(/\s+/).filter(word =>
|
|
111
235
|
word.length > 2 &&
|
|
@@ -113,103 +237,132 @@ function generateTabTitle(prompt: string, completedLine?: string): string {
|
|
|
113
237
|
);
|
|
114
238
|
|
|
115
239
|
const lowerPrompt = prompt.toLowerCase();
|
|
116
|
-
|
|
117
|
-
// Find action verb if present
|
|
118
240
|
const actionVerbs = ['test', 'rename', 'fix', 'debug', 'research', 'write', 'create', 'make', 'build', 'implement', 'analyze', 'review', 'update', 'modify', 'generate', 'develop', 'design', 'deploy', 'configure', 'setup', 'install', 'remove', 'delete', 'add', 'check', 'verify', 'validate', 'optimize', 'refactor', 'enhance', 'improve', 'send', 'email', 'help', 'updated', 'fixed', 'created', 'built', 'added'];
|
|
119
|
-
|
|
120
241
|
let titleWords: string[] = [];
|
|
121
242
|
|
|
122
|
-
// Check for action verb
|
|
123
243
|
for (const verb of actionVerbs) {
|
|
124
244
|
if (lowerPrompt.includes(verb)) {
|
|
125
|
-
// Convert to past tense for summary
|
|
126
245
|
let pastTense = verb;
|
|
127
246
|
if (verb === 'write') pastTense = 'Wrote';
|
|
128
247
|
else if (verb === 'make') pastTense = 'Made';
|
|
129
248
|
else if (verb === 'send') pastTense = 'Sent';
|
|
130
249
|
else if (verb.endsWith('e')) pastTense = verb.charAt(0).toUpperCase() + verb.slice(1, -1) + 'ed';
|
|
131
250
|
else pastTense = verb.charAt(0).toUpperCase() + verb.slice(1) + 'ed';
|
|
132
|
-
|
|
133
251
|
titleWords.push(pastTense);
|
|
134
252
|
break;
|
|
135
253
|
}
|
|
136
254
|
}
|
|
137
255
|
|
|
138
|
-
// Add most meaningful remaining words
|
|
139
256
|
const remainingWords = words
|
|
140
257
|
.filter(word => !actionVerbs.includes(word.toLowerCase()))
|
|
141
258
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
|
142
259
|
|
|
143
|
-
// Fill up to 4 words total
|
|
144
260
|
for (const word of remainingWords) {
|
|
145
|
-
if (titleWords.length < 4)
|
|
146
|
-
|
|
147
|
-
} else {
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
261
|
+
if (titleWords.length < 4) titleWords.push(word);
|
|
262
|
+
else break;
|
|
150
263
|
}
|
|
151
264
|
|
|
152
|
-
|
|
153
|
-
if (titleWords.length ===
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (titleWords.length === 1) {
|
|
157
|
-
titleWords.push('Task');
|
|
158
|
-
}
|
|
159
|
-
if (titleWords.length === 2) {
|
|
160
|
-
titleWords.push('Successfully');
|
|
161
|
-
}
|
|
162
|
-
if (titleWords.length === 3) {
|
|
163
|
-
titleWords.push('Done');
|
|
164
|
-
}
|
|
265
|
+
if (titleWords.length === 0) titleWords.push('Completed');
|
|
266
|
+
if (titleWords.length === 1) titleWords.push('Task');
|
|
267
|
+
if (titleWords.length === 2) titleWords.push('Successfully');
|
|
268
|
+
if (titleWords.length === 3) titleWords.push('Done');
|
|
165
269
|
|
|
166
270
|
return titleWords.slice(0, 4).join(' ');
|
|
167
271
|
}
|
|
168
272
|
|
|
169
273
|
/**
|
|
170
|
-
*
|
|
274
|
+
* Do the heavy work directly in the hook process.
|
|
275
|
+
* Used when the daemon is unreachable.
|
|
171
276
|
*/
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
process.stderr.write(`\x1b]30;${title}\x07`);
|
|
184
|
-
} else {
|
|
185
|
-
process.stderr.write(`\x1b]0;${title}\x07`);
|
|
186
|
-
process.stderr.write(`\x1b]2;${title}\x07`);
|
|
277
|
+
async function executeDirectly(
|
|
278
|
+
lines: string[],
|
|
279
|
+
transcriptPath: string,
|
|
280
|
+
cwd: string,
|
|
281
|
+
message: string,
|
|
282
|
+
lastUserQuery: string
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
// Set terminal tab title
|
|
285
|
+
let tabTitle = message || '';
|
|
286
|
+
if (!tabTitle && lastUserQuery) {
|
|
287
|
+
tabTitle = generateTabTitle(lastUserQuery, '');
|
|
187
288
|
}
|
|
188
289
|
|
|
189
|
-
if (
|
|
190
|
-
|
|
290
|
+
if (tabTitle) {
|
|
291
|
+
try {
|
|
292
|
+
const escapedTitle = tabTitle.replace(/'/g, "'\\''");
|
|
293
|
+
const { execSync } = await import('child_process');
|
|
294
|
+
execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
|
|
295
|
+
execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
|
|
296
|
+
execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
|
|
297
|
+
console.error(`Tab title set to: "${tabTitle}"`);
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.error(`Failed to set tab title: ${e}`);
|
|
300
|
+
}
|
|
191
301
|
}
|
|
192
|
-
}
|
|
193
302
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
303
|
+
// Final tab title override
|
|
304
|
+
if (message) {
|
|
305
|
+
const finalTabTitle = message.slice(0, 50);
|
|
306
|
+
process.stderr.write(`\x1b]2;${finalTabTitle}\x07`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Finalize session note
|
|
310
|
+
try {
|
|
311
|
+
const notesInfo = findNotesDir(cwd);
|
|
312
|
+
const currentNotePath = getCurrentNotePath(notesInfo.path);
|
|
313
|
+
|
|
314
|
+
if (currentNotePath) {
|
|
315
|
+
const workItems = extractWorkFromTranscript(lines);
|
|
316
|
+
if (workItems.length > 0) {
|
|
317
|
+
addWorkToSessionNote(currentNotePath, workItems);
|
|
318
|
+
console.error(`Added ${workItems.length} work item(s) to session note`);
|
|
319
|
+
} else if (message) {
|
|
320
|
+
addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);
|
|
321
|
+
console.error(`Added completion message to session note`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const summary = message || 'Session completed.';
|
|
325
|
+
finalizeSessionNote(currentNotePath, summary);
|
|
326
|
+
console.error(`Session note finalized: ${basename(currentNotePath)}`);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const stateLines: string[] = [];
|
|
330
|
+
stateLines.push(`Working directory: ${cwd}`);
|
|
331
|
+
if (workItems.length > 0) {
|
|
332
|
+
stateLines.push('', 'Work completed:');
|
|
333
|
+
for (const item of workItems.slice(0, 5)) {
|
|
334
|
+
stateLines.push(`- ${item.title}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (message) {
|
|
338
|
+
stateLines.push('', `Last completed: ${message}`);
|
|
339
|
+
}
|
|
340
|
+
updateTodoContinue(cwd, basename(currentNotePath), stateLines.join('\n'), 'session-end');
|
|
341
|
+
} catch (todoError) {
|
|
342
|
+
console.error(`Could not update TODO.md: ${todoError}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (noteError) {
|
|
346
|
+
console.error(`Could not finalize session note: ${noteError}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Move session .jsonl files to sessions/
|
|
350
|
+
try {
|
|
351
|
+
const transcriptDir = dirname(transcriptPath);
|
|
352
|
+
const movedCount = moveSessionFilesToSessionsDir(transcriptDir);
|
|
353
|
+
if (movedCount > 0) {
|
|
354
|
+
console.error(`Moved ${movedCount} session file(s) to sessions/`);
|
|
355
|
+
}
|
|
356
|
+
} catch (moveError) {
|
|
357
|
+
console.error(`Could not move session files: ${moveError}`);
|
|
207
358
|
}
|
|
208
|
-
return '';
|
|
209
359
|
}
|
|
210
360
|
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Main
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
211
365
|
async function main() {
|
|
212
|
-
// Skip probe/health-check sessions (e.g. CodexBar ClaudeProbe)
|
|
213
366
|
if (isProbeSession()) {
|
|
214
367
|
process.exit(0);
|
|
215
368
|
}
|
|
@@ -217,10 +370,9 @@ async function main() {
|
|
|
217
370
|
const timestamp = new Date().toISOString();
|
|
218
371
|
console.error(`\nSTOP-HOOK TRIGGERED AT ${timestamp}`);
|
|
219
372
|
|
|
220
|
-
//
|
|
373
|
+
// Read stdin
|
|
221
374
|
let input = '';
|
|
222
375
|
const decoder = new TextDecoder();
|
|
223
|
-
|
|
224
376
|
try {
|
|
225
377
|
for await (const chunk of process.stdin) {
|
|
226
378
|
input += decoder.decode(chunk, { stream: true });
|
|
@@ -253,8 +405,8 @@ async function main() {
|
|
|
253
405
|
process.exit(0);
|
|
254
406
|
}
|
|
255
407
|
|
|
256
|
-
// Read
|
|
257
|
-
let transcript;
|
|
408
|
+
// Read transcript
|
|
409
|
+
let transcript: string;
|
|
258
410
|
try {
|
|
259
411
|
transcript = readFileSync(transcriptPath, 'utf-8');
|
|
260
412
|
console.error(`Transcript loaded: ${transcript.split('\n').length} lines`);
|
|
@@ -263,10 +415,9 @@ async function main() {
|
|
|
263
415
|
process.exit(0);
|
|
264
416
|
}
|
|
265
417
|
|
|
266
|
-
// Parse the JSON lines to find what happened in this session
|
|
267
418
|
const lines = transcript.trim().split('\n');
|
|
268
419
|
|
|
269
|
-
//
|
|
420
|
+
// Extract last user query for tab title / fallback
|
|
270
421
|
let lastUserQuery = '';
|
|
271
422
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
272
423
|
try {
|
|
@@ -285,61 +436,26 @@ async function main() {
|
|
|
285
436
|
}
|
|
286
437
|
if (lastUserQuery) break;
|
|
287
438
|
}
|
|
288
|
-
} catch
|
|
439
|
+
} catch {
|
|
289
440
|
// Skip invalid JSON
|
|
290
441
|
}
|
|
291
442
|
}
|
|
292
443
|
|
|
293
|
-
// Extract
|
|
294
|
-
|
|
444
|
+
// Extract completion message
|
|
445
|
+
const message = extractCompletedMessage(lines);
|
|
295
446
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const entry = JSON.parse(lastResponse);
|
|
299
|
-
if (entry.type === 'assistant' && entry.message?.content) {
|
|
300
|
-
const content = contentToText(entry.message.content);
|
|
301
|
-
|
|
302
|
-
// Look for COMPLETED line
|
|
303
|
-
const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
304
|
-
if (completedMatch) {
|
|
305
|
-
message = completedMatch[1].trim()
|
|
306
|
-
.replace(/\*+/g, '')
|
|
307
|
-
.replace(/\[.*?\]/g, '')
|
|
308
|
-
.trim();
|
|
309
|
-
console.error(`COMPLETION: ${message}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
} catch (e) {
|
|
313
|
-
console.error('Error parsing assistant response:', e);
|
|
314
|
-
}
|
|
447
|
+
console.error(`User query: ${lastUserQuery || 'No query found'}`);
|
|
448
|
+
console.error(`Message: ${message || 'No completion message'}`);
|
|
315
449
|
|
|
316
|
-
//
|
|
450
|
+
// Always set terminal tab title immediately (fast, no daemon needed)
|
|
317
451
|
let tabTitle = message || '';
|
|
318
|
-
|
|
319
452
|
if (!tabTitle && lastUserQuery) {
|
|
320
|
-
|
|
321
|
-
const entry = JSON.parse(lastResponse);
|
|
322
|
-
if (entry.type === 'assistant' && entry.message?.content) {
|
|
323
|
-
const content = contentToText(entry.message.content);
|
|
324
|
-
const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/im);
|
|
325
|
-
if (completedMatch) {
|
|
326
|
-
tabTitle = completedMatch[1].trim()
|
|
327
|
-
.replace(/\*+/g, '')
|
|
328
|
-
.replace(/\[.*?\]/g, '')
|
|
329
|
-
.trim();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
} catch (e) {}
|
|
333
|
-
|
|
334
|
-
if (!tabTitle) {
|
|
335
|
-
tabTitle = generateTabTitle(lastUserQuery, '');
|
|
336
|
-
}
|
|
453
|
+
tabTitle = generateTabTitle(lastUserQuery, '');
|
|
337
454
|
}
|
|
338
|
-
|
|
339
455
|
if (tabTitle) {
|
|
340
456
|
try {
|
|
341
|
-
const escapedTitle = tabTitle.replace(/'/g, "'\\''");
|
|
342
457
|
const { execSync } = await import('child_process');
|
|
458
|
+
const escapedTitle = tabTitle.replace(/'/g, "'\\''");
|
|
343
459
|
execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
|
|
344
460
|
execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
|
|
345
461
|
execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
|
|
@@ -348,85 +464,29 @@ async function main() {
|
|
|
348
464
|
console.error(`Failed to set tab title: ${e}`);
|
|
349
465
|
}
|
|
350
466
|
}
|
|
351
|
-
|
|
352
|
-
console.error(`User query: ${lastUserQuery || 'No query found'}`);
|
|
353
|
-
console.error(`Message: ${message || 'No completion message'}`);
|
|
354
|
-
|
|
355
|
-
// Final tab title override as the very last action
|
|
356
467
|
if (message) {
|
|
357
|
-
|
|
358
|
-
process.stderr.write(`\x1b]2;${finalTabTitle}\x07`);
|
|
468
|
+
process.stderr.write(`\x1b]2;${message.slice(0, 50)}\x07`);
|
|
359
469
|
}
|
|
360
470
|
|
|
361
|
-
// Send ntfy.sh notification
|
|
471
|
+
// Send ntfy.sh notification (fast, fire-and-forget)
|
|
362
472
|
if (message) {
|
|
363
473
|
await sendNtfyNotification(message);
|
|
364
474
|
} else {
|
|
365
475
|
await sendNtfyNotification('Session ended');
|
|
366
476
|
}
|
|
367
477
|
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
} else {
|
|
381
|
-
// If no structured work items found, at least add the completion message
|
|
382
|
-
if (message) {
|
|
383
|
-
addWorkToSessionNote(currentNotePath, [{
|
|
384
|
-
title: message,
|
|
385
|
-
completed: true
|
|
386
|
-
}]);
|
|
387
|
-
console.error(`Added completion message to session note`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// THEN: Finalize the note
|
|
392
|
-
const summary = message || 'Session completed.';
|
|
393
|
-
finalizeSessionNote(currentNotePath, summary);
|
|
394
|
-
console.error(`Session note finalized: ${basename(currentNotePath)}`);
|
|
395
|
-
|
|
396
|
-
// Update TODO.md ## Continue section so next session has context
|
|
397
|
-
try {
|
|
398
|
-
const stateLines: string[] = [];
|
|
399
|
-
stateLines.push(`Working directory: ${cwd}`);
|
|
400
|
-
if (workItems.length > 0) {
|
|
401
|
-
stateLines.push('');
|
|
402
|
-
stateLines.push('Work completed:');
|
|
403
|
-
for (const item of workItems.slice(0, 5)) {
|
|
404
|
-
stateLines.push(`- ${item.title}`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (message) {
|
|
408
|
-
stateLines.push('');
|
|
409
|
-
stateLines.push(`Last completed: ${message}`);
|
|
410
|
-
}
|
|
411
|
-
const state = stateLines.join('\n');
|
|
412
|
-
updateTodoContinue(cwd, basename(currentNotePath), state, 'session-end');
|
|
413
|
-
} catch (todoError) {
|
|
414
|
-
console.error(`Could not update TODO.md: ${todoError}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
} catch (noteError) {
|
|
418
|
-
console.error(`Could not finalize session note: ${noteError}`);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Move all session .jsonl files to sessions/ subdirectory
|
|
422
|
-
try {
|
|
423
|
-
const transcriptDir = dirname(transcriptPath);
|
|
424
|
-
const movedCount = moveSessionFilesToSessionsDir(transcriptDir);
|
|
425
|
-
if (movedCount > 0) {
|
|
426
|
-
console.error(`Moved ${movedCount} session file(s) to sessions/`);
|
|
427
|
-
}
|
|
428
|
-
} catch (moveError) {
|
|
429
|
-
console.error(`Could not move session files: ${moveError}`);
|
|
478
|
+
// -----------------------------------------------------------------------
|
|
479
|
+
// Relay heavy work to daemon — fall back to direct execution if unavailable
|
|
480
|
+
// -----------------------------------------------------------------------
|
|
481
|
+
const relayed = await enqueueWithDaemon({
|
|
482
|
+
transcriptPath,
|
|
483
|
+
cwd,
|
|
484
|
+
message,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (!relayed) {
|
|
488
|
+
console.error('STOP-HOOK: Using direct execution fallback.');
|
|
489
|
+
await executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery);
|
|
430
490
|
}
|
|
431
491
|
|
|
432
492
|
console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${new Date().toISOString()}\n`);
|