@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.
@@ -3,6 +3,8 @@
3
3
  // src/hooks/ts/stop/stop-hook.ts
4
4
  import { readFileSync as readFileSync5 } from "fs";
5
5
  import { basename as basename3, dirname } from "path";
6
+ import { connect } from "net";
7
+ import { randomUUID } from "crypto";
6
8
 
7
9
  // src/hooks/ts/lib/project-utils/paths.ts
8
10
  import { existsSync as existsSync2, mkdirSync, readdirSync, renameSync } from "fs";
@@ -266,6 +268,30 @@ function addWorkToSessionNote(notePath, workItems, sectionTitle) {
266
268
  function sanitizeForFilename(str) {
267
269
  return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 50);
268
270
  }
271
+ function isMeaninglessCandidate(text) {
272
+ const t = text.trim();
273
+ if (!t) return true;
274
+ if (t.length < 5) return true;
275
+ if (t.startsWith("/") || t.startsWith("~")) return true;
276
+ if (t.startsWith("#!")) return true;
277
+ if (t.includes("[object Object]")) return true;
278
+ if (/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/.test(t)) return true;
279
+ if (/^\d{1,2}:\d{2}(:\d{2})?(\s*(AM|PM))?$/i.test(t)) return true;
280
+ if (/^<[a-z-]+[\s/>]/i.test(t)) return true;
281
+ if (/^[0-9a-f]{10,}$/i.test(t)) return true;
282
+ if (/^Exit code \d+/i.test(t)) return true;
283
+ if (/^Error:/i.test(t)) return true;
284
+ if (/^This session is being continued/i.test(t)) return true;
285
+ if (/^\(Bash completed/i.test(t)) return true;
286
+ if (/^Task Notification$/i.test(t)) return true;
287
+ if (/^New Session$/i.test(t)) return true;
288
+ if (/^Recovered Session$/i.test(t)) return true;
289
+ if (/^Continued Session$/i.test(t)) return true;
290
+ if (/^Untitled Session$/i.test(t)) return true;
291
+ if (/^Context Compression$/i.test(t)) return true;
292
+ if (/^[A-Fa-f0-9]{8,}\s+Output$/i.test(t)) return true;
293
+ return false;
294
+ }
269
295
  function extractMeaningfulName(noteContent, summary) {
270
296
  const workDoneMatch = noteContent.match(/## Work Done\n\n([\s\S]*?)(?=\n---|\n## Next)/);
271
297
  if (workDoneMatch) {
@@ -273,23 +299,27 @@ function extractMeaningfulName(noteContent, summary) {
273
299
  const subheadings = workDoneSection.match(/### ([^\n]+)/g);
274
300
  if (subheadings && subheadings.length > 0) {
275
301
  const firstHeading = subheadings[0].replace("### ", "").trim();
276
- if (firstHeading.length > 5 && firstHeading.length < 60) {
302
+ if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {
277
303
  return sanitizeForFilename(firstHeading);
278
304
  }
279
305
  }
280
306
  const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
281
307
  if (boldMatches && boldMatches.length > 0) {
282
308
  const firstBold = boldMatches[0].replace(/\*\*/g, "").trim();
283
- if (firstBold.length > 3 && firstBold.length < 50) {
309
+ if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {
284
310
  return sanitizeForFilename(firstBold);
285
311
  }
286
312
  }
287
313
  const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
288
- if (numberedItems) return sanitizeForFilename(numberedItems[1]);
314
+ if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {
315
+ return sanitizeForFilename(numberedItems[1]);
316
+ }
289
317
  }
290
- if (summary && summary.length > 5 && summary !== "Session completed.") {
318
+ if (summary && summary.length > 5 && summary !== "Session completed." && !isMeaninglessCandidate(summary)) {
291
319
  const cleanSummary = summary.replace(/[^\w\s-]/g, " ").trim().split(/\s+/).slice(0, 5).join(" ");
292
- if (cleanSummary.length > 3) return sanitizeForFilename(cleanSummary);
320
+ if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {
321
+ return sanitizeForFilename(cleanSummary);
322
+ }
293
323
  }
294
324
  return "";
295
325
  }
@@ -418,6 +448,105 @@ ${stateLines}
418
448
  }
419
449
 
420
450
  // src/hooks/ts/stop/stop-hook.ts
451
+ var DAEMON_SOCKET = process.env.PAI_SOCKET ?? "/tmp/pai.sock";
452
+ var DAEMON_TIMEOUT_MS = 3e3;
453
+ function contentToText(content) {
454
+ if (typeof content === "string") return content;
455
+ if (Array.isArray(content)) {
456
+ return content.map((c) => {
457
+ if (typeof c === "string") return c;
458
+ if (c?.text) return c.text;
459
+ if (c?.content) return String(c.content);
460
+ return "";
461
+ }).join(" ").trim();
462
+ }
463
+ return "";
464
+ }
465
+ function extractCompletedMessage(lines) {
466
+ for (let i = lines.length - 1; i >= 0; i--) {
467
+ try {
468
+ const entry = JSON.parse(lines[i]);
469
+ if (entry.type === "assistant" && entry.message?.content) {
470
+ const content = contentToText(entry.message.content);
471
+ const m = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
472
+ if (m) {
473
+ return m[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "").trim();
474
+ }
475
+ }
476
+ } catch {
477
+ }
478
+ }
479
+ return "";
480
+ }
481
+ function enqueueWithDaemon(payload) {
482
+ return new Promise((resolve2) => {
483
+ let done = false;
484
+ let buffer = "";
485
+ let timer = null;
486
+ function finish(ok) {
487
+ if (done) return;
488
+ done = true;
489
+ if (timer !== null) {
490
+ clearTimeout(timer);
491
+ timer = null;
492
+ }
493
+ try {
494
+ client.destroy();
495
+ } catch {
496
+ }
497
+ resolve2(ok);
498
+ }
499
+ const client = connect(DAEMON_SOCKET, () => {
500
+ const msg = JSON.stringify({
501
+ id: randomUUID(),
502
+ method: "work_queue_enqueue",
503
+ params: {
504
+ type: "session-end",
505
+ priority: 2,
506
+ payload: {
507
+ transcriptPath: payload.transcriptPath,
508
+ cwd: payload.cwd,
509
+ message: payload.message
510
+ }
511
+ }
512
+ }) + "\n";
513
+ client.write(msg);
514
+ });
515
+ client.on("data", (chunk) => {
516
+ buffer += chunk.toString();
517
+ const nl = buffer.indexOf("\n");
518
+ if (nl === -1) return;
519
+ const line = buffer.slice(0, nl);
520
+ try {
521
+ const response = JSON.parse(line);
522
+ if (response.ok) {
523
+ console.error(`STOP-HOOK: Work enqueued with daemon (id=${response.result?.id}).`);
524
+ finish(true);
525
+ } else {
526
+ console.error(`STOP-HOOK: Daemon rejected enqueue: ${response.error}`);
527
+ finish(false);
528
+ }
529
+ } catch {
530
+ finish(false);
531
+ }
532
+ });
533
+ client.on("error", (e) => {
534
+ if (e.code === "ENOENT" || e.code === "ECONNREFUSED") {
535
+ console.error("STOP-HOOK: Daemon not running \u2014 falling back to direct execution.");
536
+ } else {
537
+ console.error(`STOP-HOOK: Daemon socket error: ${e.message}`);
538
+ }
539
+ finish(false);
540
+ });
541
+ client.on("end", () => {
542
+ if (!done) finish(false);
543
+ });
544
+ timer = setTimeout(() => {
545
+ console.error(`STOP-HOOK: Daemon timeout after ${DAEMON_TIMEOUT_MS}ms \u2014 falling back.`);
546
+ finish(false);
547
+ }, DAEMON_TIMEOUT_MS);
548
+ });
549
+ }
421
550
  function extractWorkFromTranscript(lines) {
422
551
  const workItems = [];
423
552
  const seenSummaries = /* @__PURE__ */ new Set();
@@ -446,13 +575,10 @@ function extractWorkFromTranscript(lines) {
446
575
  }
447
576
  const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
448
577
  if (completedMatch && workItems.length === 0) {
449
- const completed = completedMatch[1].trim().replace(/\*+/g, "");
578
+ const completed = completedMatch[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "");
450
579
  if (completed && !seenSummaries.has(completed) && completed.length > 5) {
451
580
  seenSummaries.add(completed);
452
- workItems.push({
453
- title: completed,
454
- completed: true
455
- });
581
+ workItems.push({ title: completed, completed: true });
456
582
  }
457
583
  }
458
584
  }
@@ -467,9 +593,7 @@ function generateTabTitle(prompt, completedLine) {
467
593
  const completedWords = cleanCompleted.split(/\s+/).filter((word) => word.length > 2 && !["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())).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
468
594
  if (completedWords.length >= 2) {
469
595
  const summary = completedWords.slice(0, 4);
470
- while (summary.length < 4) {
471
- summary.push("Done");
472
- }
596
+ while (summary.length < 4) summary.push("Done");
473
597
  return summary.slice(0, 4).join(" ");
474
598
  }
475
599
  }
@@ -494,37 +618,80 @@ function generateTabTitle(prompt, completedLine) {
494
618
  }
495
619
  const remainingWords = words.filter((word) => !actionVerbs.includes(word.toLowerCase())).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
496
620
  for (const word of remainingWords) {
497
- if (titleWords.length < 4) {
498
- titleWords.push(word);
499
- } else {
500
- break;
501
- }
621
+ if (titleWords.length < 4) titleWords.push(word);
622
+ else break;
502
623
  }
503
- if (titleWords.length === 0) {
504
- titleWords.push("Completed");
624
+ if (titleWords.length === 0) titleWords.push("Completed");
625
+ if (titleWords.length === 1) titleWords.push("Task");
626
+ if (titleWords.length === 2) titleWords.push("Successfully");
627
+ if (titleWords.length === 3) titleWords.push("Done");
628
+ return titleWords.slice(0, 4).join(" ");
629
+ }
630
+ async function executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery) {
631
+ let tabTitle = message || "";
632
+ if (!tabTitle && lastUserQuery) {
633
+ tabTitle = generateTabTitle(lastUserQuery, "");
505
634
  }
506
- if (titleWords.length === 1) {
507
- titleWords.push("Task");
635
+ if (tabTitle) {
636
+ try {
637
+ const escapedTitle = tabTitle.replace(/'/g, "'\\''");
638
+ const { execSync } = await import("child_process");
639
+ execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
640
+ execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
641
+ execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
642
+ console.error(`Tab title set to: "${tabTitle}"`);
643
+ } catch (e) {
644
+ console.error(`Failed to set tab title: ${e}`);
645
+ }
508
646
  }
509
- if (titleWords.length === 2) {
510
- titleWords.push("Successfully");
647
+ if (message) {
648
+ const finalTabTitle = message.slice(0, 50);
649
+ process.stderr.write(`\x1B]2;${finalTabTitle}\x07`);
511
650
  }
512
- if (titleWords.length === 3) {
513
- titleWords.push("Done");
651
+ try {
652
+ const notesInfo = findNotesDir(cwd);
653
+ const currentNotePath = getCurrentNotePath(notesInfo.path);
654
+ if (currentNotePath) {
655
+ const workItems = extractWorkFromTranscript(lines);
656
+ if (workItems.length > 0) {
657
+ addWorkToSessionNote(currentNotePath, workItems);
658
+ console.error(`Added ${workItems.length} work item(s) to session note`);
659
+ } else if (message) {
660
+ addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);
661
+ console.error(`Added completion message to session note`);
662
+ }
663
+ const summary = message || "Session completed.";
664
+ finalizeSessionNote(currentNotePath, summary);
665
+ console.error(`Session note finalized: ${basename3(currentNotePath)}`);
666
+ try {
667
+ const stateLines = [];
668
+ stateLines.push(`Working directory: ${cwd}`);
669
+ if (workItems.length > 0) {
670
+ stateLines.push("", "Work completed:");
671
+ for (const item of workItems.slice(0, 5)) {
672
+ stateLines.push(`- ${item.title}`);
673
+ }
674
+ }
675
+ if (message) {
676
+ stateLines.push("", `Last completed: ${message}`);
677
+ }
678
+ updateTodoContinue(cwd, basename3(currentNotePath), stateLines.join("\n"), "session-end");
679
+ } catch (todoError) {
680
+ console.error(`Could not update TODO.md: ${todoError}`);
681
+ }
682
+ }
683
+ } catch (noteError) {
684
+ console.error(`Could not finalize session note: ${noteError}`);
514
685
  }
515
- return titleWords.slice(0, 4).join(" ");
516
- }
517
- function contentToText(content) {
518
- if (typeof content === "string") return content;
519
- if (Array.isArray(content)) {
520
- return content.map((c) => {
521
- if (typeof c === "string") return c;
522
- if (c?.text) return c.text;
523
- if (c?.content) return String(c.content);
524
- return "";
525
- }).join(" ").trim();
686
+ try {
687
+ const transcriptDir = dirname(transcriptPath);
688
+ const movedCount = moveSessionFilesToSessionsDir(transcriptDir);
689
+ if (movedCount > 0) {
690
+ console.error(`Moved ${movedCount} session file(s) to sessions/`);
691
+ }
692
+ } catch (moveError) {
693
+ console.error(`Could not move session files: ${moveError}`);
526
694
  }
527
- return "";
528
695
  }
529
696
  async function main() {
530
697
  if (isProbeSession()) {
@@ -590,45 +757,20 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
590
757
  }
591
758
  if (lastUserQuery) break;
592
759
  }
593
- } catch (e) {
594
- }
595
- }
596
- let message = "";
597
- const lastResponse = lines[lines.length - 1];
598
- try {
599
- const entry = JSON.parse(lastResponse);
600
- if (entry.type === "assistant" && entry.message?.content) {
601
- const content = contentToText(entry.message.content);
602
- const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
603
- if (completedMatch) {
604
- message = completedMatch[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "").trim();
605
- console.error(`COMPLETION: ${message}`);
606
- }
760
+ } catch {
607
761
  }
608
- } catch (e) {
609
- console.error("Error parsing assistant response:", e);
610
762
  }
763
+ const message = extractCompletedMessage(lines);
764
+ console.error(`User query: ${lastUserQuery || "No query found"}`);
765
+ console.error(`Message: ${message || "No completion message"}`);
611
766
  let tabTitle = message || "";
612
767
  if (!tabTitle && lastUserQuery) {
613
- try {
614
- const entry = JSON.parse(lastResponse);
615
- if (entry.type === "assistant" && entry.message?.content) {
616
- const content = contentToText(entry.message.content);
617
- const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/im);
618
- if (completedMatch) {
619
- tabTitle = completedMatch[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "").trim();
620
- }
621
- }
622
- } catch (e) {
623
- }
624
- if (!tabTitle) {
625
- tabTitle = generateTabTitle(lastUserQuery, "");
626
- }
768
+ tabTitle = generateTabTitle(lastUserQuery, "");
627
769
  }
628
770
  if (tabTitle) {
629
771
  try {
630
- const escapedTitle = tabTitle.replace(/'/g, "'\\''");
631
772
  const { execSync } = await import("child_process");
773
+ const escapedTitle = tabTitle.replace(/'/g, "'\\''");
632
774
  execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
633
775
  execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
634
776
  execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
@@ -637,69 +779,22 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
637
779
  console.error(`Failed to set tab title: ${e}`);
638
780
  }
639
781
  }
640
- console.error(`User query: ${lastUserQuery || "No query found"}`);
641
- console.error(`Message: ${message || "No completion message"}`);
642
782
  if (message) {
643
- const finalTabTitle = message.slice(0, 50);
644
- process.stderr.write(`\x1B]2;${finalTabTitle}\x07`);
783
+ process.stderr.write(`\x1B]2;${message.slice(0, 50)}\x07`);
645
784
  }
646
785
  if (message) {
647
786
  await sendNtfyNotification(message);
648
787
  } else {
649
788
  await sendNtfyNotification("Session ended");
650
789
  }
651
- try {
652
- const notesInfo = findNotesDir(cwd);
653
- console.error(`Notes directory: ${notesInfo.path} (${notesInfo.isLocal ? "local" : "central"})`);
654
- const currentNotePath = getCurrentNotePath(notesInfo.path);
655
- if (currentNotePath) {
656
- const workItems = extractWorkFromTranscript(lines);
657
- if (workItems.length > 0) {
658
- addWorkToSessionNote(currentNotePath, workItems);
659
- console.error(`Added ${workItems.length} work item(s) to session note`);
660
- } else {
661
- if (message) {
662
- addWorkToSessionNote(currentNotePath, [{
663
- title: message,
664
- completed: true
665
- }]);
666
- console.error(`Added completion message to session note`);
667
- }
668
- }
669
- const summary = message || "Session completed.";
670
- finalizeSessionNote(currentNotePath, summary);
671
- console.error(`Session note finalized: ${basename3(currentNotePath)}`);
672
- try {
673
- const stateLines = [];
674
- stateLines.push(`Working directory: ${cwd}`);
675
- if (workItems.length > 0) {
676
- stateLines.push("");
677
- stateLines.push("Work completed:");
678
- for (const item of workItems.slice(0, 5)) {
679
- stateLines.push(`- ${item.title}`);
680
- }
681
- }
682
- if (message) {
683
- stateLines.push("");
684
- stateLines.push(`Last completed: ${message}`);
685
- }
686
- const state = stateLines.join("\n");
687
- updateTodoContinue(cwd, basename3(currentNotePath), state, "session-end");
688
- } catch (todoError) {
689
- console.error(`Could not update TODO.md: ${todoError}`);
690
- }
691
- }
692
- } catch (noteError) {
693
- console.error(`Could not finalize session note: ${noteError}`);
694
- }
695
- try {
696
- const transcriptDir = dirname(transcriptPath);
697
- const movedCount = moveSessionFilesToSessionsDir(transcriptDir);
698
- if (movedCount > 0) {
699
- console.error(`Moved ${movedCount} session file(s) to sessions/`);
700
- }
701
- } catch (moveError) {
702
- console.error(`Could not move session files: ${moveError}`);
790
+ const relayed = await enqueueWithDaemon({
791
+ transcriptPath,
792
+ cwd,
793
+ message
794
+ });
795
+ if (!relayed) {
796
+ console.error("STOP-HOOK: Using direct execution fallback.");
797
+ await executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery);
703
798
  }
704
799
  console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${(/* @__PURE__ */ new Date()).toISOString()}
705
800
  `);