@tekmidian/pai 0.5.6 → 0.6.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 (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +107 -3
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1897 -1569
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +12 -9
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
  18. package/dist/db-BtuN768f.mjs.map +1 -0
  19. package/dist/db-DdUperSl.mjs +110 -0
  20. package/dist/db-DdUperSl.mjs.map +1 -0
  21. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  22. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  23. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  24. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  25. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  26. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  27. package/dist/helpers-BEST-4Gx.mjs +420 -0
  28. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  29. package/dist/hooks/capture-all-events.mjs +19 -4
  30. package/dist/hooks/capture-all-events.mjs.map +4 -4
  31. package/dist/hooks/capture-session-summary.mjs +38 -0
  32. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  33. package/dist/hooks/cleanup-session-files.mjs +6 -12
  34. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  35. package/dist/hooks/context-compression-hook.mjs +105 -111
  36. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  37. package/dist/hooks/initialize-session.mjs +26 -17
  38. package/dist/hooks/initialize-session.mjs.map +4 -4
  39. package/dist/hooks/inject-observations.mjs +220 -0
  40. package/dist/hooks/inject-observations.mjs.map +7 -0
  41. package/dist/hooks/load-core-context.mjs +18 -2
  42. package/dist/hooks/load-core-context.mjs.map +4 -4
  43. package/dist/hooks/load-project-context.mjs +102 -97
  44. package/dist/hooks/load-project-context.mjs.map +4 -4
  45. package/dist/hooks/observe.mjs +354 -0
  46. package/dist/hooks/observe.mjs.map +7 -0
  47. package/dist/hooks/stop-hook.mjs +174 -90
  48. package/dist/hooks/stop-hook.mjs.map +4 -4
  49. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  50. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  51. package/dist/index.d.mts +32 -9
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +6 -9
  54. package/dist/indexer-D53l5d1U.mjs +1 -0
  55. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  56. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  57. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  58. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  59. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  60. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  61. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  62. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  63. package/dist/note-context-BK24bX8Y.mjs +126 -0
  64. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  65. package/dist/postgres-CKf-EDtS.mjs +846 -0
  66. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  67. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  68. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  69. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  70. package/dist/search-DC1qhkKn.mjs.map +1 -0
  71. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  72. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  73. package/dist/state-C6_vqz7w.mjs +102 -0
  74. package/dist/state-C6_vqz7w.mjs.map +1 -0
  75. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  76. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  77. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  78. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  79. package/dist/themes-BvYF0W8T.mjs +148 -0
  80. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  81. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  82. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  83. package/dist/trace-CRx9lPuc.mjs +137 -0
  84. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  85. package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
  86. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  87. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  88. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  89. package/package.json +1 -2
  90. package/src/hooks/ts/capture-all-events.ts +6 -0
  91. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  92. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  93. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  94. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  95. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  96. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  97. package/src/hooks/ts/lib/project-utils.ts +40 -999
  98. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  99. package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
  100. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  101. package/src/hooks/ts/session-start/initialize-session.ts +7 -1
  102. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  103. package/src/hooks/ts/session-start/load-core-context.ts +7 -0
  104. package/src/hooks/ts/session-start/load-project-context.ts +8 -1
  105. package/src/hooks/ts/stop/stop-hook.ts +28 -0
  106. package/templates/claude-md.template.md +7 -74
  107. package/templates/skills/user/.gitkeep +0 -0
  108. package/dist/chunker-CbnBe0s0.mjs +0 -191
  109. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  110. package/dist/config-Cf92lGX_.mjs.map +0 -1
  111. package/dist/daemon-D9evGlgR.mjs.map +0 -1
  112. package/dist/db-4lSqLFb8.mjs.map +0 -1
  113. package/dist/db-Dp8VXIMR.mjs +0 -212
  114. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  115. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  116. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  117. package/dist/mcp/index.d.mts +0 -1
  118. package/dist/mcp/index.mjs +0 -500
  119. package/dist/mcp/index.mjs.map +0 -1
  120. package/dist/postgres-FXrHDPcE.mjs +0 -358
  121. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  122. package/dist/schemas-BFIgGntb.mjs +0 -3405
  123. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  124. package/dist/search-_oHfguA5.mjs.map +0 -1
  125. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  126. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  127. package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
  128. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  129. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  130. package/templates/README.md +0 -181
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. package/templates/templates.md +0 -20
@@ -1,1000 +1,41 @@
1
1
  /**
2
- * project-utils.ts - Shared utilities for project context management
3
- *
4
- * Provides:
5
- * - Path encoding (matching Claude Code's scheme)
6
- * - ntfy.sh notifications (mandatory, synchronous)
7
- * - Session notes management
8
- * - Session token calculation
9
- */
10
-
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
12
- import { join, basename } from 'path';
13
-
14
- // Import from pai-paths which handles .env loading and path resolution
15
- import { PAI_DIR } from './pai-paths.js';
16
-
17
- // Re-export PAI_DIR for consumers
18
- export { PAI_DIR };
19
- export const PROJECTS_DIR = join(PAI_DIR, 'projects');
20
-
21
- /**
22
- * Encode a path the same way Claude Code does:
23
- * - Replace / with -
24
- * - Replace . with - (hidden directories become --name)
25
- *
26
- * This matches Claude Code's internal encoding to ensure Notes
27
- * are stored in the same project directory as transcripts.
28
- */
29
- export function encodePath(path: string): string {
30
- return path
31
- .replace(/\//g, '-') // Slashes become dashes
32
- .replace(/\./g, '-') // Dots also become dashes
33
- .replace(/ /g, '-'); // Spaces become dashes (matches Claude Code native encoding)
34
- }
35
-
36
- /**
37
- * Get the project directory for a given working directory
38
- */
39
- export function getProjectDir(cwd: string): string {
40
- const encoded = encodePath(cwd);
41
- return join(PROJECTS_DIR, encoded);
42
- }
43
-
44
- /**
45
- * Get the Notes directory for a project (central location)
46
- */
47
- export function getNotesDir(cwd: string): string {
48
- return join(getProjectDir(cwd), 'Notes');
49
- }
50
-
51
- /**
52
- * Find Notes directory - check local first, fallback to central
53
- * DOES NOT create the directory - just finds the right location
54
- *
55
- * Logic:
56
- * - If cwd itself IS a Notes directory → use it directly
57
- * - If local Notes/ exists → use it (can be checked into git)
58
- * - Otherwise → use central ~/.claude/projects/.../Notes/
59
- */
60
- export function findNotesDir(cwd: string): { path: string; isLocal: boolean } {
61
- // FIRST: Check if cwd itself IS a Notes directory
62
- const cwdBasename = basename(cwd).toLowerCase();
63
- if (cwdBasename === 'notes' && existsSync(cwd)) {
64
- return { path: cwd, isLocal: true };
65
- }
66
-
67
- // Check local locations
68
- const localPaths = [
69
- join(cwd, 'Notes'),
70
- join(cwd, 'notes'),
71
- join(cwd, '.claude', 'Notes')
72
- ];
73
-
74
- for (const path of localPaths) {
75
- if (existsSync(path)) {
76
- return { path, isLocal: true };
77
- }
78
- }
79
-
80
- // Fallback to central location
81
- return { path: getNotesDir(cwd), isLocal: false };
82
- }
83
-
84
- /**
85
- * Get the Sessions directory for a project (stores .jsonl transcripts)
86
- */
87
- export function getSessionsDir(cwd: string): string {
88
- return join(getProjectDir(cwd), 'sessions');
89
- }
90
-
91
- /**
92
- * Get the Sessions directory from a project directory path
93
- */
94
- export function getSessionsDirFromProjectDir(projectDir: string): string {
95
- return join(projectDir, 'sessions');
96
- }
97
-
98
- /**
99
- * Check if WhatsApp (Whazaa) is configured as an enabled MCP server.
100
- *
101
- * Uses standard Claude Code config at ~/.claude/settings.json.
102
- * No PAI dependency — works for any Claude Code user with whazaa installed.
103
- */
104
- export function isWhatsAppEnabled(): boolean {
105
- try {
106
- const { homedir } = require('os');
107
- const settingsPath = join(homedir(), '.claude', 'settings.json');
108
- if (!existsSync(settingsPath)) return false;
109
-
110
- const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
111
- const enabled: string[] = settings.enabledMcpjsonServers || [];
112
- return enabled.includes('whazaa');
113
- } catch {
114
- return false;
115
- }
116
- }
117
-
118
- /**
119
- * Send push notification — WhatsApp-aware with ntfy fallback.
120
- *
121
- * When WhatsApp (Whazaa) is enabled in MCP config, ntfy is SKIPPED
122
- * because the AI sends WhatsApp messages directly via MCP. Sending both
123
- * would cause duplicate notifications.
124
- *
125
- * When WhatsApp is NOT configured, ntfy fires as the fallback channel.
126
- */
127
- export async function sendNtfyNotification(message: string, retries = 2): Promise<boolean> {
128
- // Skip ntfy when WhatsApp is configured — the AI handles notifications via MCP
129
- if (isWhatsAppEnabled()) {
130
- console.error(`WhatsApp (Whazaa) enabled in MCP config — skipping ntfy`);
131
- return true;
132
- }
133
-
134
- const topic = process.env.NTFY_TOPIC;
135
-
136
- if (!topic) {
137
- console.error('NTFY_TOPIC not set and WhatsApp not active — notifications disabled');
138
- return false;
139
- }
140
-
141
- for (let attempt = 0; attempt <= retries; attempt++) {
142
- try {
143
- const response = await fetch(`https://ntfy.sh/${topic}`, {
144
- method: 'POST',
145
- body: message,
146
- headers: {
147
- 'Title': 'Claude Code',
148
- 'Priority': 'default'
149
- }
150
- });
151
-
152
- if (response.ok) {
153
- console.error(`ntfy.sh notification sent (WhatsApp inactive): "${message}"`);
154
- return true;
155
- } else {
156
- console.error(`ntfy.sh attempt ${attempt + 1} failed: ${response.status}`);
157
- }
158
- } catch (error) {
159
- console.error(`ntfy.sh attempt ${attempt + 1} error: ${error}`);
160
- }
161
-
162
- // Wait before retry
163
- if (attempt < retries) {
164
- await new Promise(resolve => setTimeout(resolve, 1000));
165
- }
166
- }
167
-
168
- console.error('ntfy.sh notification failed after all retries');
169
- return false;
170
- }
171
-
172
- /**
173
- * Ensure the Notes directory exists for a project
174
- * DEPRECATED: Use ensureNotesDirSmart() instead
175
- */
176
- export function ensureNotesDir(cwd: string): string {
177
- const notesDir = getNotesDir(cwd);
178
-
179
- if (!existsSync(notesDir)) {
180
- mkdirSync(notesDir, { recursive: true });
181
- console.error(`Created Notes directory: ${notesDir}`);
182
- }
183
-
184
- return notesDir;
185
- }
186
-
187
- /**
188
- * Smart Notes directory handling:
189
- * - If local Notes/ exists → use it (don't create anything new)
190
- * - If no local Notes/ → ensure central exists and use that
191
- *
192
- * This respects the user's choice:
193
- * - Projects with local Notes/ keep notes there (git-trackable)
194
- * - Other directories don't get cluttered with auto-created Notes/
195
- */
196
- export function ensureNotesDirSmart(cwd: string): { path: string; isLocal: boolean } {
197
- const found = findNotesDir(cwd);
198
-
199
- if (found.isLocal) {
200
- // Local Notes/ exists - use it as-is
201
- return found;
202
- }
203
-
204
- // No local Notes/ - ensure central exists
205
- if (!existsSync(found.path)) {
206
- mkdirSync(found.path, { recursive: true });
207
- console.error(`Created central Notes directory: ${found.path}`);
208
- }
209
-
210
- return found;
211
- }
212
-
213
- /**
214
- * Ensure the Sessions directory exists for a project
215
- */
216
- export function ensureSessionsDir(cwd: string): string {
217
- const sessionsDir = getSessionsDir(cwd);
218
-
219
- if (!existsSync(sessionsDir)) {
220
- mkdirSync(sessionsDir, { recursive: true });
221
- console.error(`Created sessions directory: ${sessionsDir}`);
222
- }
223
-
224
- return sessionsDir;
225
- }
226
-
227
- /**
228
- * Ensure the Sessions directory exists (from project dir path)
229
- */
230
- export function ensureSessionsDirFromProjectDir(projectDir: string): string {
231
- const sessionsDir = getSessionsDirFromProjectDir(projectDir);
232
-
233
- if (!existsSync(sessionsDir)) {
234
- mkdirSync(sessionsDir, { recursive: true });
235
- console.error(`Created sessions directory: ${sessionsDir}`);
236
- }
237
-
238
- return sessionsDir;
239
- }
240
-
241
- /**
242
- * Move all .jsonl session files from project root to sessions/ subdirectory
243
- * @param projectDir - The project directory path
244
- * @param excludeFile - Optional filename to exclude (e.g., current active session)
245
- * @param silent - If true, suppress console output
246
- * Returns the number of files moved
247
- */
248
- export function moveSessionFilesToSessionsDir(
249
- projectDir: string,
250
- excludeFile?: string,
251
- silent: boolean = false
252
- ): number {
253
- const sessionsDir = ensureSessionsDirFromProjectDir(projectDir);
254
-
255
- if (!existsSync(projectDir)) {
256
- return 0;
257
- }
258
-
259
- const files = readdirSync(projectDir);
260
- let movedCount = 0;
261
-
262
- for (const file of files) {
263
- // Match session files: uuid.jsonl or agent-*.jsonl
264
- // Skip the excluded file (typically the current active session)
265
- if (file.endsWith('.jsonl') && file !== excludeFile) {
266
- const sourcePath = join(projectDir, file);
267
- const destPath = join(sessionsDir, file);
268
-
269
- try {
270
- renameSync(sourcePath, destPath);
271
- if (!silent) {
272
- console.error(`Moved ${file} → sessions/`);
273
- }
274
- movedCount++;
275
- } catch (error) {
276
- if (!silent) {
277
- console.error(`Could not move ${file}: ${error}`);
278
- }
279
- }
280
- }
281
- }
282
-
283
- return movedCount;
284
- }
285
-
286
- /**
287
- * Get the YYYY/MM subdirectory for the current month inside notesDir.
288
- * Creates the directory if it doesn't exist.
289
- */
290
- function getMonthDir(notesDir: string): string {
291
- const now = new Date();
292
- const year = String(now.getFullYear());
293
- const month = String(now.getMonth() + 1).padStart(2, '0');
294
- const monthDir = join(notesDir, year, month);
295
- if (!existsSync(monthDir)) {
296
- mkdirSync(monthDir, { recursive: true });
297
- }
298
- return monthDir;
299
- }
300
-
301
- /**
302
- * Get the next note number (4-digit format: 0001, 0002, etc.)
303
- * ALWAYS uses 4-digit format with space-dash-space separators
304
- * Format: NNNN - YYYY-MM-DD - Description.md
305
- * Numbers reset per month (each YYYY/MM directory has its own sequence).
306
- */
307
- export function getNextNoteNumber(notesDir: string): string {
308
- const monthDir = getMonthDir(notesDir);
309
-
310
- // Match CORRECT format: "0001 - " (4-digit with space-dash-space)
311
- // Also match legacy formats for backwards compatibility when detecting max number
312
- const files = readdirSync(monthDir)
313
- .filter(f => f.match(/^\d{3,4}[\s_-]/)) // Starts with 3-4 digits followed by separator
314
- .filter(f => f.endsWith('.md'))
315
- .sort();
316
-
317
- if (files.length === 0) {
318
- return '0001'; // Default to 4-digit
319
- }
320
-
321
- // Find the highest number across all formats
322
- let maxNumber = 0;
323
- for (const file of files) {
324
- const digitMatch = file.match(/^(\d+)/);
325
- if (digitMatch) {
326
- const num = parseInt(digitMatch[1], 10);
327
- if (num > maxNumber) maxNumber = num;
328
- }
329
- }
330
-
331
- // ALWAYS return 4-digit format
332
- return String(maxNumber + 1).padStart(4, '0');
333
- }
334
-
335
- /**
336
- * Get the current (latest) note file path, or null if none exists.
337
- * Searches in the current month's YYYY/MM subdirectory first,
338
- * then falls back to previous month (for sessions spanning month boundaries),
339
- * then falls back to flat notesDir for legacy notes.
340
- * Supports multiple formats for backwards compatibility:
341
- * - CORRECT: "0001 - YYYY-MM-DD - Description.md" (space-dash-space)
342
- * - Legacy: "001_YYYY-MM-DD_description.md" (underscores)
343
- */
344
- export function getCurrentNotePath(notesDir: string): string | null {
345
- if (!existsSync(notesDir)) {
346
- return null;
347
- }
348
-
349
- // Helper: find latest session note in a directory
350
- const findLatestIn = (dir: string): string | null => {
351
- if (!existsSync(dir)) return null;
352
- const files = readdirSync(dir)
353
- .filter(f => f.match(/^\d{3,4}[\s_-].*\.md$/))
354
- .sort((a, b) => {
355
- const numA = parseInt(a.match(/^(\d+)/)?.[1] || '0', 10);
356
- const numB = parseInt(b.match(/^(\d+)/)?.[1] || '0', 10);
357
- return numA - numB;
358
- });
359
- if (files.length === 0) return null;
360
- return join(dir, files[files.length - 1]);
361
- };
362
-
363
- // 1. Check current month's YYYY/MM directory
364
- const now = new Date();
365
- const year = String(now.getFullYear());
366
- const month = String(now.getMonth() + 1).padStart(2, '0');
367
- const currentMonthDir = join(notesDir, year, month);
368
- const found = findLatestIn(currentMonthDir);
369
- if (found) return found;
370
-
371
- // 2. Check previous month (for sessions spanning month boundaries)
372
- const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
373
- const prevYear = String(prevDate.getFullYear());
374
- const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');
375
- const prevMonthDir = join(notesDir, prevYear, prevMonth);
376
- const prevFound = findLatestIn(prevMonthDir);
377
- if (prevFound) return prevFound;
378
-
379
- // 3. Fallback: check flat notesDir (legacy notes not yet filed)
380
- return findLatestIn(notesDir);
381
- }
382
-
383
- /**
384
- * Create a new session note
385
- * CORRECT FORMAT: "NNNN - YYYY-MM-DD - Description.md"
386
- * - 4-digit zero-padded number
387
- * - Space-dash-space separators (NOT underscores)
388
- * - Title case description
389
- *
390
- * IMPORTANT: The initial description is just a PLACEHOLDER.
391
- * Claude MUST rename the file at session end with a meaningful description
392
- * based on the actual work done. Never leave it as "New Session" or project name.
393
- */
394
- export function createSessionNote(notesDir: string, description: string): string {
395
- const noteNumber = getNextNoteNumber(notesDir);
396
- const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
397
-
398
- // Use "New Session" as placeholder - Claude MUST rename at session end!
399
- // The project name alone is NOT descriptive enough.
400
- const safeDescription = 'New Session';
401
-
402
- // CORRECT FORMAT: space-dash-space separators, filed into YYYY/MM subdirectory
403
- const monthDir = getMonthDir(notesDir);
404
- const filename = `${noteNumber} - ${date} - ${safeDescription}.md`;
405
- const filepath = join(monthDir, filename);
406
-
407
- const content = `# Session ${noteNumber}: ${description}
408
-
409
- **Date:** ${date}
410
- **Status:** In Progress
411
-
412
- ---
413
-
414
- ## Work Done
415
-
416
- <!-- PAI will add completed work here during session -->
417
-
418
- ---
419
-
420
- ## Next Steps
421
-
422
- <!-- To be filled at session end -->
423
-
424
- ---
425
-
426
- **Tags:** #Session
427
- `;
428
-
429
- writeFileSync(filepath, content);
430
- console.error(`Created session note: ${filename}`);
431
-
432
- return filepath;
433
- }
434
-
435
- /**
436
- * Append checkpoint to current session note
437
- */
438
- export function appendCheckpoint(notePath: string, checkpoint: string): void {
439
- if (!existsSync(notePath)) {
440
- // Note vanished (cloud sync, cleanup, etc.) — recreate it
441
- console.error(`Note file not found, recreating: ${notePath}`);
442
- try {
443
- const parentDir = join(notePath, '..');
444
- if (!existsSync(parentDir)) {
445
- mkdirSync(parentDir, { recursive: true });
446
- }
447
- const noteFilename = basename(notePath);
448
- const numberMatch = noteFilename.match(/^(\d+)/);
449
- const noteNumber = numberMatch ? numberMatch[1] : '0000';
450
- const date = new Date().toISOString().split('T')[0];
451
- const content = `# Session ${noteNumber}: Recovered\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;
452
- writeFileSync(notePath, content);
453
- console.error(`Recreated session note: ${noteFilename}`);
454
- } catch (err) {
455
- console.error(`Failed to recreate note: ${err}`);
456
- return;
457
- }
458
- }
459
-
460
- const content = readFileSync(notePath, 'utf-8');
461
- const timestamp = new Date().toISOString();
462
- const checkpointText = `\n### Checkpoint ${timestamp}\n\n${checkpoint}\n`;
463
-
464
- // Insert before "## Next Steps" if it exists, otherwise append
465
- const nextStepsIndex = content.indexOf('## Next Steps');
466
- let newContent: string;
467
-
468
- if (nextStepsIndex !== -1) {
469
- newContent = content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex);
470
- } else {
471
- newContent = content + checkpointText;
472
- }
473
-
474
- writeFileSync(notePath, newContent);
475
- console.error(`Checkpoint added to: ${basename(notePath)}`);
476
- }
477
-
478
- /**
479
- * Work item for session notes
480
- */
481
- export interface WorkItem {
482
- title: string;
483
- details?: string[];
484
- completed?: boolean;
485
- }
486
-
487
- /**
488
- * Add work items to the "Work Done" section of a session note
489
- * This is the main way to capture what was accomplished in a session
490
- */
491
- export function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {
492
- if (!existsSync(notePath)) {
493
- console.error(`Note file not found: ${notePath}`);
494
- return;
495
- }
496
-
497
- let content = readFileSync(notePath, 'utf-8');
498
-
499
- // Build the work section content
500
- let workText = '';
501
- if (sectionTitle) {
502
- workText += `\n### ${sectionTitle}\n\n`;
503
- }
504
-
505
- for (const item of workItems) {
506
- const checkbox = item.completed !== false ? '[x]' : '[ ]';
507
- workText += `- ${checkbox} **${item.title}**\n`;
508
- if (item.details && item.details.length > 0) {
509
- for (const detail of item.details) {
510
- workText += ` - ${detail}\n`;
511
- }
512
- }
513
- }
514
-
515
- // Find the Work Done section and insert after the comment/placeholder
516
- const workDoneMatch = content.match(/## Work Done\n\n(<!-- .*? -->)?/);
517
- if (workDoneMatch) {
518
- const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;
519
- content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);
520
- } else {
521
- // Fallback: insert before Next Steps
522
- const nextStepsIndex = content.indexOf('## Next Steps');
523
- if (nextStepsIndex !== -1) {
524
- content = content.substring(0, nextStepsIndex) + workText + '\n' + content.substring(nextStepsIndex);
525
- }
526
- }
527
-
528
- writeFileSync(notePath, content);
529
- console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
530
- }
531
-
532
- /**
533
- * Update the session note title to be more descriptive
534
- * Called when we know what work was done
535
- */
536
- export function updateSessionNoteTitle(notePath: string, newTitle: string): void {
537
- if (!existsSync(notePath)) {
538
- console.error(`Note file not found: ${notePath}`);
539
- return;
540
- }
541
-
542
- let content = readFileSync(notePath, 'utf-8');
543
-
544
- // Update the H1 title
545
- content = content.replace(/^# Session \d+:.*$/m, (match) => {
546
- const sessionNum = match.match(/Session (\d+)/)?.[1] || '';
547
- return `# Session ${sessionNum}: ${newTitle}`;
548
- });
549
-
550
- writeFileSync(notePath, content);
551
-
552
- // Also rename the file
553
- renameSessionNote(notePath, sanitizeForFilename(newTitle));
554
- }
555
-
556
- /**
557
- * Sanitize a string for use in a filename (exported for use elsewhere)
558
- */
559
- export function sanitizeForFilename(str: string): string {
560
- return str
561
- .toLowerCase()
562
- .replace(/[^a-z0-9\s-]/g, '') // Remove special chars
563
- .replace(/\s+/g, '-') // Spaces to hyphens
564
- .replace(/-+/g, '-') // Collapse multiple hyphens
565
- .replace(/^-|-$/g, '') // Trim hyphens
566
- .substring(0, 50); // Limit length
567
- }
568
-
569
- /**
570
- * Extract a meaningful name from session note content
571
- * Looks at Work Done section and summary to generate a descriptive name
572
- */
573
- export function extractMeaningfulName(noteContent: string, summary: string): string {
574
- // Try to extract from Work Done section headers (### headings)
575
- const workDoneMatch = noteContent.match(/## Work Done\n\n([\s\S]*?)(?=\n---|\n## Next)/);
576
-
577
- if (workDoneMatch) {
578
- const workDoneSection = workDoneMatch[1];
579
-
580
- // Look for ### subheadings which typically describe what was done
581
- const subheadings = workDoneSection.match(/### ([^\n]+)/g);
582
- if (subheadings && subheadings.length > 0) {
583
- // Use the first subheading, clean it up
584
- const firstHeading = subheadings[0].replace('### ', '').trim();
585
- if (firstHeading.length > 5 && firstHeading.length < 60) {
586
- return sanitizeForFilename(firstHeading);
587
- }
588
- }
589
-
590
- // Look for bold text which often indicates key topics
591
- const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
592
- if (boldMatches && boldMatches.length > 0) {
593
- const firstBold = boldMatches[0].replace(/\*\*/g, '').trim();
594
- if (firstBold.length > 3 && firstBold.length < 50) {
595
- return sanitizeForFilename(firstBold);
596
- }
597
- }
598
-
599
- // Look for numbered list items (1. Something)
600
- const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
601
- if (numberedItems) {
602
- return sanitizeForFilename(numberedItems[1]);
603
- }
604
- }
605
-
606
- // Fall back to summary if provided
607
- if (summary && summary.length > 5 && summary !== 'Session completed.') {
608
- // Take first meaningful phrase from summary
609
- const cleanSummary = summary
610
- .replace(/[^\w\s-]/g, ' ')
611
- .trim()
612
- .split(/\s+/)
613
- .slice(0, 5)
614
- .join(' ');
615
-
616
- if (cleanSummary.length > 3) {
617
- return sanitizeForFilename(cleanSummary);
618
- }
619
- }
620
-
621
- return '';
622
- }
623
-
624
- /**
625
- * Rename session note with a meaningful name
626
- * ALWAYS uses correct format: "NNNN - YYYY-MM-DD - Description.md"
627
- * Returns the new path, or original path if rename fails
628
- */
629
- export function renameSessionNote(notePath: string, meaningfulName: string): string {
630
- if (!meaningfulName || !existsSync(notePath)) {
631
- return notePath;
632
- }
633
-
634
- const dir = join(notePath, '..');
635
- const oldFilename = basename(notePath);
636
-
637
- // Parse existing filename - support multiple formats:
638
- // CORRECT: "0001 - 2026-01-02 - Description.md"
639
- // Legacy: "001_2026-01-02_description.md"
640
- const correctMatch = oldFilename.match(/^(\d{3,4}) - (\d{4}-\d{2}-\d{2}) - .*\.md$/);
641
- const legacyMatch = oldFilename.match(/^(\d{3,4})_(\d{4}-\d{2}-\d{2})_.*\.md$/);
642
-
643
- const match = correctMatch || legacyMatch;
644
- if (!match) {
645
- return notePath; // Can't parse, don't rename
646
- }
647
-
648
- const [, noteNumber, date] = match;
649
-
650
- // Convert to Title Case
651
- const titleCaseName = meaningfulName
652
- .split(/[\s_-]+/)
653
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
654
- .join(' ')
655
- .trim();
656
-
657
- // ALWAYS use correct format with 4-digit number
658
- const paddedNumber = noteNumber.padStart(4, '0');
659
- const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
660
- const newPath = join(dir, newFilename);
661
-
662
- // Don't rename if name is the same
663
- if (newFilename === oldFilename) {
664
- return notePath;
665
- }
666
-
667
- try {
668
- renameSync(notePath, newPath);
669
- console.error(`Renamed note: ${oldFilename} → ${newFilename}`);
670
- return newPath;
671
- } catch (error) {
672
- console.error(`Could not rename note: ${error}`);
673
- return notePath;
674
- }
675
- }
676
-
677
- /**
678
- * Finalize session note (mark as complete, add summary, rename with meaningful name)
679
- * IDEMPOTENT: Will only finalize once, subsequent calls are no-ops
680
- * Returns the final path (may be renamed)
681
- */
682
- export function finalizeSessionNote(notePath: string, summary: string): string {
683
- if (!existsSync(notePath)) {
684
- console.error(`Note file not found: ${notePath}`);
685
- return notePath;
686
- }
687
-
688
- let content = readFileSync(notePath, 'utf-8');
689
-
690
- // IDEMPOTENT CHECK: If already completed, don't modify again
691
- if (content.includes('**Status:** Completed')) {
692
- console.error(`Note already finalized: ${basename(notePath)}`);
693
- return notePath;
694
- }
695
-
696
- // Update status
697
- content = content.replace('**Status:** In Progress', '**Status:** Completed');
698
-
699
- // Add completion timestamp (only if not already present)
700
- if (!content.includes('**Completed:**')) {
701
- const completionTime = new Date().toISOString();
702
- content = content.replace(
703
- '---\n\n## Work Done',
704
- `**Completed:** ${completionTime}\n\n---\n\n## Work Done`
705
- );
706
- }
707
-
708
- // Add summary to Next Steps section (only if placeholder exists)
709
- const nextStepsMatch = content.match(/## Next Steps\n\n(<!-- .*? -->)/);
710
- if (nextStepsMatch) {
711
- content = content.replace(
712
- nextStepsMatch[0],
713
- `## Next Steps\n\n${summary || 'Session completed.'}`
714
- );
715
- }
716
-
717
- writeFileSync(notePath, content);
718
- console.error(`Session note finalized: ${basename(notePath)}`);
719
-
720
- // Extract meaningful name and rename the file
721
- const meaningfulName = extractMeaningfulName(content, summary);
722
- if (meaningfulName) {
723
- const newPath = renameSessionNote(notePath, meaningfulName);
724
- return newPath;
725
- }
726
-
727
- return notePath;
728
- }
729
-
730
- /**
731
- * Calculate total tokens from a session .jsonl file
732
- */
733
- export function calculateSessionTokens(jsonlPath: string): number {
734
- if (!existsSync(jsonlPath)) {
735
- return 0;
736
- }
737
-
738
- try {
739
- const content = readFileSync(jsonlPath, 'utf-8');
740
- const lines = content.trim().split('\n');
741
- let totalTokens = 0;
742
-
743
- for (const line of lines) {
744
- try {
745
- const entry = JSON.parse(line);
746
- if (entry.message?.usage) {
747
- const usage = entry.message.usage;
748
- totalTokens += (usage.input_tokens || 0);
749
- totalTokens += (usage.output_tokens || 0);
750
- totalTokens += (usage.cache_creation_input_tokens || 0);
751
- totalTokens += (usage.cache_read_input_tokens || 0);
752
- }
753
- } catch {
754
- // Skip invalid JSON lines
755
- }
756
- }
757
-
758
- return totalTokens;
759
- } catch (error) {
760
- console.error(`Error calculating tokens: ${error}`);
761
- return 0;
762
- }
763
- }
764
-
765
- /**
766
- * Find TODO.md - check local first, fallback to central
767
- */
768
- export function findTodoPath(cwd: string): string {
769
- // Check local locations first
770
- const localPaths = [
771
- join(cwd, 'TODO.md'),
772
- join(cwd, 'notes', 'TODO.md'),
773
- join(cwd, 'Notes', 'TODO.md'),
774
- join(cwd, '.claude', 'TODO.md')
775
- ];
776
-
777
- for (const path of localPaths) {
778
- if (existsSync(path)) {
779
- return path;
780
- }
781
- }
782
-
783
- // Fallback to central location (inside Notes/)
784
- return join(getNotesDir(cwd), 'TODO.md');
785
- }
786
-
787
- /**
788
- * Find CLAUDE.md - check local locations
789
- * Returns the FIRST found path (for backwards compatibility)
790
- */
791
- export function findClaudeMdPath(cwd: string): string | null {
792
- const paths = findAllClaudeMdPaths(cwd);
793
- return paths.length > 0 ? paths[0] : null;
794
- }
795
-
796
- /**
797
- * Find ALL CLAUDE.md files in local locations
798
- * Returns paths in priority order (most specific first):
799
- * 1. .claude/CLAUDE.md (project-specific config dir)
800
- * 2. CLAUDE.md (project root)
801
- * 3. Notes/CLAUDE.md (notes directory)
802
- * 4. Prompts/CLAUDE.md (prompts directory)
803
- *
804
- * All found files will be loaded and injected into context.
805
- */
806
- export function findAllClaudeMdPaths(cwd: string): string[] {
807
- const foundPaths: string[] = [];
808
-
809
- // Priority order: most specific first
810
- const localPaths = [
811
- join(cwd, '.claude', 'CLAUDE.md'),
812
- join(cwd, 'CLAUDE.md'),
813
- join(cwd, 'Notes', 'CLAUDE.md'),
814
- join(cwd, 'notes', 'CLAUDE.md'),
815
- join(cwd, 'Prompts', 'CLAUDE.md'),
816
- join(cwd, 'prompts', 'CLAUDE.md')
817
- ];
818
-
819
- for (const path of localPaths) {
820
- if (existsSync(path)) {
821
- foundPaths.push(path);
822
- }
823
- }
824
-
825
- return foundPaths;
826
- }
827
-
828
- /**
829
- * Ensure TODO.md exists
830
- */
831
- export function ensureTodoMd(cwd: string): string {
832
- const todoPath = findTodoPath(cwd);
833
-
834
- if (!existsSync(todoPath)) {
835
- // Ensure parent directory exists
836
- const parentDir = join(todoPath, '..');
837
- if (!existsSync(parentDir)) {
838
- mkdirSync(parentDir, { recursive: true });
839
- }
840
-
841
- const content = `# TODO
842
-
843
- ## Current Session
844
-
845
- - [ ] (Tasks will be tracked here)
846
-
847
- ## Backlog
848
-
849
- - [ ] (Future tasks)
850
-
851
- ---
852
-
853
- *Last updated: ${new Date().toISOString()}*
854
- `;
855
-
856
- writeFileSync(todoPath, content);
857
- console.error(`Created TODO.md: ${todoPath}`);
858
- }
859
-
860
- return todoPath;
861
- }
862
-
863
- /**
864
- * Task item for TODO.md
865
- */
866
- export interface TodoItem {
867
- content: string;
868
- completed: boolean;
869
- }
870
-
871
- /**
872
- * Update TODO.md with current session tasks
873
- * Preserves the Backlog section
874
- * Ensures only ONE timestamp line at the end
875
- */
876
- export function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {
877
- const todoPath = ensureTodoMd(cwd);
878
- const content = readFileSync(todoPath, 'utf-8');
879
-
880
- // Find Backlog section to preserve it (but strip any trailing timestamps/separators)
881
- const backlogMatch = content.match(/## Backlog[\s\S]*?(?=\n---|\n\*Last updated|$)/);
882
- let backlogSection = backlogMatch ? backlogMatch[0].trim() : '## Backlog\n\n- [ ] (Future tasks)';
883
-
884
- // Format tasks
885
- const taskLines = tasks.length > 0
886
- ? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\n')
887
- : '- [ ] (No active tasks)';
888
-
889
- // Build new content with exactly ONE timestamp at the end
890
- const newContent = `# TODO
891
-
892
- ## Current Session
893
-
894
- ${taskLines}
895
-
896
- ${sessionSummary ? `**Session Summary:** ${sessionSummary}\n\n` : ''}${backlogSection}
897
-
898
- ---
899
-
900
- *Last updated: ${new Date().toISOString()}*
901
- `;
902
-
903
- writeFileSync(todoPath, newContent);
904
- console.error(`Updated TODO.md: ${todoPath}`);
905
- }
906
-
907
- /**
908
- * Add a checkpoint entry to TODO.md (without replacing tasks)
909
- * Ensures only ONE timestamp line at the end
910
- * Works regardless of TODO.md structure — appends if no known section found
911
- */
912
- export function addTodoCheckpoint(cwd: string, checkpoint: string): void {
913
- const todoPath = ensureTodoMd(cwd);
914
- let content = readFileSync(todoPath, 'utf-8');
915
-
916
- // Remove ALL existing timestamp lines and trailing separators
917
- content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, '');
918
-
919
- const checkpointText = `\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\n\n`;
920
-
921
- // Try to insert before Backlog section
922
- const backlogIndex = content.indexOf('## Backlog');
923
- if (backlogIndex !== -1) {
924
- content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
925
- } else {
926
- // No Backlog section — try before Continue section, or just append
927
- const continueIndex = content.indexOf('## Continue');
928
- if (continueIndex !== -1) {
929
- // Insert after the Continue section (find the next ## or ---)
930
- const afterContinue = content.indexOf('\n---', continueIndex);
931
- if (afterContinue !== -1) {
932
- const insertAt = afterContinue + 4; // after \n---
933
- content = content.substring(0, insertAt) + '\n' + checkpointText + content.substring(insertAt);
934
- } else {
935
- content = content.trimEnd() + '\n' + checkpointText;
936
- }
937
- } else {
938
- // No known section — just append before the end
939
- content = content.trimEnd() + '\n' + checkpointText;
940
- }
941
- }
942
-
943
- // Add exactly ONE timestamp at the end
944
- content = content.trimEnd() + `\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;
945
-
946
- writeFileSync(todoPath, content);
947
- console.error(`Checkpoint added to TODO.md`);
948
- }
949
-
950
- /**
951
- * Update the ## Continue section at the top of TODO.md.
952
- * This mirrors "pause session" behavior — gives the next session a starting point.
953
- * Replaces any existing ## Continue section.
954
- */
955
- export function updateTodoContinue(
956
- cwd: string,
957
- noteFilename: string,
958
- state: string | null,
959
- tokenDisplay: string
960
- ): void {
961
- const todoPath = ensureTodoMd(cwd);
962
- let content = readFileSync(todoPath, 'utf-8');
963
-
964
- // Remove existing ## Continue section (from ## Continue to the first standalone --- line)
965
- content = content.replace(/## Continue\n[\s\S]*?\n---\n+/, '');
966
-
967
- const now = new Date().toISOString();
968
- const stateLines = state
969
- ? state.split('\n').filter(l => l.trim()).slice(0, 10).map(l => `> ${l}`).join('\n')
970
- : `> Check the latest session note for details.`;
971
-
972
- const continueSection = `## Continue
973
-
974
- > **Last session:** ${noteFilename.replace('.md', '')}
975
- > **Paused at:** ${now}
976
- >
977
- ${stateLines}
978
-
979
- ---
980
-
981
- `;
982
-
983
- // Remove leading whitespace from content
984
- content = content.replace(/^\s+/, '');
985
-
986
- // If content starts with # title, insert after it
987
- const titleMatch = content.match(/^(# [^\n]+\n+)/);
988
- if (titleMatch) {
989
- content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);
990
- } else {
991
- content = continueSection + content;
992
- }
993
-
994
- // Clean up trailing timestamps and add fresh one
995
- content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, '');
996
- content = content.trimEnd() + `\n\n---\n\n*Last updated: ${now}*\n`;
997
-
998
- writeFileSync(todoPath, content);
999
- console.error('TODO.md ## Continue section updated');
1000
- }
2
+ * Shim — re-exports from project-utils/ directory so existing importers
3
+ * continue to work without modification. See project-utils/index.ts.
4
+ */
5
+ export type { WorkItem, TodoItem } from "./project-utils/index.js";
6
+ export {
7
+ PAI_DIR,
8
+ PROJECTS_DIR,
9
+ isProbeSession,
10
+ encodePath,
11
+ getProjectDir,
12
+ getNotesDir,
13
+ findNotesDir,
14
+ getSessionsDir,
15
+ getSessionsDirFromProjectDir,
16
+ ensureNotesDir,
17
+ ensureNotesDirSmart,
18
+ ensureSessionsDir,
19
+ ensureSessionsDirFromProjectDir,
20
+ moveSessionFilesToSessionsDir,
21
+ findTodoPath,
22
+ findClaudeMdPath,
23
+ findAllClaudeMdPaths,
24
+ isWhatsAppEnabled,
25
+ sendNtfyNotification,
26
+ getNextNoteNumber,
27
+ getCurrentNotePath,
28
+ createSessionNote,
29
+ appendCheckpoint,
30
+ addWorkToSessionNote,
31
+ sanitizeForFilename,
32
+ extractMeaningfulName,
33
+ renameSessionNote,
34
+ updateSessionNoteTitle,
35
+ finalizeSessionNote,
36
+ calculateSessionTokens,
37
+ ensureTodoMd,
38
+ updateTodoMd,
39
+ addTodoCheckpoint,
40
+ updateTodoContinue,
41
+ } from "./project-utils/index.js";