@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,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse Hook - Captures tool outputs for UOCS
5
+ *
6
+ * Automatically logs all tool executions to daily JSONL files
7
+ * for later processing and analysis.
8
+ */
9
+
10
+ import { appendFileSync, mkdirSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';
13
+
14
+ interface ToolUseData {
15
+ tool_name: string;
16
+ tool_input: Record<string, any>;
17
+ tool_response: Record<string, any>;
18
+ conversation_id: string;
19
+ timestamp: string;
20
+ }
21
+
22
+ // Configuration
23
+ const CAPTURE_DIR = join(HISTORY_DIR, 'raw-outputs');
24
+ const INTERESTING_TOOLS = ['Bash', 'Edit', 'Write', 'Read', 'Task', 'NotebookEdit'];
25
+
26
+ async function main() {
27
+ try {
28
+ // Read input from stdin
29
+ const chunks: Buffer[] = [];
30
+ for await (const chunk of process.stdin) {
31
+ chunks.push(chunk);
32
+ }
33
+ const input = Buffer.concat(chunks).toString('utf-8');
34
+ if (!input || input.trim() === '') {
35
+ process.exit(0);
36
+ }
37
+
38
+ const data: ToolUseData = JSON.parse(input);
39
+
40
+ // Only capture interesting tools
41
+ if (!INTERESTING_TOOLS.includes(data.tool_name)) {
42
+ process.exit(0);
43
+ }
44
+
45
+ // Get today's date for organization
46
+ const now = new Date();
47
+ const today = now.toISOString().split('T')[0]; // YYYY-MM-DD
48
+ const yearMonth = today.substring(0, 7); // YYYY-MM
49
+
50
+ // Ensure capture directory exists
51
+ const dateDir = join(CAPTURE_DIR, yearMonth);
52
+ if (!existsSync(dateDir)) {
53
+ mkdirSync(dateDir, { recursive: true });
54
+ }
55
+
56
+ // Format output as JSONL (one JSON object per line)
57
+ const captureFile = join(dateDir, `${today}_tool-outputs.jsonl`);
58
+ const captureEntry = JSON.stringify({
59
+ timestamp: data.timestamp || now.toISOString(),
60
+ tool: data.tool_name,
61
+ input: data.tool_input,
62
+ output: data.tool_response,
63
+ session: data.conversation_id
64
+ }) + '\n';
65
+
66
+ // Append to daily log
67
+ appendFileSync(captureFile, captureEntry);
68
+
69
+ // Exit successfully (code 0 = continue normally)
70
+ process.exit(0);
71
+ } catch (error) {
72
+ // Silent failure - don't disrupt workflow
73
+ console.error(`[UOCS] PostToolUse hook error: ${error}`);
74
+ process.exit(0);
75
+ }
76
+ }
77
+
78
+ main();
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sync-todo-to-md.ts
4
+ *
5
+ * PostToolUse hook for TodoWrite that:
6
+ * 1. Syncs Claude's todos to TODO.md "Current Session" section
7
+ * 2. PRESERVES all user-managed sections (Plans, Completed, Backlog, etc.)
8
+ * 3. Adds completed items to the session note
9
+ *
10
+ * IMPORTANT: This hook PRESERVES user content. It only updates "Current Session".
11
+ */
12
+
13
+ import { writeFileSync, existsSync, readFileSync, mkdirSync } from 'fs';
14
+ import { join } from 'path';
15
+ import {
16
+ findTodoPath,
17
+ findNotesDir,
18
+ getCurrentNotePath,
19
+ addWorkToSessionNote,
20
+ type WorkItem
21
+ } from '../lib/project-utils';
22
+
23
+ interface TodoItem {
24
+ content: string;
25
+ status: 'pending' | 'in_progress' | 'completed';
26
+ activeForm: string;
27
+ }
28
+
29
+ interface HookInput {
30
+ session_id: string;
31
+ cwd: string;
32
+ tool_name: string;
33
+ tool_input: {
34
+ todos: TodoItem[];
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Format current session todos as markdown
40
+ */
41
+ function formatSessionTodos(todos: TodoItem[]): string {
42
+ const inProgress = todos.filter(t => t.status === 'in_progress');
43
+ const pending = todos.filter(t => t.status === 'pending');
44
+ const completed = todos.filter(t => t.status === 'completed');
45
+
46
+ let content = '';
47
+
48
+ if (inProgress.length > 0) {
49
+ content += `### In Progress\n\n`;
50
+ for (const todo of inProgress) {
51
+ content += `- [ ] **${todo.content}** _(${todo.activeForm})_\n`;
52
+ }
53
+ content += '\n';
54
+ }
55
+
56
+ if (pending.length > 0) {
57
+ content += `### Pending\n\n`;
58
+ for (const todo of pending) {
59
+ content += `- [ ] ${todo.content}\n`;
60
+ }
61
+ content += '\n';
62
+ }
63
+
64
+ if (completed.length > 0) {
65
+ content += `### Completed\n\n`;
66
+ for (const todo of completed) {
67
+ content += `- [x] ${todo.content}\n`;
68
+ }
69
+ content += '\n';
70
+ }
71
+
72
+ if (todos.length === 0) {
73
+ content += `_(No active session tasks)_\n\n`;
74
+ }
75
+
76
+ return content;
77
+ }
78
+
79
+ /**
80
+ * Extract all sections from TODO.md EXCEPT "Current Session"
81
+ * These are user-managed sections that should be preserved.
82
+ */
83
+ function extractPreservedSections(content: string): string {
84
+ let preserved = '';
85
+
86
+ // Match all ## sections that are NOT "Current Session"
87
+ const sectionRegex = /\n(## (?!Current Session)[^\n]+[\s\S]*?)(?=\n## |\n---\n+\*Last updated|$)/g;
88
+ const matches = content.matchAll(sectionRegex);
89
+
90
+ for (const match of matches) {
91
+ preserved += match[1];
92
+ }
93
+
94
+ return preserved;
95
+ }
96
+
97
+ /**
98
+ * Fix malformed headings: Remove --- prefix from headings (---# → #)
99
+ * Claude sometimes incorrectly merges horizontal rules with headings.
100
+ */
101
+ function fixMalformedHeadings(content: string): string {
102
+ return content.replace(/^---#/gm, '#');
103
+ }
104
+
105
+ /**
106
+ * Build new TODO.md preserving user sections
107
+ */
108
+ function buildTodoContent(todos: TodoItem[], existingContent: string): string {
109
+ const now = new Date().toISOString();
110
+
111
+ // Get all preserved sections (everything except Current Session)
112
+ const preserved = extractPreservedSections(existingContent);
113
+
114
+ // Build new content
115
+ let content = `# TODO
116
+
117
+ ## Current Session
118
+
119
+ ${formatSessionTodos(todos)}`;
120
+
121
+ // Add preserved sections
122
+ if (preserved.trim()) {
123
+ content += preserved;
124
+ }
125
+
126
+ // Ensure we end with exactly one timestamp
127
+ content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)*$/, '');
128
+ content += `\n---\n\n*Last updated: ${now}*\n`;
129
+
130
+ return content;
131
+ }
132
+
133
+ async function main() {
134
+ try {
135
+ const chunks: Buffer[] = [];
136
+ for await (const chunk of process.stdin) {
137
+ chunks.push(chunk);
138
+ }
139
+ const stdinData = Buffer.concat(chunks).toString('utf-8');
140
+
141
+ if (!stdinData.trim()) {
142
+ console.error('No input received');
143
+ process.exit(0);
144
+ }
145
+
146
+ const hookInput: HookInput = JSON.parse(stdinData);
147
+
148
+ if (hookInput.tool_name !== 'TodoWrite') {
149
+ process.exit(0);
150
+ }
151
+
152
+ const todos = hookInput.tool_input?.todos;
153
+
154
+ if (!todos || !Array.isArray(todos)) {
155
+ console.error('No todos in tool input');
156
+ process.exit(0);
157
+ }
158
+
159
+ const cwd = hookInput.cwd || process.cwd();
160
+
161
+ // Find TODO.md path
162
+ const todoPath = findTodoPath(cwd);
163
+
164
+ // Create TODO.md if it doesn't exist
165
+ if (!existsSync(todoPath)) {
166
+ const parentDir = todoPath.replace(/\/[^/]+$/, '');
167
+ mkdirSync(parentDir, { recursive: true });
168
+ console.error(`Creating TODO.md at ${todoPath}`);
169
+ }
170
+
171
+ // Read existing content to preserve user sections
172
+ let existingContent = '';
173
+ try {
174
+ existingContent = readFileSync(todoPath, 'utf-8');
175
+ } catch (e) {
176
+ // New file, no content to preserve
177
+ }
178
+
179
+ // Build and write new content (with heading fix)
180
+ let newContent = buildTodoContent(todos, existingContent);
181
+ newContent = fixMalformedHeadings(newContent);
182
+ writeFileSync(todoPath, newContent);
183
+
184
+ const stats = {
185
+ inProgress: todos.filter(t => t.status === 'in_progress').length,
186
+ pending: todos.filter(t => t.status === 'pending').length,
187
+ completed: todos.filter(t => t.status === 'completed').length
188
+ };
189
+ console.error(`TODO.md synced: ${stats.inProgress} in progress, ${stats.pending} pending, ${stats.completed} completed`);
190
+
191
+ // Add completed items to session note (if local Notes/ exists)
192
+ const completedTodos = todos.filter(t => t.status === 'completed');
193
+
194
+ if (completedTodos.length > 0) {
195
+ const notesInfo = findNotesDir(cwd);
196
+
197
+ if (notesInfo.isLocal) {
198
+ const currentNotePath = getCurrentNotePath(notesInfo.path);
199
+
200
+ if (currentNotePath) {
201
+ let noteContent = '';
202
+ try {
203
+ noteContent = readFileSync(currentNotePath, 'utf-8');
204
+ } catch (e) {
205
+ console.error('Could not read session note:', e);
206
+ }
207
+
208
+ const newlyCompleted = completedTodos.filter(t => !noteContent.includes(t.content));
209
+
210
+ if (newlyCompleted.length > 0) {
211
+ const workItems: WorkItem[] = newlyCompleted.map(t => ({
212
+ title: t.content,
213
+ completed: true
214
+ }));
215
+
216
+ addWorkToSessionNote(currentNotePath, workItems);
217
+ console.error(`Added ${newlyCompleted.length} completed item(s) to session note`);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ } catch (error) {
224
+ console.error('sync-todo-to-md error:', error);
225
+ }
226
+
227
+ process.exit(0);
228
+ }
229
+
230
+ main();
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tab Title Update on Action Tools
4
+ * Updates Kitty tab title when action tools complete (Edit, Write, Bash, Task)
5
+ * Uses the tool's description directly - no AI call needed
6
+ *
7
+ * PAI-safe: No sensitive data, just tool descriptions
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+
12
+ // Action tools that warrant a tab title update
13
+ const ACTION_TOOLS = new Set([
14
+ 'Edit',
15
+ 'Write',
16
+ 'Bash',
17
+ 'Task',
18
+ 'NotebookEdit',
19
+ 'MultiEdit'
20
+ ]);
21
+
22
+ interface HookInput {
23
+ session_id: string;
24
+ tool_name: string;
25
+ tool_input: {
26
+ description?: string;
27
+ command?: string;
28
+ file_path?: string;
29
+ prompt?: string;
30
+ subagent_type?: string;
31
+ };
32
+ tool_response?: unknown;
33
+ hook_event_name: string;
34
+ }
35
+
36
+ /**
37
+ * Read stdin with timeout
38
+ */
39
+ async function readStdinWithTimeout(timeout: number = 3000): Promise<string> {
40
+ return new Promise((resolve, reject) => {
41
+ let data = '';
42
+ const timer = setTimeout(() => {
43
+ reject(new Error('Timeout'));
44
+ }, timeout);
45
+
46
+ process.stdin.on('data', (chunk) => {
47
+ data += chunk.toString();
48
+ });
49
+
50
+ process.stdin.on('end', () => {
51
+ clearTimeout(timer);
52
+ resolve(data);
53
+ });
54
+
55
+ process.stdin.on('error', (err) => {
56
+ clearTimeout(timer);
57
+ reject(err);
58
+ });
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Update Kitty tab title using escape codes
64
+ */
65
+ function setTabTitle(title: string): void {
66
+ try {
67
+ // Truncate to reasonable length
68
+ const truncated = title.length > 50 ? title.slice(0, 47) + '...' : title;
69
+ const escaped = truncated.replace(/'/g, "'\\''");
70
+
71
+ // Multiple escape sequences for compatibility
72
+ execSync(`printf '\\033]0;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });
73
+ execSync(`printf '\\033]2;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });
74
+ execSync(`printf '\\033]30;${escaped}\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });
75
+ } catch {
76
+ // Silently fail - don't interrupt Claude's work
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Generate a human-readable title from tool input
82
+ */
83
+ function generateTitle(toolName: string, input: HookInput['tool_input']): string {
84
+ // Use explicit description if provided
85
+ if (input.description) {
86
+ return input.description;
87
+ }
88
+
89
+ // Generate based on tool type
90
+ switch (toolName) {
91
+ case 'Edit':
92
+ case 'Write':
93
+ case 'MultiEdit':
94
+ case 'NotebookEdit':
95
+ if (input.file_path) {
96
+ const filename = input.file_path.split('/').pop() || input.file_path;
97
+ return `Editing ${filename}`;
98
+ }
99
+ return `Editing file`;
100
+
101
+ case 'Bash':
102
+ if (input.command) {
103
+ // Extract first command word
104
+ const cmd = input.command.split(/\s+/)[0].split('/').pop() || 'command';
105
+ return `Running ${cmd}`;
106
+ }
107
+ return 'Running command';
108
+
109
+ case 'Task':
110
+ if (input.subagent_type) {
111
+ return `Agent: ${input.subagent_type}`;
112
+ }
113
+ if (input.prompt) {
114
+ const words = input.prompt.slice(0, 30).split(/\s+/).slice(0, 3).join(' ');
115
+ return `Task: ${words}...`;
116
+ }
117
+ return 'Spawning agent';
118
+
119
+ default:
120
+ return `${toolName}...`;
121
+ }
122
+ }
123
+
124
+ async function main() {
125
+ try {
126
+ const input = await readStdinWithTimeout();
127
+ const data: HookInput = JSON.parse(input);
128
+
129
+ // Only process action tools
130
+ if (!ACTION_TOOLS.has(data.tool_name)) {
131
+ process.exit(0);
132
+ }
133
+
134
+ // Generate and set title
135
+ const title = generateTitle(data.tool_name, data.tool_input);
136
+ setTabTitle(title);
137
+
138
+ process.exit(0);
139
+ } catch {
140
+ // Silently exit - don't interrupt Claude's flow
141
+ process.exit(0);
142
+ }
143
+ }
144
+
145
+ main();
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreCompact Hook - Triggered before context compression
4
+ * Extracts context information from transcript and notifies about compression
5
+ *
6
+ * Enhanced to:
7
+ * - Save checkpoint to current session note
8
+ * - Send ntfy.sh notification
9
+ * - Calculate approximate token count
10
+ */
11
+
12
+ import { readFileSync } from 'fs';
13
+ import { join, basename, dirname } from 'path';
14
+ import {
15
+ sendNtfyNotification,
16
+ getCurrentNotePath,
17
+ appendCheckpoint,
18
+ calculateSessionTokens
19
+ } from '../lib/project-utils';
20
+
21
+ interface HookInput {
22
+ session_id: string;
23
+ transcript_path: string;
24
+ hook_event_name: string;
25
+ compact_type?: string;
26
+ }
27
+
28
+ interface TranscriptEntry {
29
+ type: string;
30
+ message?: {
31
+ role?: string;
32
+ content?: Array<{
33
+ type: string;
34
+ text: string;
35
+ }>
36
+ };
37
+ timestamp?: string;
38
+ }
39
+
40
+ /**
41
+ * Count messages in transcript to provide context
42
+ */
43
+ function getTranscriptStats(transcriptPath: string): { messageCount: number; isLarge: boolean } {
44
+ try {
45
+ const content = readFileSync(transcriptPath, 'utf-8');
46
+ const lines = content.trim().split('\n');
47
+
48
+ let userMessages = 0;
49
+ let assistantMessages = 0;
50
+
51
+ for (const line of lines) {
52
+ if (line.trim()) {
53
+ try {
54
+ const entry = JSON.parse(line) as TranscriptEntry;
55
+ if (entry.type === 'user') {
56
+ userMessages++;
57
+ } else if (entry.type === 'assistant') {
58
+ assistantMessages++;
59
+ }
60
+ } catch {
61
+ // Skip invalid JSON lines
62
+ }
63
+ }
64
+ }
65
+
66
+ const totalMessages = userMessages + assistantMessages;
67
+ const isLarge = totalMessages > 50; // Consider large if more than 50 messages
68
+
69
+ return { messageCount: totalMessages, isLarge };
70
+ } catch (error) {
71
+ return { messageCount: 0, isLarge: false };
72
+ }
73
+ }
74
+
75
+ async function main() {
76
+ let hookInput: HookInput | null = null;
77
+
78
+ try {
79
+ // Read the JSON input from stdin
80
+ const decoder = new TextDecoder();
81
+ let input = '';
82
+
83
+ const timeoutPromise = new Promise<void>((resolve) => {
84
+ setTimeout(() => resolve(), 500);
85
+ });
86
+
87
+ const readPromise = (async () => {
88
+ for await (const chunk of process.stdin) {
89
+ input += decoder.decode(chunk, { stream: true });
90
+ }
91
+ })();
92
+
93
+ await Promise.race([readPromise, timeoutPromise]);
94
+
95
+ if (input.trim()) {
96
+ hookInput = JSON.parse(input) as HookInput;
97
+ }
98
+ } catch (error) {
99
+ // Silently handle input errors
100
+ }
101
+
102
+ // Determine the type of compression
103
+ const compactType = hookInput?.compact_type || 'auto';
104
+ let message = 'Compressing context to continue';
105
+
106
+ // Get transcript statistics if available
107
+ let tokenCount = 0;
108
+ if (hookInput && hookInput.transcript_path) {
109
+ const stats = getTranscriptStats(hookInput.transcript_path);
110
+
111
+ // Calculate approximate token count
112
+ tokenCount = calculateSessionTokens(hookInput.transcript_path);
113
+ const tokenDisplay = tokenCount > 1000
114
+ ? `${Math.round(tokenCount / 1000)}k`
115
+ : String(tokenCount);
116
+
117
+ if (stats.messageCount > 0) {
118
+ if (compactType === 'manual') {
119
+ message = `Manually compressing ${stats.messageCount} messages (~${tokenDisplay} tokens)`;
120
+ } else {
121
+ message = stats.isLarge
122
+ ? `Auto-compressing large context (~${tokenDisplay} tokens)`
123
+ : `Compressing context (~${tokenDisplay} tokens)`;
124
+ }
125
+ }
126
+
127
+ // Save checkpoint to session note before compression
128
+ try {
129
+ const transcriptDir = dirname(hookInput.transcript_path);
130
+ const notesDir = join(transcriptDir, 'Notes');
131
+ const currentNotePath = getCurrentNotePath(notesDir);
132
+
133
+ if (currentNotePath) {
134
+ const checkpoint = `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
135
+ appendCheckpoint(currentNotePath, checkpoint);
136
+ console.error(`Checkpoint saved before compression: ${basename(currentNotePath)}`);
137
+ }
138
+ } catch (noteError) {
139
+ console.error(`Could not save checkpoint: ${noteError}`);
140
+ }
141
+ }
142
+
143
+ // Send ntfy.sh notification
144
+ const ntfyMessage = tokenCount > 0
145
+ ? `Auto-pause: ~${Math.round(tokenCount / 1000)}k tokens`
146
+ : 'Context compressing';
147
+ await sendNtfyNotification(ntfyMessage);
148
+
149
+ process.exit(0);
150
+ }
151
+
152
+ // Run the hook
153
+ main().catch(() => {
154
+ process.exit(0);
155
+ });