@tekmidian/pai 0.7.1 → 0.7.3
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-DJoesjez.mjs → daemon-D8ZxcFhU.mjs} +615 -5
- package/dist/daemon-D8ZxcFhU.mjs.map +1 -0
- package/dist/hooks/context-compression-hook.mjs +140 -22
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +74 -4
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +70 -1
- 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 +24 -2
- package/src/hooks/ts/lib/project-utils.ts +1 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +159 -37
- package/src/hooks/ts/session-start/load-project-context.ts +101 -3
- package/src/hooks/ts/stop/stop-hook.ts +66 -0
- package/dist/daemon-DJoesjez.mjs.map +0 -1
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
17
17
|
import { basename, dirname, join } from 'path';
|
|
18
18
|
import { tmpdir } from 'os';
|
|
19
|
+
import { connect } from 'net';
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
19
21
|
import {
|
|
20
22
|
sendNtfyNotification,
|
|
21
23
|
getCurrentNotePath,
|
|
22
24
|
createSessionNote,
|
|
23
25
|
appendCheckpoint,
|
|
24
26
|
addWorkToSessionNote,
|
|
27
|
+
isMeaningfulTitle,
|
|
25
28
|
findNotesDir,
|
|
26
29
|
renameSessionNote,
|
|
27
30
|
updateTodoContinue,
|
|
@@ -39,6 +42,9 @@ interface HookInput {
|
|
|
39
42
|
trigger?: string;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
const DAEMON_SOCKET = process.env.PAI_SOCKET ?? '/tmp/pai.sock';
|
|
46
|
+
const DAEMON_TIMEOUT_MS = 3_000;
|
|
47
|
+
|
|
42
48
|
/** Structured data extracted from a transcript in a single pass. */
|
|
43
49
|
interface TranscriptData {
|
|
44
50
|
userMessages: string[];
|
|
@@ -233,50 +239,63 @@ function formatSessionState(data: TranscriptData, cwd?: string): string | null {
|
|
|
233
239
|
// ---------------------------------------------------------------------------
|
|
234
240
|
|
|
235
241
|
function deriveTitle(data: TranscriptData): string {
|
|
236
|
-
|
|
242
|
+
// Collect candidates in priority order, then pick the first meaningful one.
|
|
243
|
+
const candidates: string[] = [];
|
|
237
244
|
|
|
238
|
-
// 1.
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
// 1. Work item titles (most descriptive of what was accomplished)
|
|
246
|
+
for (let i = data.workItems.length - 1; i >= 0; i--) {
|
|
247
|
+
candidates.push(data.workItems[i].title);
|
|
241
248
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
|
|
250
|
+
// 2. Summaries
|
|
251
|
+
for (let i = data.summaries.length - 1; i >= 0; i--) {
|
|
252
|
+
candidates.push(data.summaries[i]);
|
|
245
253
|
}
|
|
254
|
+
|
|
246
255
|
// 3. Last completed marker
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
if (data.lastCompleted && data.lastCompleted.length > 5) {
|
|
257
|
+
candidates.push(data.lastCompleted);
|
|
249
258
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
259
|
+
|
|
260
|
+
// 4. User messages (FIRST meaningful one, not last — first is more likely
|
|
261
|
+
// to describe the session's purpose; last is often system noise)
|
|
262
|
+
for (const msg of data.userMessages) {
|
|
263
|
+
const line = msg.split('\n')[0].trim();
|
|
264
|
+
if (line.length > 10 && line.length < 80 &&
|
|
265
|
+
!line.toLowerCase().startsWith('yes') &&
|
|
266
|
+
!line.toLowerCase().startsWith('ok')) {
|
|
267
|
+
candidates.push(line);
|
|
260
268
|
}
|
|
261
269
|
}
|
|
262
|
-
|
|
263
|
-
|
|
270
|
+
|
|
271
|
+
// 5. Derive from files modified (fallback)
|
|
272
|
+
if (data.filesModified.length > 0) {
|
|
264
273
|
const basenames = data.filesModified.slice(-5).map(f => {
|
|
265
274
|
const b = basename(f);
|
|
266
275
|
return b.replace(/\.[^.]+$/, '');
|
|
267
276
|
});
|
|
268
277
|
const unique = [...new Set(basenames)];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
278
|
+
candidates.push(
|
|
279
|
+
unique.length <= 3
|
|
280
|
+
? `Updated ${unique.join(', ')}`
|
|
281
|
+
: `Modified ${data.filesModified.length} files`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Pick the first candidate that passes the meaningfulness filter
|
|
286
|
+
for (const raw of candidates) {
|
|
287
|
+
const cleaned = raw
|
|
288
|
+
.replace(/[^\w\s-]/g, ' ') // Remove special chars
|
|
289
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
290
|
+
.trim()
|
|
291
|
+
.substring(0, 60);
|
|
292
|
+
if (cleaned.length >= 5 && isMeaningfulTitle(cleaned)) {
|
|
293
|
+
return cleaned;
|
|
294
|
+
}
|
|
272
295
|
}
|
|
273
296
|
|
|
274
|
-
//
|
|
275
|
-
return
|
|
276
|
-
.replace(/[^\w\s-]/g, ' ') // Remove special chars
|
|
277
|
-
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
278
|
-
.trim()
|
|
279
|
-
.substring(0, 60);
|
|
297
|
+
// All candidates were garbage — return empty (caller will not rename)
|
|
298
|
+
return '';
|
|
280
299
|
}
|
|
281
300
|
|
|
282
301
|
// ---------------------------------------------------------------------------
|
|
@@ -338,6 +357,85 @@ function saveCumulativeState(notesDir: string, data: TranscriptData, notePath: s
|
|
|
338
357
|
}
|
|
339
358
|
}
|
|
340
359
|
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Daemon IPC — enqueue session-summary work item
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Send a session-summary work item to the daemon via IPC.
|
|
366
|
+
* Returns true on success, false if the daemon is unreachable.
|
|
367
|
+
* Times out after DAEMON_TIMEOUT_MS to avoid blocking the hook.
|
|
368
|
+
*/
|
|
369
|
+
function enqueueSessionSummary(payload: {
|
|
370
|
+
cwd: string;
|
|
371
|
+
sessionId?: string;
|
|
372
|
+
transcriptPath?: string;
|
|
373
|
+
}): Promise<boolean> {
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
let done = false;
|
|
376
|
+
let buffer = '';
|
|
377
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
378
|
+
|
|
379
|
+
function finish(ok: boolean): void {
|
|
380
|
+
if (done) return;
|
|
381
|
+
done = true;
|
|
382
|
+
if (timer !== null) { clearTimeout(timer); timer = null; }
|
|
383
|
+
try { client.destroy(); } catch { /* ignore */ }
|
|
384
|
+
resolve(ok);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const client = connect(DAEMON_SOCKET, () => {
|
|
388
|
+
const msg = JSON.stringify({
|
|
389
|
+
id: randomUUID(),
|
|
390
|
+
method: 'work_queue_enqueue',
|
|
391
|
+
params: {
|
|
392
|
+
type: 'session-summary',
|
|
393
|
+
priority: 4, // lower priority than session-end (2)
|
|
394
|
+
payload: {
|
|
395
|
+
cwd: payload.cwd,
|
|
396
|
+
sessionId: payload.sessionId,
|
|
397
|
+
transcriptPath: payload.transcriptPath,
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
}) + '\n';
|
|
401
|
+
client.write(msg);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
client.on('data', (chunk: Buffer) => {
|
|
405
|
+
buffer += chunk.toString();
|
|
406
|
+
const nl = buffer.indexOf('\n');
|
|
407
|
+
if (nl === -1) return;
|
|
408
|
+
const line = buffer.slice(0, nl);
|
|
409
|
+
try {
|
|
410
|
+
const response = JSON.parse(line) as { ok: boolean; error?: string; result?: { id: string } };
|
|
411
|
+
if (response.ok) {
|
|
412
|
+
console.error(`PRE-COMPACT: Session summary enqueued with daemon (id=${response.result?.id}).`);
|
|
413
|
+
finish(true);
|
|
414
|
+
} else {
|
|
415
|
+
console.error(`PRE-COMPACT: Daemon rejected session-summary: ${response.error}`);
|
|
416
|
+
finish(false);
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
finish(false);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
client.on('error', (e: NodeJS.ErrnoException) => {
|
|
424
|
+
if (e.code === 'ENOENT' || e.code === 'ECONNREFUSED') {
|
|
425
|
+
console.error('PRE-COMPACT: Daemon not running — skipping session summary.');
|
|
426
|
+
} else {
|
|
427
|
+
console.error(`PRE-COMPACT: Daemon socket error: ${e.message}`);
|
|
428
|
+
}
|
|
429
|
+
finish(false);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
timer = setTimeout(() => {
|
|
433
|
+
console.error('PRE-COMPACT: Daemon IPC timed out — skipping session summary.');
|
|
434
|
+
finish(false);
|
|
435
|
+
}, DAEMON_TIMEOUT_MS);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
341
439
|
// ---------------------------------------------------------------------------
|
|
342
440
|
// Main
|
|
343
441
|
// ---------------------------------------------------------------------------
|
|
@@ -402,23 +500,32 @@ async function main() {
|
|
|
402
500
|
}
|
|
403
501
|
|
|
404
502
|
// -----------------------------------------------------------------
|
|
405
|
-
// Persist session state to numbered session note
|
|
503
|
+
// Persist session state to numbered session note.
|
|
504
|
+
// RULE: ONE note per session. NEVER create a new note during compaction.
|
|
505
|
+
// If the latest note is completed, reopen it (remove completed status)
|
|
506
|
+
// rather than creating a duplicate.
|
|
406
507
|
// -----------------------------------------------------------------
|
|
407
508
|
let notePath: string | null = null;
|
|
408
509
|
|
|
409
510
|
try {
|
|
410
511
|
notePath = getCurrentNotePath(notesInfo.path);
|
|
411
512
|
|
|
412
|
-
// If no note found, or the latest note is completed, create a new one
|
|
413
513
|
if (!notePath) {
|
|
514
|
+
// Truly no note exists at all — create one (first compaction of a session
|
|
515
|
+
// that started before PAI was installed, or corrupted notes dir)
|
|
414
516
|
console.error('No session note found — creating one for checkpoint');
|
|
415
|
-
notePath = createSessionNote(notesInfo.path, '
|
|
517
|
+
notePath = createSessionNote(notesInfo.path, 'Untitled Session');
|
|
416
518
|
} else {
|
|
519
|
+
// If the latest note is completed, reopen it for this session's checkpoints
|
|
520
|
+
// instead of creating a duplicate. This handles the case where session-stop
|
|
521
|
+
// finalized a note but the session continued (e.g., user said "end session"
|
|
522
|
+
// but kept working).
|
|
417
523
|
try {
|
|
418
|
-
|
|
419
|
-
if (noteContent.includes('**Status:** Completed')
|
|
420
|
-
|
|
421
|
-
notePath
|
|
524
|
+
let noteContent = readFileSync(notePath, 'utf-8');
|
|
525
|
+
if (noteContent.includes('**Status:** Completed')) {
|
|
526
|
+
noteContent = noteContent.replace('**Status:** Completed', '**Status:** In Progress');
|
|
527
|
+
writeFileSync(notePath, noteContent);
|
|
528
|
+
console.error(`Reopened completed note for continued session: ${basename(notePath)}`);
|
|
422
529
|
}
|
|
423
530
|
} catch { /* proceed with existing note */ }
|
|
424
531
|
}
|
|
@@ -516,6 +623,21 @@ async function main() {
|
|
|
516
623
|
}
|
|
517
624
|
}
|
|
518
625
|
|
|
626
|
+
// -----------------------------------------------------------------------
|
|
627
|
+
// Enqueue session-summary work item with daemon for AI-powered note generation
|
|
628
|
+
// -----------------------------------------------------------------------
|
|
629
|
+
if (hookInput?.cwd) {
|
|
630
|
+
try {
|
|
631
|
+
await enqueueSessionSummary({
|
|
632
|
+
cwd: hookInput.cwd,
|
|
633
|
+
sessionId: hookInput.session_id,
|
|
634
|
+
transcriptPath: hookInput.transcript_path,
|
|
635
|
+
});
|
|
636
|
+
} catch (err) {
|
|
637
|
+
console.error(`Could not enqueue session-summary: ${err}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
519
641
|
// Send ntfy.sh notification
|
|
520
642
|
const ntfyMessage = tokenCount > 0
|
|
521
643
|
? `Auto-pause: ~${Math.round(tokenCount / 1000)}k tokens`
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
20
|
-
import { join, basename, dirname } from 'path';
|
|
20
|
+
import { join, basename, dirname, resolve } from 'path';
|
|
21
|
+
import { homedir } from 'os';
|
|
21
22
|
import { execSync } from 'child_process';
|
|
22
23
|
import {
|
|
23
24
|
PAI_DIR,
|
|
@@ -72,6 +73,64 @@ function getRoutedNotesPath(): string | null {
|
|
|
72
73
|
return null;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Project signals that indicate a directory is a real project root.
|
|
78
|
+
*/
|
|
79
|
+
const PROJECT_SIGNALS = [
|
|
80
|
+
'.git',
|
|
81
|
+
'package.json',
|
|
82
|
+
'pubspec.yaml',
|
|
83
|
+
'Cargo.toml',
|
|
84
|
+
'go.mod',
|
|
85
|
+
'pyproject.toml',
|
|
86
|
+
'setup.py',
|
|
87
|
+
'build.gradle',
|
|
88
|
+
'pom.xml',
|
|
89
|
+
'composer.json',
|
|
90
|
+
'Gemfile',
|
|
91
|
+
'Makefile',
|
|
92
|
+
'CMakeLists.txt',
|
|
93
|
+
'tsconfig.json',
|
|
94
|
+
'CLAUDE.md',
|
|
95
|
+
join('Notes', 'PAI.md'),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns true if the given directory looks like a project root.
|
|
100
|
+
* Checks for the presence of well-known project signal files/dirs.
|
|
101
|
+
*/
|
|
102
|
+
function hasProjectSignals(dir: string): boolean {
|
|
103
|
+
for (const signal of PROJECT_SIGNALS) {
|
|
104
|
+
if (existsSync(join(dir, signal))) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns true if the directory should NOT be auto-registered.
|
|
111
|
+
* Guards: home directory, shallow paths, temp directories.
|
|
112
|
+
*/
|
|
113
|
+
function isGuardedPath(dir: string): boolean {
|
|
114
|
+
const home = homedir();
|
|
115
|
+
const resolved = resolve(dir);
|
|
116
|
+
|
|
117
|
+
// Never register the home directory itself
|
|
118
|
+
if (resolved === home) return true;
|
|
119
|
+
|
|
120
|
+
// Depth guard: require at least 3 path segments beyond root
|
|
121
|
+
// e.g. /Users/i052341/foo is depth 3 on macOS — reject it
|
|
122
|
+
const parts = resolved.split('/').filter(Boolean);
|
|
123
|
+
if (parts.length < 3) return true;
|
|
124
|
+
|
|
125
|
+
// Temp/system directories
|
|
126
|
+
const forbidden = ['/tmp', '/var', '/private/tmp', '/private/var/folders'];
|
|
127
|
+
for (const prefix of forbidden) {
|
|
128
|
+
if (resolved === prefix || resolved.startsWith(prefix + '/')) return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
75
134
|
interface HookInput {
|
|
76
135
|
session_id: string;
|
|
77
136
|
cwd: string;
|
|
@@ -311,9 +370,48 @@ async function main() {
|
|
|
311
370
|
};
|
|
312
371
|
|
|
313
372
|
if (detected.error === 'no_match') {
|
|
314
|
-
|
|
373
|
+
// Attempt auto-registration if the directory looks like a real project
|
|
374
|
+
let autoRegistered = false;
|
|
375
|
+
|
|
376
|
+
if (!isGuardedPath(cwd) && hasProjectSignals(cwd)) {
|
|
377
|
+
try {
|
|
378
|
+
execFileSync(paiBin, ['project', 'add', cwd], {
|
|
379
|
+
encoding: 'utf-8',
|
|
380
|
+
env: process.env,
|
|
381
|
+
});
|
|
382
|
+
console.error(`PAI auto-registered project at: ${cwd}`);
|
|
383
|
+
|
|
384
|
+
// Re-run detect to get the proper detection result
|
|
385
|
+
try {
|
|
386
|
+
const raw2 = execFileSync(paiBin, ['project', 'detect', '--json', cwd], {
|
|
387
|
+
encoding: 'utf-8',
|
|
388
|
+
env: process.env,
|
|
389
|
+
}).trim();
|
|
390
|
+
|
|
391
|
+
if (raw2) {
|
|
392
|
+
const detected2 = JSON.parse(raw2) as typeof detected;
|
|
393
|
+
if (detected2.slug) {
|
|
394
|
+
const name2 = detected2.display_name || detected2.slug;
|
|
395
|
+
console.error(`PAI auto-registered: "${detected2.slug}" (${detected2.match_type})`);
|
|
396
|
+
paiProjectBlock = `PAI Project Registry: ${name2} (slug: ${detected2.slug}) [AUTO-REGISTERED]
|
|
397
|
+
Match: ${detected2.match_type ?? 'exact'} | Sessions: 0`;
|
|
398
|
+
autoRegistered = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (detectErr) {
|
|
402
|
+
console.error('PAI auto-registration: project added but re-detect failed:', detectErr);
|
|
403
|
+
autoRegistered = true; // project IS registered, just can't load context
|
|
404
|
+
}
|
|
405
|
+
} catch (addErr) {
|
|
406
|
+
console.error('PAI auto-registration failed (project add):', addErr);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!autoRegistered) {
|
|
411
|
+
paiProjectBlock = `PAI Project Registry: No registered project matches this directory.
|
|
315
412
|
Run "pai project add ." to register this project, or use /route to tag the session.`;
|
|
316
|
-
|
|
413
|
+
console.error('PAI detect: no match for', cwd);
|
|
414
|
+
}
|
|
317
415
|
} else if (detected.slug) {
|
|
318
416
|
const name = detected.display_name || detected.slug;
|
|
319
417
|
const nameSlug = ` (slug: ${detected.slug})`;
|
|
@@ -146,6 +146,67 @@ function enqueueWithDaemon(payload: {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Enqueue a session-summary work item with the daemon for AI-powered note generation.
|
|
151
|
+
* Non-blocking — if daemon is unavailable, silently skips (the mechanical note is sufficient).
|
|
152
|
+
*
|
|
153
|
+
* Note: we intentionally omit transcriptPath here to let the worker call findLatestJsonl()
|
|
154
|
+
* itself. At session-end, Claude Code may still be moving the JSONL to sessions/, so a
|
|
155
|
+
* stale path passed from the hook could point to a file that no longer exists.
|
|
156
|
+
*/
|
|
157
|
+
function enqueueSessionSummaryWithDaemon(payload: {
|
|
158
|
+
cwd: string;
|
|
159
|
+
}): Promise<boolean> {
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
let done = false;
|
|
162
|
+
let buffer = '';
|
|
163
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
164
|
+
|
|
165
|
+
function finish(ok: boolean): void {
|
|
166
|
+
if (done) return;
|
|
167
|
+
done = true;
|
|
168
|
+
if (timer !== null) { clearTimeout(timer); timer = null; }
|
|
169
|
+
try { client.destroy(); } catch { /* ignore */ }
|
|
170
|
+
resolve(ok);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const client = connect(DAEMON_SOCKET, () => {
|
|
174
|
+
const msg = JSON.stringify({
|
|
175
|
+
id: randomUUID(),
|
|
176
|
+
method: 'work_queue_enqueue',
|
|
177
|
+
params: {
|
|
178
|
+
type: 'session-summary',
|
|
179
|
+
priority: 4,
|
|
180
|
+
payload: {
|
|
181
|
+
cwd: payload.cwd,
|
|
182
|
+
force: true,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}) + '\n';
|
|
186
|
+
client.write(msg);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
client.on('data', (chunk: Buffer) => {
|
|
190
|
+
buffer += chunk.toString();
|
|
191
|
+
const nl = buffer.indexOf('\n');
|
|
192
|
+
if (nl === -1) return;
|
|
193
|
+
const line = buffer.slice(0, nl);
|
|
194
|
+
try {
|
|
195
|
+
const response = JSON.parse(line) as { ok: boolean; result?: { id: string } };
|
|
196
|
+
if (response.ok) {
|
|
197
|
+
console.error(`STOP-HOOK: Session summary enqueued (id=${response.result?.id}).`);
|
|
198
|
+
}
|
|
199
|
+
} catch { /* ignore */ }
|
|
200
|
+
finish(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
client.on('error', () => finish(false));
|
|
204
|
+
client.on('end', () => { if (!done) finish(false); });
|
|
205
|
+
|
|
206
|
+
timer = setTimeout(() => finish(false), DAEMON_TIMEOUT_MS);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
// ---------------------------------------------------------------------------
|
|
150
211
|
// Direct execution — fallback path (original stop-hook logic)
|
|
151
212
|
// ---------------------------------------------------------------------------
|
|
@@ -489,6 +550,11 @@ async function main() {
|
|
|
489
550
|
await executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery);
|
|
490
551
|
}
|
|
491
552
|
|
|
553
|
+
// Also enqueue a session-summary for AI-powered note generation.
|
|
554
|
+
// We omit transcriptPath so the worker resolves it via findLatestJsonl(),
|
|
555
|
+
// avoiding a race where the session-end hook moves the JSONL before the worker reads it.
|
|
556
|
+
await enqueueSessionSummaryWithDaemon({ cwd });
|
|
557
|
+
|
|
492
558
|
console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${new Date().toISOString()}\n`);
|
|
493
559
|
}
|
|
494
560
|
|