@tekmidian/pai 0.5.1 → 0.5.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.
Files changed (28) hide show
  1. package/ARCHITECTURE.md +84 -0
  2. package/README.md +94 -0
  3. package/dist/cli/index.mjs +72 -12
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/daemon/index.mjs +3 -3
  6. package/dist/{daemon-a1W4KgFq.mjs → daemon-D9evGlgR.mjs} +8 -8
  7. package/dist/{daemon-a1W4KgFq.mjs.map → daemon-D9evGlgR.mjs.map} +1 -1
  8. package/dist/{factory-CeXQzlwn.mjs → factory-Bzcy70G9.mjs} +3 -3
  9. package/dist/{factory-CeXQzlwn.mjs.map → factory-Bzcy70G9.mjs.map} +1 -1
  10. package/dist/hooks/context-compression-hook.mjs +333 -33
  11. package/dist/hooks/context-compression-hook.mjs.map +3 -3
  12. package/dist/hooks/post-compact-inject.mjs +51 -0
  13. package/dist/hooks/post-compact-inject.mjs.map +7 -0
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +1 -1
  16. package/dist/{indexer-CKQcgKsz.mjs → indexer-CMPOiY1r.mjs} +22 -1
  17. package/dist/{indexer-CKQcgKsz.mjs.map → indexer-CMPOiY1r.mjs.map} +1 -1
  18. package/dist/{indexer-backend-DQO-FqAI.mjs → indexer-backend-CIMXedqk.mjs} +26 -9
  19. package/dist/indexer-backend-CIMXedqk.mjs.map +1 -0
  20. package/dist/{postgres-CIxeqf_n.mjs → postgres-FXrHDPcE.mjs} +36 -13
  21. package/dist/postgres-FXrHDPcE.mjs.map +1 -0
  22. package/dist/{sqlite-CymLKiDE.mjs → sqlite-WWBq7_2C.mjs} +18 -1
  23. package/dist/{sqlite-CymLKiDE.mjs.map → sqlite-WWBq7_2C.mjs.map} +1 -1
  24. package/package.json +1 -1
  25. package/src/hooks/ts/pre-compact/context-compression-hook.ts +292 -70
  26. package/src/hooks/ts/session-start/post-compact-inject.ts +85 -0
  27. package/dist/indexer-backend-DQO-FqAI.mjs.map +0 -1
  28. package/dist/postgres-CIxeqf_n.mjs.map +0 -1
@@ -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 { join as join3, basename as basename2, dirname } from "path";
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");
@@ -172,6 +200,43 @@ ${checkpoint}
172
200
  writeFileSync(notePath, newContent);
173
201
  console.error(`Checkpoint added to: ${basename(notePath)}`);
174
202
  }
203
+ function addWorkToSessionNote(notePath, workItems, sectionTitle) {
204
+ if (!existsSync2(notePath)) {
205
+ console.error(`Note file not found: ${notePath}`);
206
+ return;
207
+ }
208
+ let content = readFileSync2(notePath, "utf-8");
209
+ let workText = "";
210
+ if (sectionTitle) {
211
+ workText += `
212
+ ### ${sectionTitle}
213
+
214
+ `;
215
+ }
216
+ for (const item of workItems) {
217
+ const checkbox = item.completed !== false ? "[x]" : "[ ]";
218
+ workText += `- ${checkbox} **${item.title}**
219
+ `;
220
+ if (item.details && item.details.length > 0) {
221
+ for (const detail of item.details) {
222
+ workText += ` - ${detail}
223
+ `;
224
+ }
225
+ }
226
+ }
227
+ const workDoneMatch = content.match(/## Work Done\n\n(<!-- .*? -->)?/);
228
+ if (workDoneMatch) {
229
+ const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;
230
+ content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);
231
+ } else {
232
+ const nextStepsIndex = content.indexOf("## Next Steps");
233
+ if (nextStepsIndex !== -1) {
234
+ content = content.substring(0, nextStepsIndex) + workText + "\n" + content.substring(nextStepsIndex);
235
+ }
236
+ }
237
+ writeFileSync(notePath, content);
238
+ console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
239
+ }
175
240
  function calculateSessionTokens(jsonlPath) {
176
241
  if (!existsSync2(jsonlPath)) {
177
242
  return 0;
@@ -199,8 +264,81 @@ function calculateSessionTokens(jsonlPath) {
199
264
  return 0;
200
265
  }
201
266
  }
267
+ function findTodoPath(cwd) {
268
+ const localPaths = [
269
+ join2(cwd, "TODO.md"),
270
+ join2(cwd, "notes", "TODO.md"),
271
+ join2(cwd, "Notes", "TODO.md"),
272
+ join2(cwd, ".claude", "TODO.md")
273
+ ];
274
+ for (const path of localPaths) {
275
+ if (existsSync2(path)) {
276
+ return path;
277
+ }
278
+ }
279
+ return join2(getNotesDir(cwd), "TODO.md");
280
+ }
281
+ function ensureTodoMd(cwd) {
282
+ const todoPath = findTodoPath(cwd);
283
+ if (!existsSync2(todoPath)) {
284
+ const parentDir = join2(todoPath, "..");
285
+ if (!existsSync2(parentDir)) {
286
+ mkdirSync(parentDir, { recursive: true });
287
+ }
288
+ const content = `# TODO
289
+
290
+ ## Current Session
291
+
292
+ - [ ] (Tasks will be tracked here)
293
+
294
+ ## Backlog
295
+
296
+ - [ ] (Future tasks)
297
+
298
+ ---
299
+
300
+ *Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
301
+ `;
302
+ writeFileSync(todoPath, content);
303
+ console.error(`Created TODO.md: ${todoPath}`);
304
+ }
305
+ return todoPath;
306
+ }
307
+ function addTodoCheckpoint(cwd, checkpoint) {
308
+ const todoPath = ensureTodoMd(cwd);
309
+ let content = readFileSync2(todoPath, "utf-8");
310
+ content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, "");
311
+ const backlogIndex = content.indexOf("## Backlog");
312
+ if (backlogIndex !== -1) {
313
+ const checkpointText = `
314
+ **Checkpoint (${(/* @__PURE__ */ new Date()).toISOString()}):** ${checkpoint}
315
+
316
+ `;
317
+ content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
318
+ }
319
+ content = content.trimEnd() + `
320
+
321
+ ---
322
+
323
+ *Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
324
+ `;
325
+ writeFileSync(todoPath, content);
326
+ console.error(`Checkpoint added to TODO.md`);
327
+ }
202
328
 
203
329
  // src/hooks/ts/pre-compact/context-compression-hook.ts
330
+ function contentToText(content) {
331
+ if (typeof content === "string") return content;
332
+ if (Array.isArray(content)) {
333
+ return content.map((c) => {
334
+ if (typeof c === "string") return c;
335
+ if (c?.text) return c.text;
336
+ if (c?.content) return String(c.content);
337
+ return "";
338
+ }).join(" ").trim();
339
+ }
340
+ return "";
341
+ }
204
342
  function getTranscriptStats(transcriptPath) {
205
343
  try {
206
344
  const content = readFileSync3(transcriptPath, "utf-8");
@@ -208,32 +346,165 @@ function getTranscriptStats(transcriptPath) {
208
346
  let userMessages = 0;
209
347
  let assistantMessages = 0;
210
348
  for (const line of lines) {
211
- if (line.trim()) {
212
- try {
213
- const entry = JSON.parse(line);
214
- if (entry.type === "user") {
215
- userMessages++;
216
- } else if (entry.type === "assistant") {
217
- assistantMessages++;
218
- }
219
- } catch {
220
- }
349
+ if (!line.trim()) continue;
350
+ try {
351
+ const entry = JSON.parse(line);
352
+ if (entry.type === "user") userMessages++;
353
+ else if (entry.type === "assistant") assistantMessages++;
354
+ } catch {
221
355
  }
222
356
  }
223
357
  const totalMessages = userMessages + assistantMessages;
224
- const isLarge = totalMessages > 50;
225
- return { messageCount: totalMessages, isLarge };
226
- } catch (error) {
358
+ return { messageCount: totalMessages, isLarge: totalMessages > 50 };
359
+ } catch {
227
360
  return { messageCount: 0, isLarge: false };
228
361
  }
229
362
  }
363
+ function extractSessionState(transcriptPath, cwd) {
364
+ try {
365
+ const raw = readFileSync3(transcriptPath, "utf-8");
366
+ const lines = raw.trim().split("\n");
367
+ const userMessages = [];
368
+ const summaries = [];
369
+ const captures = [];
370
+ let lastCompleted = "";
371
+ const filesModified = /* @__PURE__ */ new Set();
372
+ for (const line of lines) {
373
+ if (!line.trim()) continue;
374
+ let entry;
375
+ try {
376
+ entry = JSON.parse(line);
377
+ } catch {
378
+ continue;
379
+ }
380
+ if (entry.type === "user" && entry.message?.content) {
381
+ const text = contentToText(entry.message.content).slice(0, 300);
382
+ if (text) userMessages.push(text);
383
+ }
384
+ if (entry.type === "assistant" && entry.message?.content) {
385
+ const text = contentToText(entry.message.content);
386
+ const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
387
+ if (summaryMatch) {
388
+ const s = summaryMatch[1].trim();
389
+ if (s.length > 5 && !summaries.includes(s)) summaries.push(s);
390
+ }
391
+ const captureMatch = text.match(/CAPTURE:\s*(.+?)(?:\n|$)/i);
392
+ if (captureMatch) {
393
+ const c = captureMatch[1].trim();
394
+ if (c.length > 5 && !captures.includes(c)) captures.push(c);
395
+ }
396
+ const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
397
+ if (completedMatch) {
398
+ lastCompleted = completedMatch[1].trim().replace(/\*+/g, "");
399
+ }
400
+ }
401
+ if (entry.type === "assistant" && entry.message?.content && Array.isArray(entry.message.content)) {
402
+ for (const block of entry.message.content) {
403
+ if (block.type === "tool_use") {
404
+ const tool = block.name;
405
+ if ((tool === "Edit" || tool === "Write") && block.input?.file_path) {
406
+ filesModified.add(block.input.file_path);
407
+ }
408
+ }
409
+ }
410
+ }
411
+ }
412
+ const parts = [];
413
+ if (cwd) {
414
+ parts.push(`Working directory: ${cwd}`);
415
+ }
416
+ const recentUser = userMessages.slice(-3);
417
+ if (recentUser.length > 0) {
418
+ parts.push("\nRecent user requests:");
419
+ for (const msg of recentUser) {
420
+ const firstLine = msg.split("\n")[0].slice(0, 200);
421
+ parts.push(`- ${firstLine}`);
422
+ }
423
+ }
424
+ const recentSummaries = summaries.slice(-3);
425
+ if (recentSummaries.length > 0) {
426
+ parts.push("\nWork summaries:");
427
+ for (const s of recentSummaries) {
428
+ parts.push(`- ${s.slice(0, 150)}`);
429
+ }
430
+ }
431
+ const recentCaptures = captures.slice(-5);
432
+ if (recentCaptures.length > 0) {
433
+ parts.push("\nCaptured context:");
434
+ for (const c of recentCaptures) {
435
+ parts.push(`- ${c.slice(0, 150)}`);
436
+ }
437
+ }
438
+ const files = Array.from(filesModified).slice(-10);
439
+ if (files.length > 0) {
440
+ parts.push("\nFiles modified this session:");
441
+ for (const f of files) {
442
+ parts.push(`- ${f}`);
443
+ }
444
+ }
445
+ if (lastCompleted) {
446
+ parts.push(`
447
+ Last completed: ${lastCompleted.slice(0, 150)}`);
448
+ }
449
+ const result = parts.join("\n");
450
+ return result.length > 50 ? result : null;
451
+ } catch (err) {
452
+ console.error(`extractSessionState error: ${err}`);
453
+ return null;
454
+ }
455
+ }
456
+ function extractWorkFromTranscript(transcriptPath) {
457
+ try {
458
+ const raw = readFileSync3(transcriptPath, "utf-8");
459
+ const lines = raw.trim().split("\n");
460
+ const workItems = [];
461
+ const seenSummaries = /* @__PURE__ */ new Set();
462
+ for (const line of lines) {
463
+ if (!line.trim()) continue;
464
+ let entry;
465
+ try {
466
+ entry = JSON.parse(line);
467
+ } catch {
468
+ continue;
469
+ }
470
+ if (entry.type === "assistant" && entry.message?.content) {
471
+ const text = contentToText(entry.message.content);
472
+ const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
473
+ if (summaryMatch) {
474
+ const summary = summaryMatch[1].trim();
475
+ if (summary && !seenSummaries.has(summary) && summary.length > 5) {
476
+ seenSummaries.add(summary);
477
+ const details = [];
478
+ const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
479
+ if (actionsMatch) {
480
+ const actionLines = actionsMatch[1].split("\n").map((l) => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 3 && l.length < 100);
481
+ details.push(...actionLines.slice(0, 3));
482
+ }
483
+ workItems.push({ title: summary, details: details.length > 0 ? details : void 0, completed: true });
484
+ }
485
+ }
486
+ const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
487
+ if (completedMatch && workItems.length === 0) {
488
+ const completed = completedMatch[1].trim().replace(/\*+/g, "");
489
+ if (completed && !seenSummaries.has(completed) && completed.length > 5) {
490
+ seenSummaries.add(completed);
491
+ workItems.push({ title: completed, completed: true });
492
+ }
493
+ }
494
+ }
495
+ }
496
+ return workItems;
497
+ } catch {
498
+ return [];
499
+ }
500
+ }
230
501
  async function main() {
231
502
  let hookInput = null;
232
503
  try {
233
504
  const decoder = new TextDecoder();
234
505
  let input = "";
235
506
  const timeoutPromise = new Promise((resolve2) => {
236
- setTimeout(() => resolve2(), 500);
507
+ setTimeout(resolve2, 500);
237
508
  });
238
509
  const readPromise = (async () => {
239
510
  for await (const chunk of process.stdin) {
@@ -244,34 +515,63 @@ async function main() {
244
515
  if (input.trim()) {
245
516
  hookInput = JSON.parse(input);
246
517
  }
247
- } catch (error) {
518
+ } catch {
248
519
  }
249
- const compactType = hookInput?.compact_type || "auto";
250
- let message = "Compressing context to continue";
520
+ const compactType = hookInput?.compact_type || hookInput?.trigger || "auto";
251
521
  let tokenCount = 0;
252
- if (hookInput && hookInput.transcript_path) {
522
+ if (hookInput?.transcript_path) {
253
523
  const stats = getTranscriptStats(hookInput.transcript_path);
254
524
  tokenCount = calculateSessionTokens(hookInput.transcript_path);
255
525
  const tokenDisplay = tokenCount > 1e3 ? `${Math.round(tokenCount / 1e3)}k` : String(tokenCount);
256
- if (stats.messageCount > 0) {
257
- if (compactType === "manual") {
258
- message = `Manually compressing ${stats.messageCount} messages (~${tokenDisplay} tokens)`;
259
- } else {
260
- message = stats.isLarge ? `Auto-compressing large context (~${tokenDisplay} tokens)` : `Compressing context (~${tokenDisplay} tokens)`;
261
- }
262
- }
526
+ const state = extractSessionState(hookInput.transcript_path, hookInput.cwd);
263
527
  try {
264
- const transcriptDir = dirname(hookInput.transcript_path);
265
- const notesDir = join3(transcriptDir, "Notes");
266
- const currentNotePath = getCurrentNotePath(notesDir);
528
+ const notesInfo = hookInput.cwd ? findNotesDir(hookInput.cwd) : { path: join3(dirname(hookInput.transcript_path), "Notes"), isLocal: false };
529
+ const currentNotePath = getCurrentNotePath(notesInfo.path);
267
530
  if (currentNotePath) {
268
- const checkpoint = `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
269
- appendCheckpoint(currentNotePath, checkpoint);
270
- console.error(`Checkpoint saved before compression: ${basename2(currentNotePath)}`);
531
+ const checkpointBody = state ? `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.
532
+
533
+ ${state}` : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
534
+ appendCheckpoint(currentNotePath, checkpointBody);
535
+ const workItems = extractWorkFromTranscript(hookInput.transcript_path);
536
+ if (workItems.length > 0) {
537
+ addWorkToSessionNote(currentNotePath, workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
538
+ console.error(`Added ${workItems.length} work item(s) to session note`);
539
+ }
540
+ console.error(`Rich checkpoint saved: ${basename2(currentNotePath)}`);
271
541
  }
272
542
  } catch (noteError) {
273
543
  console.error(`Could not save checkpoint: ${noteError}`);
274
544
  }
545
+ if (hookInput.cwd && state) {
546
+ try {
547
+ addTodoCheckpoint(hookInput.cwd, `Pre-compact checkpoint (~${tokenDisplay} tokens):
548
+ ${state}`);
549
+ console.error("TODO.md checkpoint added");
550
+ } catch (todoError) {
551
+ console.error(`Could not update TODO.md: ${todoError}`);
552
+ }
553
+ }
554
+ if (state && hookInput.session_id) {
555
+ const injection = [
556
+ "<system-reminder>",
557
+ `SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,
558
+ "",
559
+ state,
560
+ "",
561
+ "IMPORTANT: This session state was captured before context compaction.",
562
+ "Use it to maintain continuity. Continue the conversation from where",
563
+ "it left off without asking the user to repeat themselves.",
564
+ "Continue with the last task that you were asked to work on.",
565
+ "</system-reminder>"
566
+ ].join("\n");
567
+ try {
568
+ const stateFile = join3(tmpdir(), `pai-compact-state-${hookInput.session_id}.txt`);
569
+ writeFileSync2(stateFile, injection, "utf-8");
570
+ console.error(`Session state saved to ${stateFile} (${injection.length} chars)`);
571
+ } catch (err) {
572
+ console.error(`Failed to save state file: ${err}`);
573
+ }
574
+ }
275
575
  }
276
576
  const ntfyMessage = tokenCount > 0 ? `Auto-pause: ~${Math.round(tokenCount / 1e3)}k tokens` : "Context compressing";
277
577
  await sendNtfyNotification(ntfyMessage);