@tekmidian/pai 0.7.0 → 0.7.1

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,16 @@ 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.startsWith("/")) return true;
275
+ if (t.startsWith("#!")) return true;
276
+ if (t.includes("[object Object]")) return true;
277
+ if (/^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/.test(t)) return true;
278
+ if (/^\d{1,2}:\d{2}(:\d{2})?(\s*(AM|PM))?$/i.test(t)) return true;
279
+ return false;
280
+ }
269
281
  function extractMeaningfulName(noteContent, summary) {
270
282
  const workDoneMatch = noteContent.match(/## Work Done\n\n([\s\S]*?)(?=\n---|\n## Next)/);
271
283
  if (workDoneMatch) {
@@ -273,23 +285,27 @@ function extractMeaningfulName(noteContent, summary) {
273
285
  const subheadings = workDoneSection.match(/### ([^\n]+)/g);
274
286
  if (subheadings && subheadings.length > 0) {
275
287
  const firstHeading = subheadings[0].replace("### ", "").trim();
276
- if (firstHeading.length > 5 && firstHeading.length < 60) {
288
+ if (!isMeaninglessCandidate(firstHeading) && firstHeading.length > 5 && firstHeading.length < 60) {
277
289
  return sanitizeForFilename(firstHeading);
278
290
  }
279
291
  }
280
292
  const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
281
293
  if (boldMatches && boldMatches.length > 0) {
282
294
  const firstBold = boldMatches[0].replace(/\*\*/g, "").trim();
283
- if (firstBold.length > 3 && firstBold.length < 50) {
295
+ if (!isMeaninglessCandidate(firstBold) && firstBold.length > 3 && firstBold.length < 50) {
284
296
  return sanitizeForFilename(firstBold);
285
297
  }
286
298
  }
287
299
  const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
288
- if (numberedItems) return sanitizeForFilename(numberedItems[1]);
300
+ if (numberedItems && !isMeaninglessCandidate(numberedItems[1])) {
301
+ return sanitizeForFilename(numberedItems[1]);
302
+ }
289
303
  }
290
- if (summary && summary.length > 5 && summary !== "Session completed.") {
304
+ if (summary && summary.length > 5 && summary !== "Session completed." && !isMeaninglessCandidate(summary)) {
291
305
  const cleanSummary = summary.replace(/[^\w\s-]/g, " ").trim().split(/\s+/).slice(0, 5).join(" ");
292
- if (cleanSummary.length > 3) return sanitizeForFilename(cleanSummary);
306
+ if (cleanSummary.length > 3 && !isMeaninglessCandidate(cleanSummary)) {
307
+ return sanitizeForFilename(cleanSummary);
308
+ }
293
309
  }
294
310
  return "";
295
311
  }
@@ -418,6 +434,105 @@ ${stateLines}
418
434
  }
419
435
 
420
436
  // src/hooks/ts/stop/stop-hook.ts
437
+ var DAEMON_SOCKET = process.env.PAI_SOCKET ?? "/tmp/pai.sock";
438
+ var DAEMON_TIMEOUT_MS = 3e3;
439
+ function contentToText(content) {
440
+ if (typeof content === "string") return content;
441
+ if (Array.isArray(content)) {
442
+ return content.map((c) => {
443
+ if (typeof c === "string") return c;
444
+ if (c?.text) return c.text;
445
+ if (c?.content) return String(c.content);
446
+ return "";
447
+ }).join(" ").trim();
448
+ }
449
+ return "";
450
+ }
451
+ function extractCompletedMessage(lines) {
452
+ for (let i = lines.length - 1; i >= 0; i--) {
453
+ try {
454
+ const entry = JSON.parse(lines[i]);
455
+ if (entry.type === "assistant" && entry.message?.content) {
456
+ const content = contentToText(entry.message.content);
457
+ const m = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
458
+ if (m) {
459
+ return m[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "").trim();
460
+ }
461
+ }
462
+ } catch {
463
+ }
464
+ }
465
+ return "";
466
+ }
467
+ function enqueueWithDaemon(payload) {
468
+ return new Promise((resolve2) => {
469
+ let done = false;
470
+ let buffer = "";
471
+ let timer = null;
472
+ function finish(ok) {
473
+ if (done) return;
474
+ done = true;
475
+ if (timer !== null) {
476
+ clearTimeout(timer);
477
+ timer = null;
478
+ }
479
+ try {
480
+ client.destroy();
481
+ } catch {
482
+ }
483
+ resolve2(ok);
484
+ }
485
+ const client = connect(DAEMON_SOCKET, () => {
486
+ const msg = JSON.stringify({
487
+ id: randomUUID(),
488
+ method: "work_queue_enqueue",
489
+ params: {
490
+ type: "session-end",
491
+ priority: 2,
492
+ payload: {
493
+ transcriptPath: payload.transcriptPath,
494
+ cwd: payload.cwd,
495
+ message: payload.message
496
+ }
497
+ }
498
+ }) + "\n";
499
+ client.write(msg);
500
+ });
501
+ client.on("data", (chunk) => {
502
+ buffer += chunk.toString();
503
+ const nl = buffer.indexOf("\n");
504
+ if (nl === -1) return;
505
+ const line = buffer.slice(0, nl);
506
+ try {
507
+ const response = JSON.parse(line);
508
+ if (response.ok) {
509
+ console.error(`STOP-HOOK: Work enqueued with daemon (id=${response.result?.id}).`);
510
+ finish(true);
511
+ } else {
512
+ console.error(`STOP-HOOK: Daemon rejected enqueue: ${response.error}`);
513
+ finish(false);
514
+ }
515
+ } catch {
516
+ finish(false);
517
+ }
518
+ });
519
+ client.on("error", (e) => {
520
+ if (e.code === "ENOENT" || e.code === "ECONNREFUSED") {
521
+ console.error("STOP-HOOK: Daemon not running \u2014 falling back to direct execution.");
522
+ } else {
523
+ console.error(`STOP-HOOK: Daemon socket error: ${e.message}`);
524
+ }
525
+ finish(false);
526
+ });
527
+ client.on("end", () => {
528
+ if (!done) finish(false);
529
+ });
530
+ timer = setTimeout(() => {
531
+ console.error(`STOP-HOOK: Daemon timeout after ${DAEMON_TIMEOUT_MS}ms \u2014 falling back.`);
532
+ finish(false);
533
+ }, DAEMON_TIMEOUT_MS);
534
+ });
535
+ }
421
536
  function extractWorkFromTranscript(lines) {
422
537
  const workItems = [];
423
538
  const seenSummaries = /* @__PURE__ */ new Set();
@@ -446,13 +561,10 @@ function extractWorkFromTranscript(lines) {
446
561
  }
447
562
  const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
448
563
  if (completedMatch && workItems.length === 0) {
449
- const completed = completedMatch[1].trim().replace(/\*+/g, "");
564
+ const completed = completedMatch[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "");
450
565
  if (completed && !seenSummaries.has(completed) && completed.length > 5) {
451
566
  seenSummaries.add(completed);
452
- workItems.push({
453
- title: completed,
454
- completed: true
455
- });
567
+ workItems.push({ title: completed, completed: true });
456
568
  }
457
569
  }
458
570
  }
@@ -467,9 +579,7 @@ function generateTabTitle(prompt, completedLine) {
467
579
  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
580
  if (completedWords.length >= 2) {
469
581
  const summary = completedWords.slice(0, 4);
470
- while (summary.length < 4) {
471
- summary.push("Done");
472
- }
582
+ while (summary.length < 4) summary.push("Done");
473
583
  return summary.slice(0, 4).join(" ");
474
584
  }
475
585
  }
@@ -494,37 +604,80 @@ function generateTabTitle(prompt, completedLine) {
494
604
  }
495
605
  const remainingWords = words.filter((word) => !actionVerbs.includes(word.toLowerCase())).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
496
606
  for (const word of remainingWords) {
497
- if (titleWords.length < 4) {
498
- titleWords.push(word);
499
- } else {
500
- break;
501
- }
607
+ if (titleWords.length < 4) titleWords.push(word);
608
+ else break;
502
609
  }
503
- if (titleWords.length === 0) {
504
- titleWords.push("Completed");
610
+ if (titleWords.length === 0) titleWords.push("Completed");
611
+ if (titleWords.length === 1) titleWords.push("Task");
612
+ if (titleWords.length === 2) titleWords.push("Successfully");
613
+ if (titleWords.length === 3) titleWords.push("Done");
614
+ return titleWords.slice(0, 4).join(" ");
615
+ }
616
+ async function executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery) {
617
+ let tabTitle = message || "";
618
+ if (!tabTitle && lastUserQuery) {
619
+ tabTitle = generateTabTitle(lastUserQuery, "");
505
620
  }
506
- if (titleWords.length === 1) {
507
- titleWords.push("Task");
621
+ if (tabTitle) {
622
+ try {
623
+ const escapedTitle = tabTitle.replace(/'/g, "'\\''");
624
+ const { execSync } = await import("child_process");
625
+ execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
626
+ execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
627
+ execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
628
+ console.error(`Tab title set to: "${tabTitle}"`);
629
+ } catch (e) {
630
+ console.error(`Failed to set tab title: ${e}`);
631
+ }
508
632
  }
509
- if (titleWords.length === 2) {
510
- titleWords.push("Successfully");
633
+ if (message) {
634
+ const finalTabTitle = message.slice(0, 50);
635
+ process.stderr.write(`\x1B]2;${finalTabTitle}\x07`);
511
636
  }
512
- if (titleWords.length === 3) {
513
- titleWords.push("Done");
637
+ try {
638
+ const notesInfo = findNotesDir(cwd);
639
+ const currentNotePath = getCurrentNotePath(notesInfo.path);
640
+ if (currentNotePath) {
641
+ const workItems = extractWorkFromTranscript(lines);
642
+ if (workItems.length > 0) {
643
+ addWorkToSessionNote(currentNotePath, workItems);
644
+ console.error(`Added ${workItems.length} work item(s) to session note`);
645
+ } else if (message) {
646
+ addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);
647
+ console.error(`Added completion message to session note`);
648
+ }
649
+ const summary = message || "Session completed.";
650
+ finalizeSessionNote(currentNotePath, summary);
651
+ console.error(`Session note finalized: ${basename3(currentNotePath)}`);
652
+ try {
653
+ const stateLines = [];
654
+ stateLines.push(`Working directory: ${cwd}`);
655
+ if (workItems.length > 0) {
656
+ stateLines.push("", "Work completed:");
657
+ for (const item of workItems.slice(0, 5)) {
658
+ stateLines.push(`- ${item.title}`);
659
+ }
660
+ }
661
+ if (message) {
662
+ stateLines.push("", `Last completed: ${message}`);
663
+ }
664
+ updateTodoContinue(cwd, basename3(currentNotePath), stateLines.join("\n"), "session-end");
665
+ } catch (todoError) {
666
+ console.error(`Could not update TODO.md: ${todoError}`);
667
+ }
668
+ }
669
+ } catch (noteError) {
670
+ console.error(`Could not finalize session note: ${noteError}`);
514
671
  }
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();
672
+ try {
673
+ const transcriptDir = dirname(transcriptPath);
674
+ const movedCount = moveSessionFilesToSessionsDir(transcriptDir);
675
+ if (movedCount > 0) {
676
+ console.error(`Moved ${movedCount} session file(s) to sessions/`);
677
+ }
678
+ } catch (moveError) {
679
+ console.error(`Could not move session files: ${moveError}`);
526
680
  }
527
- return "";
528
681
  }
529
682
  async function main() {
530
683
  if (isProbeSession()) {
@@ -590,45 +743,20 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
590
743
  }
591
744
  if (lastUserQuery) break;
592
745
  }
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
- }
746
+ } catch {
607
747
  }
608
- } catch (e) {
609
- console.error("Error parsing assistant response:", e);
610
748
  }
749
+ const message = extractCompletedMessage(lines);
750
+ console.error(`User query: ${lastUserQuery || "No query found"}`);
751
+ console.error(`Message: ${message || "No completion message"}`);
611
752
  let tabTitle = message || "";
612
753
  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
- }
754
+ tabTitle = generateTabTitle(lastUserQuery, "");
627
755
  }
628
756
  if (tabTitle) {
629
757
  try {
630
- const escapedTitle = tabTitle.replace(/'/g, "'\\''");
631
758
  const { execSync } = await import("child_process");
759
+ const escapedTitle = tabTitle.replace(/'/g, "'\\''");
632
760
  execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
633
761
  execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
634
762
  execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
@@ -637,69 +765,22 @@ STOP-HOOK TRIGGERED AT ${timestamp}`);
637
765
  console.error(`Failed to set tab title: ${e}`);
638
766
  }
639
767
  }
640
- console.error(`User query: ${lastUserQuery || "No query found"}`);
641
- console.error(`Message: ${message || "No completion message"}`);
642
768
  if (message) {
643
- const finalTabTitle = message.slice(0, 50);
644
- process.stderr.write(`\x1B]2;${finalTabTitle}\x07`);
769
+ process.stderr.write(`\x1B]2;${message.slice(0, 50)}\x07`);
645
770
  }
646
771
  if (message) {
647
772
  await sendNtfyNotification(message);
648
773
  } else {
649
774
  await sendNtfyNotification("Session ended");
650
775
  }
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}`);
776
+ const relayed = await enqueueWithDaemon({
777
+ transcriptPath,
778
+ cwd,
779
+ message
780
+ });
781
+ if (!relayed) {
782
+ console.error("STOP-HOOK: Using direct execution fallback.");
783
+ await executeDirectly(lines, transcriptPath, cwd, message, lastUserQuery);
703
784
  }
704
785
  console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${(/* @__PURE__ */ new Date()).toISOString()}
705
786
  `);