@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.
- package/ARCHITECTURE.md +72 -1
- package/README.md +107 -3
- package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
- package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
- package/dist/cli/index.mjs +1897 -1569
- package/dist/cli/index.mjs.map +1 -1
- package/dist/clusters-JIDQW65f.mjs +201 -0
- package/dist/clusters-JIDQW65f.mjs.map +1 -0
- package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
- package/dist/config-BuhHWyOK.mjs.map +1 -0
- package/dist/daemon/index.mjs +12 -9
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
- package/dist/daemon-D3hYb5_C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +4597 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
- package/dist/db-BtuN768f.mjs.map +1 -0
- package/dist/db-DdUperSl.mjs +110 -0
- package/dist/db-DdUperSl.mjs.map +1 -0
- package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
- package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
- package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
- package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
- package/dist/helpers-BEST-4Gx.mjs +420 -0
- package/dist/helpers-BEST-4Gx.mjs.map +1 -0
- package/dist/hooks/capture-all-events.mjs +19 -4
- package/dist/hooks/capture-all-events.mjs.map +4 -4
- package/dist/hooks/capture-session-summary.mjs +38 -0
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +6 -12
- package/dist/hooks/cleanup-session-files.mjs.map +4 -4
- package/dist/hooks/context-compression-hook.mjs +105 -111
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/initialize-session.mjs +26 -17
- package/dist/hooks/initialize-session.mjs.map +4 -4
- package/dist/hooks/inject-observations.mjs +220 -0
- package/dist/hooks/inject-observations.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +18 -2
- package/dist/hooks/load-core-context.mjs.map +4 -4
- package/dist/hooks/load-project-context.mjs +102 -97
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/observe.mjs +354 -0
- package/dist/hooks/observe.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +174 -90
- package/dist/hooks/stop-hook.mjs.map +4 -4
- package/dist/hooks/sync-todo-to-md.mjs +31 -33
- package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
- package/dist/index.d.mts +32 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -9
- package/dist/indexer-D53l5d1U.mjs +1 -0
- package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
- package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
- package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
- package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
- package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
- package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
- package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
- package/dist/note-context-BK24bX8Y.mjs +126 -0
- package/dist/note-context-BK24bX8Y.mjs.map +1 -0
- package/dist/postgres-CKf-EDtS.mjs +846 -0
- package/dist/postgres-CKf-EDtS.mjs.map +1 -0
- package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
- package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
- package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
- package/dist/search-DC1qhkKn.mjs.map +1 -0
- package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
- package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
- package/dist/state-C6_vqz7w.mjs +102 -0
- package/dist/state-C6_vqz7w.mjs.map +1 -0
- package/dist/stop-words-BaMEGVeY.mjs +326 -0
- package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
- package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
- package/dist/sync-BOsnEj2-.mjs.map +1 -0
- package/dist/themes-BvYF0W8T.mjs +148 -0
- package/dist/themes-BvYF0W8T.mjs.map +1 -0
- package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
- package/dist/tools-DcaJlYDN.mjs.map +1 -0
- package/dist/trace-CRx9lPuc.mjs +137 -0
- package/dist/trace-CRx9lPuc.mjs.map +1 -0
- package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
- package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
- package/dist/zettelkasten-cdajbnPr.mjs +708 -0
- package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
- package/package.json +1 -2
- package/src/hooks/ts/capture-all-events.ts +6 -0
- package/src/hooks/ts/lib/project-utils/index.ts +50 -0
- package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
- package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
- package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
- package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
- package/src/hooks/ts/lib/project-utils.ts +40 -999
- package/src/hooks/ts/post-tool-use/observe.ts +327 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
- package/src/hooks/ts/session-start/initialize-session.ts +7 -1
- package/src/hooks/ts/session-start/inject-observations.ts +254 -0
- package/src/hooks/ts/session-start/load-core-context.ts +7 -0
- package/src/hooks/ts/session-start/load-project-context.ts +8 -1
- package/src/hooks/ts/stop/stop-hook.ts +28 -0
- package/templates/claude-md.template.md +7 -74
- package/templates/skills/user/.gitkeep +0 -0
- package/dist/chunker-CbnBe0s0.mjs +0 -191
- package/dist/chunker-CbnBe0s0.mjs.map +0 -1
- package/dist/config-Cf92lGX_.mjs.map +0 -1
- package/dist/daemon-D9evGlgR.mjs.map +0 -1
- package/dist/db-4lSqLFb8.mjs.map +0 -1
- package/dist/db-Dp8VXIMR.mjs +0 -212
- package/dist/db-Dp8VXIMR.mjs.map +0 -1
- package/dist/indexer-CMPOiY1r.mjs.map +0 -1
- package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
- package/dist/mcp/index.d.mts +0 -1
- package/dist/mcp/index.mjs +0 -500
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/postgres-FXrHDPcE.mjs +0 -358
- package/dist/postgres-FXrHDPcE.mjs.map +0 -1
- package/dist/schemas-BFIgGntb.mjs +0 -3405
- package/dist/schemas-BFIgGntb.mjs.map +0 -1
- package/dist/search-_oHfguA5.mjs.map +0 -1
- package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
- package/dist/tools-DV_lsiCc.mjs.map +0 -1
- package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
- package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
- package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
- package/templates/README.md +0 -181
- package/templates/skills/createskill-skill.template.md +0 -78
- package/templates/skills/history-system.template.md +0 -371
- package/templates/skills/hook-system.template.md +0 -913
- package/templates/skills/sessions-skill.template.md +0 -102
- package/templates/skills/skill-system.template.md +0 -214
- package/templates/skills/terminal-tabs.template.md +0 -120
- package/templates/templates.md +0 -20
|
@@ -1,1000 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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";
|