@tekmidian/pai 0.3.2 → 0.4.0

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 (57) hide show
  1. package/dist/cli/index.mjs +279 -21
  2. package/dist/cli/index.mjs.map +1 -1
  3. package/dist/hooks/capture-all-events.mjs +238 -0
  4. package/dist/hooks/capture-all-events.mjs.map +7 -0
  5. package/dist/hooks/capture-session-summary.mjs +198 -0
  6. package/dist/hooks/capture-session-summary.mjs.map +7 -0
  7. package/dist/hooks/capture-tool-output.mjs +105 -0
  8. package/dist/hooks/capture-tool-output.mjs.map +7 -0
  9. package/dist/hooks/cleanup-session-files.mjs +129 -0
  10. package/dist/hooks/cleanup-session-files.mjs.map +7 -0
  11. package/dist/hooks/context-compression-hook.mjs +283 -0
  12. package/dist/hooks/context-compression-hook.mjs.map +7 -0
  13. package/dist/hooks/initialize-session.mjs +206 -0
  14. package/dist/hooks/initialize-session.mjs.map +7 -0
  15. package/dist/hooks/load-core-context.mjs +110 -0
  16. package/dist/hooks/load-core-context.mjs.map +7 -0
  17. package/dist/hooks/load-project-context.mjs +548 -0
  18. package/dist/hooks/load-project-context.mjs.map +7 -0
  19. package/dist/hooks/security-validator.mjs +159 -0
  20. package/dist/hooks/security-validator.mjs.map +7 -0
  21. package/dist/hooks/stop-hook.mjs +625 -0
  22. package/dist/hooks/stop-hook.mjs.map +7 -0
  23. package/dist/hooks/subagent-stop-hook.mjs +152 -0
  24. package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
  25. package/dist/hooks/sync-todo-to-md.mjs +322 -0
  26. package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
  27. package/dist/hooks/update-tab-on-action.mjs +90 -0
  28. package/dist/hooks/update-tab-on-action.mjs.map +7 -0
  29. package/dist/hooks/update-tab-titles.mjs +55 -0
  30. package/dist/hooks/update-tab-titles.mjs.map +7 -0
  31. package/package.json +4 -2
  32. package/scripts/build-hooks.mjs +51 -0
  33. package/src/hooks/ts/capture-all-events.ts +179 -0
  34. package/src/hooks/ts/lib/detect-environment.ts +53 -0
  35. package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
  36. package/src/hooks/ts/lib/pai-paths.ts +124 -0
  37. package/src/hooks/ts/lib/project-utils.ts +914 -0
  38. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
  39. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
  40. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
  41. package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
  42. package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
  43. package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
  44. package/src/hooks/ts/session-start/initialize-session.ts +155 -0
  45. package/src/hooks/ts/session-start/load-core-context.ts +104 -0
  46. package/src/hooks/ts/session-start/load-project-context.ts +394 -0
  47. package/src/hooks/ts/stop/stop-hook.ts +407 -0
  48. package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
  49. package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
  50. package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
  51. package/tab-color-command.sh +24 -0
  52. package/templates/skills/createskill-skill.template.md +78 -0
  53. package/templates/skills/history-system.template.md +371 -0
  54. package/templates/skills/hook-system.template.md +913 -0
  55. package/templates/skills/sessions-skill.template.md +102 -0
  56. package/templates/skills/skill-system.template.md +214 -0
  57. package/templates/skills/terminal-tabs.template.md +120 -0
@@ -0,0 +1,625 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/hooks/ts/stop/stop-hook.ts
10
+ import { readFileSync as readFileSync3 } from "fs";
11
+ import { basename as basename2, dirname } from "path";
12
+
13
+ // src/hooks/ts/lib/project-utils.ts
14
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
15
+ import { join as join2, basename } from "path";
16
+
17
+ // src/hooks/ts/lib/pai-paths.ts
18
+ import { homedir } from "os";
19
+ import { resolve, join } from "path";
20
+ import { existsSync, readFileSync } from "fs";
21
+ function loadEnvFile() {
22
+ const possiblePaths = [
23
+ resolve(process.env.PAI_DIR || "", ".env"),
24
+ resolve(homedir(), ".claude", ".env")
25
+ ];
26
+ for (const envPath of possiblePaths) {
27
+ if (existsSync(envPath)) {
28
+ try {
29
+ const content = readFileSync(envPath, "utf-8");
30
+ for (const line of content.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith("#")) continue;
33
+ const eqIndex = trimmed.indexOf("=");
34
+ if (eqIndex > 0) {
35
+ const key = trimmed.substring(0, eqIndex).trim();
36
+ let value = trimmed.substring(eqIndex + 1).trim();
37
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
38
+ value = value.slice(1, -1);
39
+ }
40
+ value = value.replace(/\$HOME/g, homedir());
41
+ value = value.replace(/^~(?=\/|$)/, homedir());
42
+ if (process.env[key] === void 0) {
43
+ process.env[key] = value;
44
+ }
45
+ }
46
+ }
47
+ break;
48
+ } catch {
49
+ }
50
+ }
51
+ }
52
+ }
53
+ loadEnvFile();
54
+ var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(homedir(), ".claude");
55
+ var HOOKS_DIR = join(PAI_DIR, "Hooks");
56
+ var SKILLS_DIR = join(PAI_DIR, "Skills");
57
+ var AGENTS_DIR = join(PAI_DIR, "Agents");
58
+ var HISTORY_DIR = join(PAI_DIR, "History");
59
+ var COMMANDS_DIR = join(PAI_DIR, "Commands");
60
+ function validatePAIStructure() {
61
+ if (!existsSync(PAI_DIR)) {
62
+ console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
63
+ console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
64
+ process.exit(1);
65
+ }
66
+ if (!existsSync(HOOKS_DIR)) {
67
+ console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
68
+ console.error(` Your PAI_DIR may be misconfigured`);
69
+ console.error(` Current PAI_DIR: ${PAI_DIR}`);
70
+ process.exit(1);
71
+ }
72
+ }
73
+ validatePAIStructure();
74
+
75
+ // src/hooks/ts/lib/project-utils.ts
76
+ var PROJECTS_DIR = join2(PAI_DIR, "projects");
77
+ function encodePath(path) {
78
+ return path.replace(/\//g, "-").replace(/\./g, "-").replace(/ /g, "-");
79
+ }
80
+ function getProjectDir(cwd) {
81
+ const encoded = encodePath(cwd);
82
+ return join2(PROJECTS_DIR, encoded);
83
+ }
84
+ function getNotesDir(cwd) {
85
+ return join2(getProjectDir(cwd), "Notes");
86
+ }
87
+ function findNotesDir(cwd) {
88
+ const cwdBasename = basename(cwd).toLowerCase();
89
+ if (cwdBasename === "notes" && existsSync2(cwd)) {
90
+ return { path: cwd, isLocal: true };
91
+ }
92
+ const localPaths = [
93
+ join2(cwd, "Notes"),
94
+ join2(cwd, "notes"),
95
+ join2(cwd, ".claude", "Notes")
96
+ ];
97
+ for (const path of localPaths) {
98
+ if (existsSync2(path)) {
99
+ return { path, isLocal: true };
100
+ }
101
+ }
102
+ return { path: getNotesDir(cwd), isLocal: false };
103
+ }
104
+ function getSessionsDirFromProjectDir(projectDir) {
105
+ return join2(projectDir, "sessions");
106
+ }
107
+ function isWhatsAppEnabled() {
108
+ try {
109
+ const { homedir: homedir2 } = __require("os");
110
+ const settingsPath = join2(homedir2(), ".claude", "settings.json");
111
+ if (!existsSync2(settingsPath)) return false;
112
+ const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
113
+ const enabled = settings.enabledMcpjsonServers || [];
114
+ return enabled.includes("whazaa");
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+ async function sendNtfyNotification(message, retries = 2) {
120
+ if (isWhatsAppEnabled()) {
121
+ console.error(`WhatsApp (Whazaa) enabled in MCP config \u2014 skipping ntfy`);
122
+ return true;
123
+ }
124
+ const topic = process.env.NTFY_TOPIC;
125
+ if (!topic) {
126
+ console.error("NTFY_TOPIC not set and WhatsApp not active \u2014 notifications disabled");
127
+ return false;
128
+ }
129
+ for (let attempt = 0; attempt <= retries; attempt++) {
130
+ try {
131
+ const response = await fetch(`https://ntfy.sh/${topic}`, {
132
+ method: "POST",
133
+ body: message,
134
+ headers: {
135
+ "Title": "Claude Code",
136
+ "Priority": "default"
137
+ }
138
+ });
139
+ if (response.ok) {
140
+ console.error(`ntfy.sh notification sent (WhatsApp inactive): "${message}"`);
141
+ return true;
142
+ } else {
143
+ console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);
144
+ }
145
+ } catch (error) {
146
+ console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);
147
+ }
148
+ if (attempt < retries) {
149
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
150
+ }
151
+ }
152
+ console.error("ntfy.sh notification failed after all retries");
153
+ return false;
154
+ }
155
+ function ensureSessionsDirFromProjectDir(projectDir) {
156
+ const sessionsDir = getSessionsDirFromProjectDir(projectDir);
157
+ if (!existsSync2(sessionsDir)) {
158
+ mkdirSync(sessionsDir, { recursive: true });
159
+ console.error(`Created sessions directory: ${sessionsDir}`);
160
+ }
161
+ return sessionsDir;
162
+ }
163
+ function moveSessionFilesToSessionsDir(projectDir, excludeFile, silent = false) {
164
+ const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);
165
+ if (!existsSync2(projectDir)) {
166
+ return 0;
167
+ }
168
+ const files = readdirSync(projectDir);
169
+ let movedCount = 0;
170
+ for (const file of files) {
171
+ if (file.endsWith(".jsonl") && file !== excludeFile) {
172
+ const sourcePath = join2(projectDir, file);
173
+ const destPath = join2(sessionsDir, file);
174
+ try {
175
+ renameSync(sourcePath, destPath);
176
+ if (!silent) {
177
+ console.error(`Moved ${file} \u2192 sessions/`);
178
+ }
179
+ movedCount++;
180
+ } catch (error) {
181
+ if (!silent) {
182
+ console.error(`Could not move ${file}: ${error}`);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return movedCount;
188
+ }
189
+ function getCurrentNotePath(notesDir) {
190
+ if (!existsSync2(notesDir)) {
191
+ return null;
192
+ }
193
+ const findLatestIn = (dir) => {
194
+ if (!existsSync2(dir)) return null;
195
+ const files = readdirSync(dir).filter((f) => f.match(/^\d{3,4}[\s_-].*\.md$/)).sort((a, b) => {
196
+ const numA = parseInt(a.match(/^(\d+)/)?.[1] || "0", 10);
197
+ const numB = parseInt(b.match(/^(\d+)/)?.[1] || "0", 10);
198
+ return numA - numB;
199
+ });
200
+ if (files.length === 0) return null;
201
+ return join2(dir, files[files.length - 1]);
202
+ };
203
+ const now = /* @__PURE__ */ new Date();
204
+ const year = String(now.getFullYear());
205
+ const month = String(now.getMonth() + 1).padStart(2, "0");
206
+ const currentMonthDir = join2(notesDir, year, month);
207
+ const found = findLatestIn(currentMonthDir);
208
+ if (found) return found;
209
+ const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
210
+ const prevYear = String(prevDate.getFullYear());
211
+ const prevMonth = String(prevDate.getMonth() + 1).padStart(2, "0");
212
+ const prevMonthDir = join2(notesDir, prevYear, prevMonth);
213
+ const prevFound = findLatestIn(prevMonthDir);
214
+ if (prevFound) return prevFound;
215
+ return findLatestIn(notesDir);
216
+ }
217
+ function addWorkToSessionNote(notePath, workItems, sectionTitle) {
218
+ if (!existsSync2(notePath)) {
219
+ console.error(`Note file not found: ${notePath}`);
220
+ return;
221
+ }
222
+ let content = readFileSync2(notePath, "utf-8");
223
+ let workText = "";
224
+ if (sectionTitle) {
225
+ workText += `
226
+ ### ${sectionTitle}
227
+
228
+ `;
229
+ }
230
+ for (const item of workItems) {
231
+ const checkbox = item.completed !== false ? "[x]" : "[ ]";
232
+ workText += `- ${checkbox} **${item.title}**
233
+ `;
234
+ if (item.details && item.details.length > 0) {
235
+ for (const detail of item.details) {
236
+ workText += ` - ${detail}
237
+ `;
238
+ }
239
+ }
240
+ }
241
+ const workDoneMatch = content.match(/## Work Done\n\n(<!-- .*? -->)?/);
242
+ if (workDoneMatch) {
243
+ const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;
244
+ content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);
245
+ } else {
246
+ const nextStepsIndex = content.indexOf("## Next Steps");
247
+ if (nextStepsIndex !== -1) {
248
+ content = content.substring(0, nextStepsIndex) + workText + "\n" + content.substring(nextStepsIndex);
249
+ }
250
+ }
251
+ writeFileSync(notePath, content);
252
+ console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
253
+ }
254
+ function sanitizeForFilename(str) {
255
+ return str.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 50);
256
+ }
257
+ function extractMeaningfulName(noteContent, summary) {
258
+ const workDoneMatch = noteContent.match(/## Work Done\n\n([\s\S]*?)(?=\n---|\n## Next)/);
259
+ if (workDoneMatch) {
260
+ const workDoneSection = workDoneMatch[1];
261
+ const subheadings = workDoneSection.match(/### ([^\n]+)/g);
262
+ if (subheadings && subheadings.length > 0) {
263
+ const firstHeading = subheadings[0].replace("### ", "").trim();
264
+ if (firstHeading.length > 5 && firstHeading.length < 60) {
265
+ return sanitizeForFilename(firstHeading);
266
+ }
267
+ }
268
+ const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
269
+ if (boldMatches && boldMatches.length > 0) {
270
+ const firstBold = boldMatches[0].replace(/\*\*/g, "").trim();
271
+ if (firstBold.length > 3 && firstBold.length < 50) {
272
+ return sanitizeForFilename(firstBold);
273
+ }
274
+ }
275
+ const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
276
+ if (numberedItems) {
277
+ return sanitizeForFilename(numberedItems[1]);
278
+ }
279
+ }
280
+ if (summary && summary.length > 5 && summary !== "Session completed.") {
281
+ const cleanSummary = summary.replace(/[^\w\s-]/g, " ").trim().split(/\s+/).slice(0, 5).join(" ");
282
+ if (cleanSummary.length > 3) {
283
+ return sanitizeForFilename(cleanSummary);
284
+ }
285
+ }
286
+ return "";
287
+ }
288
+ function renameSessionNote(notePath, meaningfulName) {
289
+ if (!meaningfulName || !existsSync2(notePath)) {
290
+ return notePath;
291
+ }
292
+ const dir = join2(notePath, "..");
293
+ const oldFilename = basename(notePath);
294
+ const correctMatch = oldFilename.match(/^(\d{3,4}) - (\d{4}-\d{2}-\d{2}) - .*\.md$/);
295
+ const legacyMatch = oldFilename.match(/^(\d{3,4})_(\d{4}-\d{2}-\d{2})_.*\.md$/);
296
+ const match = correctMatch || legacyMatch;
297
+ if (!match) {
298
+ return notePath;
299
+ }
300
+ const [, noteNumber, date] = match;
301
+ const titleCaseName = meaningfulName.split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
302
+ const paddedNumber = noteNumber.padStart(4, "0");
303
+ const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
304
+ const newPath = join2(dir, newFilename);
305
+ if (newFilename === oldFilename) {
306
+ return notePath;
307
+ }
308
+ try {
309
+ renameSync(notePath, newPath);
310
+ console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
311
+ return newPath;
312
+ } catch (error) {
313
+ console.error(`Could not rename note: ${error}`);
314
+ return notePath;
315
+ }
316
+ }
317
+ function finalizeSessionNote(notePath, summary) {
318
+ if (!existsSync2(notePath)) {
319
+ console.error(`Note file not found: ${notePath}`);
320
+ return notePath;
321
+ }
322
+ let content = readFileSync2(notePath, "utf-8");
323
+ if (content.includes("**Status:** Completed")) {
324
+ console.error(`Note already finalized: ${basename(notePath)}`);
325
+ return notePath;
326
+ }
327
+ content = content.replace("**Status:** In Progress", "**Status:** Completed");
328
+ if (!content.includes("**Completed:**")) {
329
+ const completionTime = (/* @__PURE__ */ new Date()).toISOString();
330
+ content = content.replace(
331
+ "---\n\n## Work Done",
332
+ `**Completed:** ${completionTime}
333
+
334
+ ---
335
+
336
+ ## Work Done`
337
+ );
338
+ }
339
+ const nextStepsMatch = content.match(/## Next Steps\n\n(<!-- .*? -->)/);
340
+ if (nextStepsMatch) {
341
+ content = content.replace(
342
+ nextStepsMatch[0],
343
+ `## Next Steps
344
+
345
+ ${summary || "Session completed."}`
346
+ );
347
+ }
348
+ writeFileSync(notePath, content);
349
+ console.error(`Session note finalized: ${basename(notePath)}`);
350
+ const meaningfulName = extractMeaningfulName(content, summary);
351
+ if (meaningfulName) {
352
+ const newPath = renameSessionNote(notePath, meaningfulName);
353
+ return newPath;
354
+ }
355
+ return notePath;
356
+ }
357
+
358
+ // src/hooks/ts/stop/stop-hook.ts
359
+ function extractWorkFromTranscript(lines) {
360
+ const workItems = [];
361
+ const seenSummaries = /* @__PURE__ */ new Set();
362
+ for (const line of lines) {
363
+ try {
364
+ const entry = JSON.parse(line);
365
+ if (entry.type === "assistant" && entry.message?.content) {
366
+ const content = contentToText(entry.message.content);
367
+ const summaryMatch = content.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
368
+ if (summaryMatch) {
369
+ const summary = summaryMatch[1].trim();
370
+ if (summary && !seenSummaries.has(summary) && summary.length > 5) {
371
+ seenSummaries.add(summary);
372
+ const details = [];
373
+ const actionsMatch = content.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
374
+ if (actionsMatch) {
375
+ const actionLines = actionsMatch[1].split("\n").map((l) => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 3 && l.length < 100);
376
+ details.push(...actionLines.slice(0, 3));
377
+ }
378
+ workItems.push({
379
+ title: summary,
380
+ details: details.length > 0 ? details : void 0,
381
+ completed: true
382
+ });
383
+ }
384
+ }
385
+ const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
386
+ if (completedMatch && workItems.length === 0) {
387
+ const completed = completedMatch[1].trim().replace(/\*+/g, "");
388
+ if (completed && !seenSummaries.has(completed) && completed.length > 5) {
389
+ seenSummaries.add(completed);
390
+ workItems.push({
391
+ title: completed,
392
+ completed: true
393
+ });
394
+ }
395
+ }
396
+ }
397
+ } catch {
398
+ }
399
+ }
400
+ return workItems;
401
+ }
402
+ function generateTabTitle(prompt, completedLine) {
403
+ if (completedLine) {
404
+ const cleanCompleted = completedLine.replace(/\*+/g, "").replace(/\[.*?\]/g, "").replace(/COMPLETED:\s*/gi, "").trim();
405
+ 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());
406
+ if (completedWords.length >= 2) {
407
+ const summary = completedWords.slice(0, 4);
408
+ while (summary.length < 4) {
409
+ summary.push("Done");
410
+ }
411
+ return summary.slice(0, 4).join(" ");
412
+ }
413
+ }
414
+ const cleanPrompt = prompt.replace(/[^\w\s]/g, " ").trim();
415
+ const words = cleanPrompt.split(/\s+/).filter(
416
+ (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"].includes(word.toLowerCase())
417
+ );
418
+ const lowerPrompt = prompt.toLowerCase();
419
+ const actionVerbs = ["test", "rename", "fix", "debug", "research", "write", "create", "make", "build", "implement", "analyze", "review", "update", "modify", "generate", "develop", "design", "deploy", "configure", "setup", "install", "remove", "delete", "add", "check", "verify", "validate", "optimize", "refactor", "enhance", "improve", "send", "email", "help", "updated", "fixed", "created", "built", "added"];
420
+ let titleWords = [];
421
+ for (const verb of actionVerbs) {
422
+ if (lowerPrompt.includes(verb)) {
423
+ let pastTense = verb;
424
+ if (verb === "write") pastTense = "Wrote";
425
+ else if (verb === "make") pastTense = "Made";
426
+ else if (verb === "send") pastTense = "Sent";
427
+ else if (verb.endsWith("e")) pastTense = verb.charAt(0).toUpperCase() + verb.slice(1, -1) + "ed";
428
+ else pastTense = verb.charAt(0).toUpperCase() + verb.slice(1) + "ed";
429
+ titleWords.push(pastTense);
430
+ break;
431
+ }
432
+ }
433
+ const remainingWords = words.filter((word) => !actionVerbs.includes(word.toLowerCase())).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
434
+ for (const word of remainingWords) {
435
+ if (titleWords.length < 4) {
436
+ titleWords.push(word);
437
+ } else {
438
+ break;
439
+ }
440
+ }
441
+ if (titleWords.length === 0) {
442
+ titleWords.push("Completed");
443
+ }
444
+ if (titleWords.length === 1) {
445
+ titleWords.push("Task");
446
+ }
447
+ if (titleWords.length === 2) {
448
+ titleWords.push("Successfully");
449
+ }
450
+ if (titleWords.length === 3) {
451
+ titleWords.push("Done");
452
+ }
453
+ return titleWords.slice(0, 4).join(" ");
454
+ }
455
+ function contentToText(content) {
456
+ if (typeof content === "string") return content;
457
+ if (Array.isArray(content)) {
458
+ return content.map((c) => {
459
+ if (typeof c === "string") return c;
460
+ if (c?.text) return c.text;
461
+ if (c?.content) return String(c.content);
462
+ return "";
463
+ }).join(" ").trim();
464
+ }
465
+ return "";
466
+ }
467
+ async function main() {
468
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
469
+ console.error(`
470
+ STOP-HOOK TRIGGERED AT ${timestamp}`);
471
+ let input = "";
472
+ const decoder = new TextDecoder();
473
+ try {
474
+ for await (const chunk of process.stdin) {
475
+ input += decoder.decode(chunk, { stream: true });
476
+ }
477
+ } catch (e) {
478
+ console.error(`Error reading input: ${e}`);
479
+ process.exit(0);
480
+ }
481
+ if (!input) {
482
+ console.error("No input received");
483
+ process.exit(0);
484
+ }
485
+ let transcriptPath;
486
+ let cwd;
487
+ try {
488
+ const parsed = JSON.parse(input);
489
+ transcriptPath = parsed.transcript_path;
490
+ cwd = parsed.cwd || process.cwd();
491
+ console.error(`Transcript path: ${transcriptPath}`);
492
+ console.error(`Working directory: ${cwd}`);
493
+ } catch (e) {
494
+ console.error(`Error parsing input JSON: ${e}`);
495
+ process.exit(0);
496
+ }
497
+ if (!transcriptPath) {
498
+ console.error("No transcript_path in input");
499
+ process.exit(0);
500
+ }
501
+ let transcript;
502
+ try {
503
+ transcript = readFileSync3(transcriptPath, "utf-8");
504
+ console.error(`Transcript loaded: ${transcript.split("\n").length} lines`);
505
+ } catch (e) {
506
+ console.error(`Error reading transcript: ${e}`);
507
+ process.exit(0);
508
+ }
509
+ const lines = transcript.trim().split("\n");
510
+ let lastUserQuery = "";
511
+ for (let i = lines.length - 1; i >= 0; i--) {
512
+ try {
513
+ const entry = JSON.parse(lines[i]);
514
+ if (entry.type === "user" && entry.message?.content) {
515
+ const content = entry.message.content;
516
+ if (typeof content === "string") {
517
+ lastUserQuery = content;
518
+ } else if (Array.isArray(content)) {
519
+ for (const item of content) {
520
+ if (item.type === "text" && item.text) {
521
+ lastUserQuery = item.text;
522
+ break;
523
+ }
524
+ }
525
+ }
526
+ if (lastUserQuery) break;
527
+ }
528
+ } catch (e) {
529
+ }
530
+ }
531
+ let message = "";
532
+ const lastResponse = lines[lines.length - 1];
533
+ try {
534
+ const entry = JSON.parse(lastResponse);
535
+ if (entry.type === "assistant" && entry.message?.content) {
536
+ const content = contentToText(entry.message.content);
537
+ const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
538
+ if (completedMatch) {
539
+ message = completedMatch[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "").trim();
540
+ console.error(`COMPLETION: ${message}`);
541
+ }
542
+ }
543
+ } catch (e) {
544
+ console.error("Error parsing assistant response:", e);
545
+ }
546
+ let tabTitle = message || "";
547
+ if (!tabTitle && lastUserQuery) {
548
+ try {
549
+ const entry = JSON.parse(lastResponse);
550
+ if (entry.type === "assistant" && entry.message?.content) {
551
+ const content = contentToText(entry.message.content);
552
+ const completedMatch = content.match(/COMPLETED:\s*(.+?)(?:\n|$)/im);
553
+ if (completedMatch) {
554
+ tabTitle = completedMatch[1].trim().replace(/\*+/g, "").replace(/\[.*?\]/g, "").trim();
555
+ }
556
+ }
557
+ } catch (e) {
558
+ }
559
+ if (!tabTitle) {
560
+ tabTitle = generateTabTitle(lastUserQuery, "");
561
+ }
562
+ }
563
+ if (tabTitle) {
564
+ try {
565
+ const escapedTitle = tabTitle.replace(/'/g, "'\\''");
566
+ const { execSync } = await import("child_process");
567
+ execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
568
+ execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
569
+ execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
570
+ console.error(`Tab title set to: "${tabTitle}"`);
571
+ } catch (e) {
572
+ console.error(`Failed to set tab title: ${e}`);
573
+ }
574
+ }
575
+ console.error(`User query: ${lastUserQuery || "No query found"}`);
576
+ console.error(`Message: ${message || "No completion message"}`);
577
+ if (message) {
578
+ const finalTabTitle = message.slice(0, 50);
579
+ process.stderr.write(`\x1B]2;${finalTabTitle}\x07`);
580
+ }
581
+ if (message) {
582
+ await sendNtfyNotification(message);
583
+ } else {
584
+ await sendNtfyNotification("Session ended");
585
+ }
586
+ try {
587
+ const notesInfo = findNotesDir(cwd);
588
+ console.error(`Notes directory: ${notesInfo.path} (${notesInfo.isLocal ? "local" : "central"})`);
589
+ const currentNotePath = getCurrentNotePath(notesInfo.path);
590
+ if (currentNotePath) {
591
+ const workItems = extractWorkFromTranscript(lines);
592
+ if (workItems.length > 0) {
593
+ addWorkToSessionNote(currentNotePath, workItems);
594
+ console.error(`Added ${workItems.length} work item(s) to session note`);
595
+ } else {
596
+ if (message) {
597
+ addWorkToSessionNote(currentNotePath, [{
598
+ title: message,
599
+ completed: true
600
+ }]);
601
+ console.error(`Added completion message to session note`);
602
+ }
603
+ }
604
+ const summary = message || "Session completed.";
605
+ finalizeSessionNote(currentNotePath, summary);
606
+ console.error(`Session note finalized: ${basename2(currentNotePath)}`);
607
+ }
608
+ } catch (noteError) {
609
+ console.error(`Could not finalize session note: ${noteError}`);
610
+ }
611
+ try {
612
+ const transcriptDir = dirname(transcriptPath);
613
+ const movedCount = moveSessionFilesToSessionsDir(transcriptDir);
614
+ if (movedCount > 0) {
615
+ console.error(`Moved ${movedCount} session file(s) to sessions/`);
616
+ }
617
+ } catch (moveError) {
618
+ console.error(`Could not move session files: ${moveError}`);
619
+ }
620
+ console.error(`STOP-HOOK COMPLETED SUCCESSFULLY at ${(/* @__PURE__ */ new Date()).toISOString()}
621
+ `);
622
+ }
623
+ main().catch(() => {
624
+ });
625
+ //# sourceMappingURL=stop-hook.mjs.map