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