@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
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session note creation, editing, checkpointing, renaming, and finalization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
|
|
6
|
+
import { join, basename } from 'path';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Internal helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */
|
|
13
|
+
function getMonthDir(notesDir: string): string {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const year = String(now.getFullYear());
|
|
16
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
17
|
+
const monthDir = join(notesDir, year, month);
|
|
18
|
+
if (!existsSync(monthDir)) {
|
|
19
|
+
mkdirSync(monthDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
return monthDir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Public API
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the next note number (4-digit format: 0001, 0002, etc.).
|
|
30
|
+
* Numbers are scoped per YYYY/MM directory.
|
|
31
|
+
*/
|
|
32
|
+
export function getNextNoteNumber(notesDir: string): string {
|
|
33
|
+
const monthDir = getMonthDir(notesDir);
|
|
34
|
+
|
|
35
|
+
const files = readdirSync(monthDir)
|
|
36
|
+
.filter(f => f.match(/^\d{3,4}[\s_-]/))
|
|
37
|
+
.filter(f => f.endsWith('.md'))
|
|
38
|
+
.sort();
|
|
39
|
+
|
|
40
|
+
if (files.length === 0) return '0001';
|
|
41
|
+
|
|
42
|
+
let maxNumber = 0;
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const digitMatch = file.match(/^(\d+)/);
|
|
45
|
+
if (digitMatch) {
|
|
46
|
+
const num = parseInt(digitMatch[1], 10);
|
|
47
|
+
if (num > maxNumber) maxNumber = num;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return String(maxNumber + 1).padStart(4, '0');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the current (latest) note file path, or null if none exists.
|
|
56
|
+
* Searches current month → previous month → flat notesDir (legacy).
|
|
57
|
+
*/
|
|
58
|
+
export function getCurrentNotePath(notesDir: string): string | null {
|
|
59
|
+
if (!existsSync(notesDir)) return null;
|
|
60
|
+
|
|
61
|
+
const findLatestIn = (dir: string): string | null => {
|
|
62
|
+
if (!existsSync(dir)) return null;
|
|
63
|
+
const files = readdirSync(dir)
|
|
64
|
+
.filter(f => f.match(/^\d{3,4}[\s_-].*\.md$/))
|
|
65
|
+
.sort((a, b) => {
|
|
66
|
+
const numA = parseInt(a.match(/^(\d+)/)?.[1] || '0', 10);
|
|
67
|
+
const numB = parseInt(b.match(/^(\d+)/)?.[1] || '0', 10);
|
|
68
|
+
return numA - numB;
|
|
69
|
+
});
|
|
70
|
+
if (files.length === 0) return null;
|
|
71
|
+
return join(dir, files[files.length - 1]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const now = new Date();
|
|
75
|
+
const year = String(now.getFullYear());
|
|
76
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
77
|
+
const currentMonthDir = join(notesDir, year, month);
|
|
78
|
+
const found = findLatestIn(currentMonthDir);
|
|
79
|
+
if (found) return found;
|
|
80
|
+
|
|
81
|
+
const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
82
|
+
const prevYear = String(prevDate.getFullYear());
|
|
83
|
+
const prevMonth = String(prevDate.getMonth() + 1).padStart(2, '0');
|
|
84
|
+
const prevMonthDir = join(notesDir, prevYear, prevMonth);
|
|
85
|
+
const prevFound = findLatestIn(prevMonthDir);
|
|
86
|
+
if (prevFound) return prevFound;
|
|
87
|
+
|
|
88
|
+
return findLatestIn(notesDir);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a new session note.
|
|
93
|
+
* Format: "NNNN - YYYY-MM-DD - New Session.md" filed into YYYY/MM subdirectory.
|
|
94
|
+
* Claude MUST rename at session end with a meaningful description.
|
|
95
|
+
*/
|
|
96
|
+
export function createSessionNote(notesDir: string, description: string): string {
|
|
97
|
+
const noteNumber = getNextNoteNumber(notesDir);
|
|
98
|
+
const date = new Date().toISOString().split('T')[0];
|
|
99
|
+
const monthDir = getMonthDir(notesDir);
|
|
100
|
+
const filename = `${noteNumber} - ${date} - New Session.md`;
|
|
101
|
+
const filepath = join(monthDir, filename);
|
|
102
|
+
|
|
103
|
+
const content = `# Session ${noteNumber}: ${description}
|
|
104
|
+
|
|
105
|
+
**Date:** ${date}
|
|
106
|
+
**Status:** In Progress
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Work Done
|
|
111
|
+
|
|
112
|
+
<!-- PAI will add completed work here during session -->
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Next Steps
|
|
117
|
+
|
|
118
|
+
<!-- To be filled at session end -->
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
**Tags:** #Session
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
writeFileSync(filepath, content);
|
|
126
|
+
console.error(`Created session note: ${filename}`);
|
|
127
|
+
|
|
128
|
+
return filepath;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Append a checkpoint to the current session note. */
|
|
132
|
+
export function appendCheckpoint(notePath: string, checkpoint: string): void {
|
|
133
|
+
if (!existsSync(notePath)) {
|
|
134
|
+
console.error(`Note file not found, recreating: ${notePath}`);
|
|
135
|
+
try {
|
|
136
|
+
const parentDir = join(notePath, '..');
|
|
137
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
138
|
+
const noteFilename = basename(notePath);
|
|
139
|
+
const numberMatch = noteFilename.match(/^(\d+)/);
|
|
140
|
+
const noteNumber = numberMatch ? numberMatch[1] : '0000';
|
|
141
|
+
const date = new Date().toISOString().split('T')[0];
|
|
142
|
+
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`;
|
|
143
|
+
writeFileSync(notePath, content);
|
|
144
|
+
console.error(`Recreated session note: ${noteFilename}`);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`Failed to recreate note: ${err}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const content = readFileSync(notePath, 'utf-8');
|
|
152
|
+
const timestamp = new Date().toISOString();
|
|
153
|
+
const checkpointText = `\n### Checkpoint ${timestamp}\n\n${checkpoint}\n`;
|
|
154
|
+
|
|
155
|
+
const nextStepsIndex = content.indexOf('## Next Steps');
|
|
156
|
+
const newContent = nextStepsIndex !== -1
|
|
157
|
+
? content.substring(0, nextStepsIndex) + checkpointText + content.substring(nextStepsIndex)
|
|
158
|
+
: content + checkpointText;
|
|
159
|
+
|
|
160
|
+
writeFileSync(notePath, newContent);
|
|
161
|
+
console.error(`Checkpoint added to: ${basename(notePath)}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Work item for session notes. */
|
|
165
|
+
export interface WorkItem {
|
|
166
|
+
title: string;
|
|
167
|
+
details?: string[];
|
|
168
|
+
completed?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Add work items to the "Work Done" section of a session note. */
|
|
172
|
+
export function addWorkToSessionNote(notePath: string, workItems: WorkItem[], sectionTitle?: string): void {
|
|
173
|
+
if (!existsSync(notePath)) {
|
|
174
|
+
console.error(`Note file not found: ${notePath}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let content = readFileSync(notePath, 'utf-8');
|
|
179
|
+
|
|
180
|
+
let workText = '';
|
|
181
|
+
if (sectionTitle) workText += `\n### ${sectionTitle}\n\n`;
|
|
182
|
+
|
|
183
|
+
for (const item of workItems) {
|
|
184
|
+
const checkbox = item.completed !== false ? '[x]' : '[ ]';
|
|
185
|
+
workText += `- ${checkbox} **${item.title}**\n`;
|
|
186
|
+
if (item.details && item.details.length > 0) {
|
|
187
|
+
for (const detail of item.details) {
|
|
188
|
+
workText += ` - ${detail}\n`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const workDoneMatch = content.match(/## Work Done\n\n(<!-- .*? -->)?/);
|
|
194
|
+
if (workDoneMatch) {
|
|
195
|
+
const insertPoint = content.indexOf(workDoneMatch[0]) + workDoneMatch[0].length;
|
|
196
|
+
content = content.substring(0, insertPoint) + workText + content.substring(insertPoint);
|
|
197
|
+
} else {
|
|
198
|
+
const nextStepsIndex = content.indexOf('## Next Steps');
|
|
199
|
+
if (nextStepsIndex !== -1) {
|
|
200
|
+
content = content.substring(0, nextStepsIndex) + workText + '\n' + content.substring(nextStepsIndex);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
writeFileSync(notePath, content);
|
|
205
|
+
console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Sanitize a string for use in a filename. */
|
|
209
|
+
export function sanitizeForFilename(str: string): string {
|
|
210
|
+
return str
|
|
211
|
+
.toLowerCase()
|
|
212
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
213
|
+
.replace(/\s+/g, '-')
|
|
214
|
+
.replace(/-+/g, '-')
|
|
215
|
+
.replace(/^-|-$/g, '')
|
|
216
|
+
.substring(0, 50);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extract a meaningful name from session note content and summary.
|
|
221
|
+
* Looks at Work Done section headers, bold text, and summary.
|
|
222
|
+
*/
|
|
223
|
+
export function extractMeaningfulName(noteContent: string, summary: string): string {
|
|
224
|
+
const workDoneMatch = noteContent.match(/## Work Done\n\n([\s\S]*?)(?=\n---|\n## Next)/);
|
|
225
|
+
|
|
226
|
+
if (workDoneMatch) {
|
|
227
|
+
const workDoneSection = workDoneMatch[1];
|
|
228
|
+
|
|
229
|
+
const subheadings = workDoneSection.match(/### ([^\n]+)/g);
|
|
230
|
+
if (subheadings && subheadings.length > 0) {
|
|
231
|
+
const firstHeading = subheadings[0].replace('### ', '').trim();
|
|
232
|
+
if (firstHeading.length > 5 && firstHeading.length < 60) {
|
|
233
|
+
return sanitizeForFilename(firstHeading);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const boldMatches = workDoneSection.match(/\*\*([^*]+)\*\*/g);
|
|
238
|
+
if (boldMatches && boldMatches.length > 0) {
|
|
239
|
+
const firstBold = boldMatches[0].replace(/\*\*/g, '').trim();
|
|
240
|
+
if (firstBold.length > 3 && firstBold.length < 50) {
|
|
241
|
+
return sanitizeForFilename(firstBold);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const numberedItems = workDoneSection.match(/^\d+\.\s+\*\*([^*]+)\*\*/m);
|
|
246
|
+
if (numberedItems) return sanitizeForFilename(numberedItems[1]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (summary && summary.length > 5 && summary !== 'Session completed.') {
|
|
250
|
+
const cleanSummary = summary
|
|
251
|
+
.replace(/[^\w\s-]/g, ' ')
|
|
252
|
+
.trim()
|
|
253
|
+
.split(/\s+/)
|
|
254
|
+
.slice(0, 5)
|
|
255
|
+
.join(' ');
|
|
256
|
+
if (cleanSummary.length > 3) return sanitizeForFilename(cleanSummary);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Rename a session note with a meaningful name.
|
|
264
|
+
* Always uses "NNNN - YYYY-MM-DD - Description.md" format.
|
|
265
|
+
* Returns the new path, or original path if rename fails.
|
|
266
|
+
*/
|
|
267
|
+
export function renameSessionNote(notePath: string, meaningfulName: string): string {
|
|
268
|
+
if (!meaningfulName || !existsSync(notePath)) return notePath;
|
|
269
|
+
|
|
270
|
+
const dir = join(notePath, '..');
|
|
271
|
+
const oldFilename = basename(notePath);
|
|
272
|
+
|
|
273
|
+
const correctMatch = oldFilename.match(/^(\d{3,4}) - (\d{4}-\d{2}-\d{2}) - .*\.md$/);
|
|
274
|
+
const legacyMatch = oldFilename.match(/^(\d{3,4})_(\d{4}-\d{2}-\d{2})_.*\.md$/);
|
|
275
|
+
const match = correctMatch || legacyMatch;
|
|
276
|
+
if (!match) return notePath;
|
|
277
|
+
|
|
278
|
+
const [, noteNumber, date] = match;
|
|
279
|
+
|
|
280
|
+
const titleCaseName = meaningfulName
|
|
281
|
+
.split(/[\s_-]+/)
|
|
282
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
283
|
+
.join(' ')
|
|
284
|
+
.trim();
|
|
285
|
+
|
|
286
|
+
const paddedNumber = noteNumber.padStart(4, '0');
|
|
287
|
+
const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
|
|
288
|
+
const newPath = join(dir, newFilename);
|
|
289
|
+
|
|
290
|
+
if (newFilename === oldFilename) return notePath;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
renameSync(notePath, newPath);
|
|
294
|
+
console.error(`Renamed note: ${oldFilename} → ${newFilename}`);
|
|
295
|
+
return newPath;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(`Could not rename note: ${error}`);
|
|
298
|
+
return notePath;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Update the session note's H1 title and rename the file. */
|
|
303
|
+
export function updateSessionNoteTitle(notePath: string, newTitle: string): void {
|
|
304
|
+
if (!existsSync(notePath)) {
|
|
305
|
+
console.error(`Note file not found: ${notePath}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let content = readFileSync(notePath, 'utf-8');
|
|
310
|
+
content = content.replace(/^# Session \d+:.*$/m, (match) => {
|
|
311
|
+
const sessionNum = match.match(/Session (\d+)/)?.[1] || '';
|
|
312
|
+
return `# Session ${sessionNum}: ${newTitle}`;
|
|
313
|
+
});
|
|
314
|
+
writeFileSync(notePath, content);
|
|
315
|
+
renameSessionNote(notePath, sanitizeForFilename(newTitle));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Finalize session note — mark as complete, add summary, rename with meaningful name.
|
|
320
|
+
* IDEMPOTENT: subsequent calls are no-ops if already finalized.
|
|
321
|
+
* Returns the final path (may be renamed).
|
|
322
|
+
*/
|
|
323
|
+
export function finalizeSessionNote(notePath: string, summary: string): string {
|
|
324
|
+
if (!existsSync(notePath)) {
|
|
325
|
+
console.error(`Note file not found: ${notePath}`);
|
|
326
|
+
return notePath;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let content = readFileSync(notePath, 'utf-8');
|
|
330
|
+
|
|
331
|
+
if (content.includes('**Status:** Completed')) {
|
|
332
|
+
console.error(`Note already finalized: ${basename(notePath)}`);
|
|
333
|
+
return notePath;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
content = content.replace('**Status:** In Progress', '**Status:** Completed');
|
|
337
|
+
|
|
338
|
+
if (!content.includes('**Completed:**')) {
|
|
339
|
+
const completionTime = new Date().toISOString();
|
|
340
|
+
content = content.replace(
|
|
341
|
+
'---\n\n## Work Done',
|
|
342
|
+
`**Completed:** ${completionTime}\n\n---\n\n## Work Done`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const nextStepsMatch = content.match(/## Next Steps\n\n(<!-- .*? -->)/);
|
|
347
|
+
if (nextStepsMatch) {
|
|
348
|
+
content = content.replace(
|
|
349
|
+
nextStepsMatch[0],
|
|
350
|
+
`## Next Steps\n\n${summary || 'Session completed.'}`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
writeFileSync(notePath, content);
|
|
355
|
+
console.error(`Session note finalized: ${basename(notePath)}`);
|
|
356
|
+
|
|
357
|
+
const meaningfulName = extractMeaningfulName(content, summary);
|
|
358
|
+
if (meaningfulName) {
|
|
359
|
+
return renameSessionNote(notePath, meaningfulName);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return notePath;
|
|
363
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODO.md management — creation, task updates, checkpoints, and Continue section.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { findTodoPath } from './paths.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Task item for TODO.md. */
|
|
14
|
+
export interface TodoItem {
|
|
15
|
+
content: string;
|
|
16
|
+
completed: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure TODO.md exists. Creates it with default structure if missing.
|
|
25
|
+
* Returns the path to the TODO.md file.
|
|
26
|
+
*/
|
|
27
|
+
export function ensureTodoMd(cwd: string): string {
|
|
28
|
+
const todoPath = findTodoPath(cwd);
|
|
29
|
+
|
|
30
|
+
if (!existsSync(todoPath)) {
|
|
31
|
+
const parentDir = join(todoPath, '..');
|
|
32
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
const content = `# TODO
|
|
35
|
+
|
|
36
|
+
## Current Session
|
|
37
|
+
|
|
38
|
+
- [ ] (Tasks will be tracked here)
|
|
39
|
+
|
|
40
|
+
## Backlog
|
|
41
|
+
|
|
42
|
+
- [ ] (Future tasks)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
*Last updated: ${new Date().toISOString()}*
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
writeFileSync(todoPath, content);
|
|
50
|
+
console.error(`Created TODO.md: ${todoPath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return todoPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Public API
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update TODO.md with current session tasks.
|
|
62
|
+
* Preserves the Backlog section and ensures exactly ONE timestamp at the end.
|
|
63
|
+
*/
|
|
64
|
+
export function updateTodoMd(cwd: string, tasks: TodoItem[], sessionSummary?: string): void {
|
|
65
|
+
const todoPath = ensureTodoMd(cwd);
|
|
66
|
+
const content = readFileSync(todoPath, 'utf-8');
|
|
67
|
+
|
|
68
|
+
const backlogMatch = content.match(/## Backlog[\s\S]*?(?=\n---|\n\*Last updated|$)/);
|
|
69
|
+
const backlogSection = backlogMatch
|
|
70
|
+
? backlogMatch[0].trim()
|
|
71
|
+
: '## Backlog\n\n- [ ] (Future tasks)';
|
|
72
|
+
|
|
73
|
+
const taskLines = tasks.length > 0
|
|
74
|
+
? tasks.map(t => `- [${t.completed ? 'x' : ' '}] ${t.content}`).join('\n')
|
|
75
|
+
: '- [ ] (No active tasks)';
|
|
76
|
+
|
|
77
|
+
const newContent = `# TODO
|
|
78
|
+
|
|
79
|
+
## Current Session
|
|
80
|
+
|
|
81
|
+
${taskLines}
|
|
82
|
+
|
|
83
|
+
${sessionSummary ? `**Session Summary:** ${sessionSummary}\n\n` : ''}${backlogSection}
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
*Last updated: ${new Date().toISOString()}*
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
writeFileSync(todoPath, newContent);
|
|
91
|
+
console.error(`Updated TODO.md: ${todoPath}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Add a checkpoint entry to TODO.md (without replacing tasks).
|
|
96
|
+
* Ensures exactly ONE timestamp line at the end.
|
|
97
|
+
*/
|
|
98
|
+
export function addTodoCheckpoint(cwd: string, checkpoint: string): void {
|
|
99
|
+
const todoPath = ensureTodoMd(cwd);
|
|
100
|
+
let content = readFileSync(todoPath, 'utf-8');
|
|
101
|
+
|
|
102
|
+
// Remove ALL existing timestamp lines and trailing separators
|
|
103
|
+
content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, '');
|
|
104
|
+
|
|
105
|
+
const checkpointText = `\n**Checkpoint (${new Date().toISOString()}):** ${checkpoint}\n\n`;
|
|
106
|
+
|
|
107
|
+
const backlogIndex = content.indexOf('## Backlog');
|
|
108
|
+
if (backlogIndex !== -1) {
|
|
109
|
+
content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
|
|
110
|
+
} else {
|
|
111
|
+
const continueIndex = content.indexOf('## Continue');
|
|
112
|
+
if (continueIndex !== -1) {
|
|
113
|
+
const afterContinue = content.indexOf('\n---', continueIndex);
|
|
114
|
+
if (afterContinue !== -1) {
|
|
115
|
+
const insertAt = afterContinue + 4;
|
|
116
|
+
content = content.substring(0, insertAt) + '\n' + checkpointText + content.substring(insertAt);
|
|
117
|
+
} else {
|
|
118
|
+
content = content.trimEnd() + '\n' + checkpointText;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
content = content.trimEnd() + '\n' + checkpointText;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
content = content.trimEnd() + `\n\n---\n\n*Last updated: ${new Date().toISOString()}*\n`;
|
|
126
|
+
|
|
127
|
+
writeFileSync(todoPath, content);
|
|
128
|
+
console.error(`Checkpoint added to TODO.md`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Update the ## Continue section at the top of TODO.md.
|
|
133
|
+
* Mirrors "pause session" behavior — gives the next session a starting point.
|
|
134
|
+
* Replaces any existing ## Continue section.
|
|
135
|
+
*/
|
|
136
|
+
export function updateTodoContinue(
|
|
137
|
+
cwd: string,
|
|
138
|
+
noteFilename: string,
|
|
139
|
+
state: string | null,
|
|
140
|
+
tokenDisplay: string
|
|
141
|
+
): void {
|
|
142
|
+
const todoPath = ensureTodoMd(cwd);
|
|
143
|
+
let content = readFileSync(todoPath, 'utf-8');
|
|
144
|
+
|
|
145
|
+
// Remove existing ## Continue section
|
|
146
|
+
content = content.replace(/## Continue\n[\s\S]*?\n---\n+/, '');
|
|
147
|
+
|
|
148
|
+
const now = new Date().toISOString();
|
|
149
|
+
const stateLines = state
|
|
150
|
+
? state.split('\n').filter(l => l.trim()).slice(0, 10).map(l => `> ${l}`).join('\n')
|
|
151
|
+
: `> Working directory: ${cwd}. Check the latest session note for details.`;
|
|
152
|
+
|
|
153
|
+
const continueSection = `## Continue
|
|
154
|
+
|
|
155
|
+
> **Last session:** ${noteFilename.replace('.md', '')}
|
|
156
|
+
> **Paused at:** ${now}
|
|
157
|
+
>
|
|
158
|
+
${stateLines}
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
content = content.replace(/^\s+/, '');
|
|
165
|
+
|
|
166
|
+
const titleMatch = content.match(/^(# [^\n]+\n+)/);
|
|
167
|
+
if (titleMatch) {
|
|
168
|
+
content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);
|
|
169
|
+
} else {
|
|
170
|
+
content = continueSection + content;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, '');
|
|
174
|
+
content = content.trimEnd() + `\n\n---\n\n*Last updated: ${now}*\n`;
|
|
175
|
+
|
|
176
|
+
writeFileSync(todoPath, content);
|
|
177
|
+
console.error('TODO.md ## Continue section updated');
|
|
178
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session token counting from .jsonl transcript files.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate total tokens from a session .jsonl file.
|
|
9
|
+
* Sums input, output, cache_creation, and cache_read tokens.
|
|
10
|
+
*/
|
|
11
|
+
export function calculateSessionTokens(jsonlPath: string): number {
|
|
12
|
+
if (!existsSync(jsonlPath)) return 0;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const content = readFileSync(jsonlPath, 'utf-8');
|
|
16
|
+
const lines = content.trim().split('\n');
|
|
17
|
+
let totalTokens = 0;
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
try {
|
|
21
|
+
const entry = JSON.parse(line);
|
|
22
|
+
if (entry.message?.usage) {
|
|
23
|
+
const usage = entry.message.usage;
|
|
24
|
+
totalTokens += (usage.input_tokens || 0);
|
|
25
|
+
totalTokens += (usage.output_tokens || 0);
|
|
26
|
+
totalTokens += (usage.cache_creation_input_tokens || 0);
|
|
27
|
+
totalTokens += (usage.cache_read_input_tokens || 0);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Skip invalid JSON lines
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return totalTokens;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error calculating tokens: ${error}`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
}
|