@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.
@@ -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");
@@ -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
- return;
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
- 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
- }
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
- const isLarge = totalMessages > 50;
225
- return { messageCount: totalMessages, isLarge };
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(() => resolve2(), 500);
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 (error) {
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 && hookInput.transcript_path) {
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
- if (stats.messageCount > 0) {
257
- if (compactType === "manual") {
258
- message = `Manually compressing ${stats.messageCount} messages (~${tokenDisplay} tokens)`;
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
- message = stats.isLarge ? `Auto-compressing large context (~${tokenDisplay} tokens)` : `Compressing context (~${tokenDisplay} tokens)`;
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
- try {
264
- const transcriptDir = dirname(hookInput.transcript_path);
265
- const notesDir = join3(transcriptDir, "Notes");
266
- const currentNotePath = getCurrentNotePath(notesDir);
267
- 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)}`);
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);