@tekmidian/pai 0.5.2 → 0.5.4
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 +84 -0
- package/README.md +38 -0
- package/dist/cli/index.mjs +66 -6
- package/dist/cli/index.mjs.map +1 -1
- package/dist/hooks/cleanup-session-files.mjs.map +1 -1
- package/dist/hooks/context-compression-hook.mjs +452 -35
- package/dist/hooks/context-compression-hook.mjs.map +3 -3
- package/dist/hooks/initialize-session.mjs.map +1 -1
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/post-compact-inject.mjs +51 -0
- package/dist/hooks/post-compact-inject.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs.map +2 -2
- package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
- package/package.json +1 -1
- package/src/hooks/ts/lib/project-utils.ts +38 -4
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +307 -71
- package/src/hooks/ts/session-start/post-compact-inject.ts +85 -0
|
@@ -7,8 +7,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/hooks/ts/pre-compact/context-compression-hook.ts
|
|
10
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
11
|
-
import {
|
|
10
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
11
|
+
import { basename as basename2, dirname, join as join3 } from "path";
|
|
12
|
+
import { tmpdir } from "os";
|
|
12
13
|
|
|
13
14
|
// src/hooks/ts/lib/project-utils.ts
|
|
14
15
|
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
|
|
@@ -74,6 +75,33 @@ validatePAIStructure();
|
|
|
74
75
|
|
|
75
76
|
// src/hooks/ts/lib/project-utils.ts
|
|
76
77
|
var PROJECTS_DIR = join2(PAI_DIR, "projects");
|
|
78
|
+
function encodePath(path) {
|
|
79
|
+
return path.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
|
|
80
|
+
}
|
|
81
|
+
function getProjectDir(cwd) {
|
|
82
|
+
const encoded = encodePath(cwd);
|
|
83
|
+
return join2(PROJECTS_DIR, encoded);
|
|
84
|
+
}
|
|
85
|
+
function getNotesDir(cwd) {
|
|
86
|
+
return join2(getProjectDir(cwd), "Notes");
|
|
87
|
+
}
|
|
88
|
+
function findNotesDir(cwd) {
|
|
89
|
+
const cwdBasename = basename(cwd).toLowerCase();
|
|
90
|
+
if (cwdBasename === "notes" && existsSync2(cwd)) {
|
|
91
|
+
return { path: cwd, isLocal: true };
|
|
92
|
+
}
|
|
93
|
+
const localPaths = [
|
|
94
|
+
join2(cwd, "Notes"),
|
|
95
|
+
join2(cwd, "notes"),
|
|
96
|
+
join2(cwd, ".claude", "Notes")
|
|
97
|
+
];
|
|
98
|
+
for (const path of localPaths) {
|
|
99
|
+
if (existsSync2(path)) {
|
|
100
|
+
return { path, isLocal: true };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { path: getNotesDir(cwd), isLocal: false };
|
|
104
|
+
}
|
|
77
105
|
function isWhatsAppEnabled() {
|
|
78
106
|
try {
|
|
79
107
|
const { homedir: homedir2 } = __require("os");
|
|
@@ -122,6 +150,32 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
122
150
|
console.error("ntfy.sh notification failed after all retries");
|
|
123
151
|
return false;
|
|
124
152
|
}
|
|
153
|
+
function getMonthDir(notesDir) {
|
|
154
|
+
const now = /* @__PURE__ */ new Date();
|
|
155
|
+
const year = String(now.getFullYear());
|
|
156
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
157
|
+
const monthDir = join2(notesDir, year, month);
|
|
158
|
+
if (!existsSync2(monthDir)) {
|
|
159
|
+
mkdirSync(monthDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
return monthDir;
|
|
162
|
+
}
|
|
163
|
+
function getNextNoteNumber(notesDir) {
|
|
164
|
+
const monthDir = getMonthDir(notesDir);
|
|
165
|
+
const files = readdirSync(monthDir).filter((f) => f.match(/^\d{3,4}[\s_-]/)).filter((f) => f.endsWith(".md")).sort();
|
|
166
|
+
if (files.length === 0) {
|
|
167
|
+
return "0001";
|
|
168
|
+
}
|
|
169
|
+
let maxNumber = 0;
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
const digitMatch = file.match(/^(\d+)/);
|
|
172
|
+
if (digitMatch) {
|
|
173
|
+
const num = parseInt(digitMatch[1], 10);
|
|
174
|
+
if (num > maxNumber) maxNumber = num;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return String(maxNumber + 1).padStart(4, "0");
|
|
178
|
+
}
|
|
125
179
|
function getCurrentNotePath(notesDir) {
|
|
126
180
|
if (!existsSync2(notesDir)) {
|
|
127
181
|
return null;
|
|
@@ -150,10 +204,77 @@ function getCurrentNotePath(notesDir) {
|
|
|
150
204
|
if (prevFound) return prevFound;
|
|
151
205
|
return findLatestIn(notesDir);
|
|
152
206
|
}
|
|
207
|
+
function createSessionNote(notesDir, description) {
|
|
208
|
+
const noteNumber = getNextNoteNumber(notesDir);
|
|
209
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
210
|
+
const safeDescription = "New Session";
|
|
211
|
+
const monthDir = getMonthDir(notesDir);
|
|
212
|
+
const filename = `${noteNumber} - ${date} - ${safeDescription}.md`;
|
|
213
|
+
const filepath = join2(monthDir, filename);
|
|
214
|
+
const content = `# Session ${noteNumber}: ${description}
|
|
215
|
+
|
|
216
|
+
**Date:** ${date}
|
|
217
|
+
**Status:** In Progress
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Work Done
|
|
222
|
+
|
|
223
|
+
<!-- PAI will add completed work here during session -->
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Next Steps
|
|
228
|
+
|
|
229
|
+
<!-- To be filled at session end -->
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
**Tags:** #Session
|
|
234
|
+
`;
|
|
235
|
+
writeFileSync(filepath, content);
|
|
236
|
+
console.error(`Created session note: ${filename}`);
|
|
237
|
+
return filepath;
|
|
238
|
+
}
|
|
153
239
|
function appendCheckpoint(notePath, checkpoint) {
|
|
154
240
|
if (!existsSync2(notePath)) {
|
|
155
|
-
console.error(`Note file not found: ${notePath}`);
|
|
156
|
-
|
|
241
|
+
console.error(`Note file not found, recreating: ${notePath}`);
|
|
242
|
+
try {
|
|
243
|
+
const parentDir = join2(notePath, "..");
|
|
244
|
+
if (!existsSync2(parentDir)) {
|
|
245
|
+
mkdirSync(parentDir, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
const noteFilename = basename(notePath);
|
|
248
|
+
const numberMatch = noteFilename.match(/^(\d+)/);
|
|
249
|
+
const noteNumber = numberMatch ? numberMatch[1] : "0000";
|
|
250
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
251
|
+
const content2 = `# Session ${noteNumber}: Recovered
|
|
252
|
+
|
|
253
|
+
**Date:** ${date}
|
|
254
|
+
**Status:** In Progress
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Work Done
|
|
259
|
+
|
|
260
|
+
<!-- PAI will add completed work here during session -->
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Next Steps
|
|
265
|
+
|
|
266
|
+
<!-- To be filled at session end -->
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
**Tags:** #Session
|
|
271
|
+
`;
|
|
272
|
+
writeFileSync(notePath, content2);
|
|
273
|
+
console.error(`Recreated session note: ${noteFilename}`);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error(`Failed to recreate note: ${err}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
157
278
|
}
|
|
158
279
|
const content = readFileSync2(notePath, "utf-8");
|
|
159
280
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -172,6 +293,43 @@ ${checkpoint}
|
|
|
172
293
|
writeFileSync(notePath, newContent);
|
|
173
294
|
console.error(`Checkpoint added to: ${basename(notePath)}`);
|
|
174
295
|
}
|
|
296
|
+
function addWorkToSessionNote(notePath, workItems, sectionTitle) {
|
|
297
|
+
if (!existsSync2(notePath)) {
|
|
298
|
+
console.error(`Note file not found: ${notePath}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
let content = readFileSync2(notePath, "utf-8");
|
|
302
|
+
let workText = "";
|
|
303
|
+
if (sectionTitle) {
|
|
304
|
+
workText += `
|
|
305
|
+
### ${sectionTitle}
|
|
306
|
+
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
for (const item of workItems) {
|
|
310
|
+
const checkbox = item.completed !== false ? "[x]" : "[ ]";
|
|
311
|
+
workText += `- ${checkbox} **${item.title}**
|
|
312
|
+
`;
|
|
313
|
+
if (item.details && item.details.length > 0) {
|
|
314
|
+
for (const detail of item.details) {
|
|
315
|
+
workText += ` - ${detail}
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const workDoneMatch = content.match(/## Work Done\n\n(<!-- .*? -->)?/);
|
|
321
|
+
if (workDoneMatch) {
|
|
322
|
+
const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;
|
|
323
|
+
content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);
|
|
324
|
+
} else {
|
|
325
|
+
const nextStepsIndex = content.indexOf("## Next Steps");
|
|
326
|
+
if (nextStepsIndex !== -1) {
|
|
327
|
+
content = content.substring(0, nextStepsIndex) + workText + "\n" + content.substring(nextStepsIndex);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
writeFileSync(notePath, content);
|
|
331
|
+
console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
|
|
332
|
+
}
|
|
175
333
|
function calculateSessionTokens(jsonlPath) {
|
|
176
334
|
if (!existsSync2(jsonlPath)) {
|
|
177
335
|
return 0;
|
|
@@ -199,8 +357,94 @@ function calculateSessionTokens(jsonlPath) {
|
|
|
199
357
|
return 0;
|
|
200
358
|
}
|
|
201
359
|
}
|
|
360
|
+
function findTodoPath(cwd) {
|
|
361
|
+
const localPaths = [
|
|
362
|
+
join2(cwd, "TODO.md"),
|
|
363
|
+
join2(cwd, "notes", "TODO.md"),
|
|
364
|
+
join2(cwd, "Notes", "TODO.md"),
|
|
365
|
+
join2(cwd, ".claude", "TODO.md")
|
|
366
|
+
];
|
|
367
|
+
for (const path of localPaths) {
|
|
368
|
+
if (existsSync2(path)) {
|
|
369
|
+
return path;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return join2(getNotesDir(cwd), "TODO.md");
|
|
373
|
+
}
|
|
374
|
+
function ensureTodoMd(cwd) {
|
|
375
|
+
const todoPath = findTodoPath(cwd);
|
|
376
|
+
if (!existsSync2(todoPath)) {
|
|
377
|
+
const parentDir = join2(todoPath, "..");
|
|
378
|
+
if (!existsSync2(parentDir)) {
|
|
379
|
+
mkdirSync(parentDir, { recursive: true });
|
|
380
|
+
}
|
|
381
|
+
const content = `# TODO
|
|
382
|
+
|
|
383
|
+
## Current Session
|
|
384
|
+
|
|
385
|
+
- [ ] (Tasks will be tracked here)
|
|
386
|
+
|
|
387
|
+
## Backlog
|
|
388
|
+
|
|
389
|
+
- [ ] (Future tasks)
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
394
|
+
`;
|
|
395
|
+
writeFileSync(todoPath, content);
|
|
396
|
+
console.error(`Created TODO.md: ${todoPath}`);
|
|
397
|
+
}
|
|
398
|
+
return todoPath;
|
|
399
|
+
}
|
|
400
|
+
function addTodoCheckpoint(cwd, checkpoint) {
|
|
401
|
+
const todoPath = ensureTodoMd(cwd);
|
|
402
|
+
let content = readFileSync2(todoPath, "utf-8");
|
|
403
|
+
content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, "");
|
|
404
|
+
const checkpointText = `
|
|
405
|
+
**Checkpoint (${(/* @__PURE__ */ new Date()).toISOString()}):** ${checkpoint}
|
|
406
|
+
|
|
407
|
+
`;
|
|
408
|
+
const backlogIndex = content.indexOf("## Backlog");
|
|
409
|
+
if (backlogIndex !== -1) {
|
|
410
|
+
content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
|
|
411
|
+
} else {
|
|
412
|
+
const continueIndex = content.indexOf("## Continue");
|
|
413
|
+
if (continueIndex !== -1) {
|
|
414
|
+
const afterContinue = content.indexOf("\n---", continueIndex);
|
|
415
|
+
if (afterContinue !== -1) {
|
|
416
|
+
const insertAt = afterContinue + 4;
|
|
417
|
+
content = content.substring(0, insertAt) + "\n" + checkpointText + content.substring(insertAt);
|
|
418
|
+
} else {
|
|
419
|
+
content = content.trimEnd() + "\n" + checkpointText;
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
content = content.trimEnd() + "\n" + checkpointText;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
content = content.trimEnd() + `
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
430
|
+
`;
|
|
431
|
+
writeFileSync(todoPath, content);
|
|
432
|
+
console.error(`Checkpoint added to TODO.md`);
|
|
433
|
+
}
|
|
202
434
|
|
|
203
435
|
// src/hooks/ts/pre-compact/context-compression-hook.ts
|
|
436
|
+
function contentToText(content) {
|
|
437
|
+
if (typeof content === "string") return content;
|
|
438
|
+
if (Array.isArray(content)) {
|
|
439
|
+
return content.map((c) => {
|
|
440
|
+
if (typeof c === "string") return c;
|
|
441
|
+
if (c?.text) return c.text;
|
|
442
|
+
if (c?.content) return String(c.content);
|
|
443
|
+
return "";
|
|
444
|
+
}).join(" ").trim();
|
|
445
|
+
}
|
|
446
|
+
return "";
|
|
447
|
+
}
|
|
204
448
|
function getTranscriptStats(transcriptPath) {
|
|
205
449
|
try {
|
|
206
450
|
const content = readFileSync3(transcriptPath, "utf-8");
|
|
@@ -208,32 +452,165 @@ function getTranscriptStats(transcriptPath) {
|
|
|
208
452
|
let userMessages = 0;
|
|
209
453
|
let assistantMessages = 0;
|
|
210
454
|
for (const line of lines) {
|
|
211
|
-
if (line.trim())
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
assistantMessages++;
|
|
218
|
-
}
|
|
219
|
-
} catch {
|
|
220
|
-
}
|
|
455
|
+
if (!line.trim()) continue;
|
|
456
|
+
try {
|
|
457
|
+
const entry = JSON.parse(line);
|
|
458
|
+
if (entry.type === "user") userMessages++;
|
|
459
|
+
else if (entry.type === "assistant") assistantMessages++;
|
|
460
|
+
} catch {
|
|
221
461
|
}
|
|
222
462
|
}
|
|
223
463
|
const totalMessages = userMessages + assistantMessages;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} catch (error) {
|
|
464
|
+
return { messageCount: totalMessages, isLarge: totalMessages > 50 };
|
|
465
|
+
} catch {
|
|
227
466
|
return { messageCount: 0, isLarge: false };
|
|
228
467
|
}
|
|
229
468
|
}
|
|
469
|
+
function extractSessionState(transcriptPath, cwd) {
|
|
470
|
+
try {
|
|
471
|
+
const raw = readFileSync3(transcriptPath, "utf-8");
|
|
472
|
+
const lines = raw.trim().split("\n");
|
|
473
|
+
const userMessages = [];
|
|
474
|
+
const summaries = [];
|
|
475
|
+
const captures = [];
|
|
476
|
+
let lastCompleted = "";
|
|
477
|
+
const filesModified = /* @__PURE__ */ new Set();
|
|
478
|
+
for (const line of lines) {
|
|
479
|
+
if (!line.trim()) continue;
|
|
480
|
+
let entry;
|
|
481
|
+
try {
|
|
482
|
+
entry = JSON.parse(line);
|
|
483
|
+
} catch {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (entry.type === "user" && entry.message?.content) {
|
|
487
|
+
const text = contentToText(entry.message.content).slice(0, 300);
|
|
488
|
+
if (text) userMessages.push(text);
|
|
489
|
+
}
|
|
490
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
491
|
+
const text = contentToText(entry.message.content);
|
|
492
|
+
const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
|
|
493
|
+
if (summaryMatch) {
|
|
494
|
+
const s = summaryMatch[1].trim();
|
|
495
|
+
if (s.length > 5 && !summaries.includes(s)) summaries.push(s);
|
|
496
|
+
}
|
|
497
|
+
const captureMatch = text.match(/CAPTURE:\s*(.+?)(?:\n|$)/i);
|
|
498
|
+
if (captureMatch) {
|
|
499
|
+
const c = captureMatch[1].trim();
|
|
500
|
+
if (c.length > 5 && !captures.includes(c)) captures.push(c);
|
|
501
|
+
}
|
|
502
|
+
const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
503
|
+
if (completedMatch) {
|
|
504
|
+
lastCompleted = completedMatch[1].trim().replace(/\*+/g, "");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (entry.type === "assistant" && entry.message?.content && Array.isArray(entry.message.content)) {
|
|
508
|
+
for (const block of entry.message.content) {
|
|
509
|
+
if (block.type === "tool_use") {
|
|
510
|
+
const tool = block.name;
|
|
511
|
+
if ((tool === "Edit" || tool === "Write") && block.input?.file_path) {
|
|
512
|
+
filesModified.add(block.input.file_path);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const parts = [];
|
|
519
|
+
if (cwd) {
|
|
520
|
+
parts.push(`Working directory: ${cwd}`);
|
|
521
|
+
}
|
|
522
|
+
const recentUser = userMessages.slice(-3);
|
|
523
|
+
if (recentUser.length > 0) {
|
|
524
|
+
parts.push("\nRecent user requests:");
|
|
525
|
+
for (const msg of recentUser) {
|
|
526
|
+
const firstLine = msg.split("\n")[0].slice(0, 200);
|
|
527
|
+
parts.push(`- ${firstLine}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const recentSummaries = summaries.slice(-3);
|
|
531
|
+
if (recentSummaries.length > 0) {
|
|
532
|
+
parts.push("\nWork summaries:");
|
|
533
|
+
for (const s of recentSummaries) {
|
|
534
|
+
parts.push(`- ${s.slice(0, 150)}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const recentCaptures = captures.slice(-5);
|
|
538
|
+
if (recentCaptures.length > 0) {
|
|
539
|
+
parts.push("\nCaptured context:");
|
|
540
|
+
for (const c of recentCaptures) {
|
|
541
|
+
parts.push(`- ${c.slice(0, 150)}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const files = Array.from(filesModified).slice(-10);
|
|
545
|
+
if (files.length > 0) {
|
|
546
|
+
parts.push("\nFiles modified this session:");
|
|
547
|
+
for (const f of files) {
|
|
548
|
+
parts.push(`- ${f}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (lastCompleted) {
|
|
552
|
+
parts.push(`
|
|
553
|
+
Last completed: ${lastCompleted.slice(0, 150)}`);
|
|
554
|
+
}
|
|
555
|
+
const result = parts.join("\n");
|
|
556
|
+
return result.length > 50 ? result : null;
|
|
557
|
+
} catch (err) {
|
|
558
|
+
console.error(`extractSessionState error: ${err}`);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function extractWorkFromTranscript(transcriptPath) {
|
|
563
|
+
try {
|
|
564
|
+
const raw = readFileSync3(transcriptPath, "utf-8");
|
|
565
|
+
const lines = raw.trim().split("\n");
|
|
566
|
+
const workItems = [];
|
|
567
|
+
const seenSummaries = /* @__PURE__ */ new Set();
|
|
568
|
+
for (const line of lines) {
|
|
569
|
+
if (!line.trim()) continue;
|
|
570
|
+
let entry;
|
|
571
|
+
try {
|
|
572
|
+
entry = JSON.parse(line);
|
|
573
|
+
} catch {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
577
|
+
const text = contentToText(entry.message.content);
|
|
578
|
+
const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
|
|
579
|
+
if (summaryMatch) {
|
|
580
|
+
const summary = summaryMatch[1].trim();
|
|
581
|
+
if (summary && !seenSummaries.has(summary) && summary.length > 5) {
|
|
582
|
+
seenSummaries.add(summary);
|
|
583
|
+
const details = [];
|
|
584
|
+
const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
|
|
585
|
+
if (actionsMatch) {
|
|
586
|
+
const actionLines = actionsMatch[1].split("\n").map((l) => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 3 && l.length < 100);
|
|
587
|
+
details.push(...actionLines.slice(0, 3));
|
|
588
|
+
}
|
|
589
|
+
workItems.push({ title: summary, details: details.length > 0 ? details : void 0, completed: true });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
593
|
+
if (completedMatch && workItems.length === 0) {
|
|
594
|
+
const completed = completedMatch[1].trim().replace(/\*+/g, "");
|
|
595
|
+
if (completed && !seenSummaries.has(completed) && completed.length > 5) {
|
|
596
|
+
seenSummaries.add(completed);
|
|
597
|
+
workItems.push({ title: completed, completed: true });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return workItems;
|
|
603
|
+
} catch {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
230
607
|
async function main() {
|
|
231
608
|
let hookInput = null;
|
|
232
609
|
try {
|
|
233
610
|
const decoder = new TextDecoder();
|
|
234
611
|
let input = "";
|
|
235
612
|
const timeoutPromise = new Promise((resolve2) => {
|
|
236
|
-
setTimeout(
|
|
613
|
+
setTimeout(resolve2, 500);
|
|
237
614
|
});
|
|
238
615
|
const readPromise = (async () => {
|
|
239
616
|
for await (const chunk of process.stdin) {
|
|
@@ -244,34 +621,74 @@ async function main() {
|
|
|
244
621
|
if (input.trim()) {
|
|
245
622
|
hookInput = JSON.parse(input);
|
|
246
623
|
}
|
|
247
|
-
} catch
|
|
624
|
+
} catch {
|
|
248
625
|
}
|
|
249
|
-
const compactType = hookInput?.compact_type || "auto";
|
|
250
|
-
let message = "Compressing context to continue";
|
|
626
|
+
const compactType = hookInput?.compact_type || hookInput?.trigger || "auto";
|
|
251
627
|
let tokenCount = 0;
|
|
252
|
-
if (hookInput
|
|
628
|
+
if (hookInput?.transcript_path) {
|
|
253
629
|
const stats = getTranscriptStats(hookInput.transcript_path);
|
|
254
630
|
tokenCount = calculateSessionTokens(hookInput.transcript_path);
|
|
255
631
|
const tokenDisplay = tokenCount > 1e3 ? `${Math.round(tokenCount / 1e3)}k` : String(tokenCount);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
632
|
+
const state = extractSessionState(hookInput.transcript_path, hookInput.cwd);
|
|
633
|
+
try {
|
|
634
|
+
const notesInfo = hookInput.cwd ? findNotesDir(hookInput.cwd) : { path: join3(dirname(hookInput.transcript_path), "Notes"), isLocal: false };
|
|
635
|
+
let notePath = getCurrentNotePath(notesInfo.path);
|
|
636
|
+
if (!notePath) {
|
|
637
|
+
console.error("No session note found \u2014 creating one for checkpoint");
|
|
638
|
+
notePath = createSessionNote(notesInfo.path, "Recovered Session");
|
|
259
639
|
} else {
|
|
260
|
-
|
|
640
|
+
try {
|
|
641
|
+
const noteContent = readFileSync3(notePath, "utf-8");
|
|
642
|
+
if (noteContent.includes("**Status:** Completed") || noteContent.includes("**Completed:**")) {
|
|
643
|
+
console.error(`Latest note is completed (${basename2(notePath)}) \u2014 creating new one`);
|
|
644
|
+
notePath = createSessionNote(notesInfo.path, "Continued Session");
|
|
645
|
+
}
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
261
648
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
console.error(`Checkpoint saved before compression: ${basename2(currentNotePath)}`);
|
|
649
|
+
const checkpointBody = state ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.
|
|
650
|
+
|
|
651
|
+
${state}` : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
|
|
652
|
+
appendCheckpoint(notePath, checkpointBody);
|
|
653
|
+
const workItems = extractWorkFromTranscript(hookInput.transcript_path);
|
|
654
|
+
if (workItems.length > 0) {
|
|
655
|
+
addWorkToSessionNote(notePath, workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
|
|
656
|
+
console.error(`Added ${workItems.length} work item(s) to session note`);
|
|
271
657
|
}
|
|
658
|
+
console.error(`Rich checkpoint saved: ${basename2(notePath)}`);
|
|
272
659
|
} catch (noteError) {
|
|
273
660
|
console.error(`Could not save checkpoint: ${noteError}`);
|
|
274
661
|
}
|
|
662
|
+
if (hookInput.cwd && state) {
|
|
663
|
+
try {
|
|
664
|
+
addTodoCheckpoint(hookInput.cwd, `Pre-compact checkpoint (~${tokenDisplay} tokens):
|
|
665
|
+
${state}`);
|
|
666
|
+
console.error("TODO.md checkpoint added");
|
|
667
|
+
} catch (todoError) {
|
|
668
|
+
console.error(`Could not update TODO.md: ${todoError}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (state && hookInput.session_id) {
|
|
672
|
+
const injection = [
|
|
673
|
+
"<system-reminder>",
|
|
674
|
+
`SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,
|
|
675
|
+
"",
|
|
676
|
+
state,
|
|
677
|
+
"",
|
|
678
|
+
"IMPORTANT: This session state was captured before context compaction.",
|
|
679
|
+
"Use it to maintain continuity. Continue the conversation from where",
|
|
680
|
+
"it left off without asking the user to repeat themselves.",
|
|
681
|
+
"Continue with the last task that you were asked to work on.",
|
|
682
|
+
"</system-reminder>"
|
|
683
|
+
].join("\n");
|
|
684
|
+
try {
|
|
685
|
+
const stateFile = join3(tmpdir(), `pai-compact-state-${hookInput.session_id}.txt`);
|
|
686
|
+
writeFileSync2(stateFile, injection, "utf-8");
|
|
687
|
+
console.error(`Session state saved to ${stateFile} (${injection.length} chars)`);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
console.error(`Failed to save state file: ${err}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
275
692
|
}
|
|
276
693
|
const ntfyMessage = tokenCount > 0 ? `Auto-pause: ~${Math.round(tokenCount / 1e3)}k tokens` : "Context compressing";
|
|
277
694
|
await sendNtfyNotification(ntfyMessage);
|