@aiplumber/session-recall 1.5.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/package.json +29 -0
- package/session-recall +1351 -0
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aiplumber/session-recall",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Pull context from previous Claude Code sessions. Sessions end, context resets - this tool lets you continue where you left off.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"session-recall": "./session-recall"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"session-recall",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"session",
|
|
16
|
+
"context",
|
|
17
|
+
"recall",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
"author": "Hung Nguyen",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/pnwtech/session-recall.git"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=16.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/session-recall
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Filter patterns
|
|
7
|
+
const SHELL_PREFIXES = ['ls ', 'cat ', 'npm ', 'cd ', 'git ', 'mkdir ', 'rm ', 'cp ', 'mv ', 'grep ', 'find '];
|
|
8
|
+
const NOISE_TYPES = ['progress', 'file-history-snapshot', 'system', 'queue-operation'];
|
|
9
|
+
const MAX_USER_MSG_LENGTH = 200;
|
|
10
|
+
const ACTIVE_SESSION_THRESHOLD_MS = 60000; // Sessions with activity in last 60s are "active"
|
|
11
|
+
|
|
12
|
+
// Token estimation (~4 chars per token for English, conservative)
|
|
13
|
+
function estimateTokens(text) {
|
|
14
|
+
if (!text) return 0;
|
|
15
|
+
return Math.ceil(text.length / 4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Marker detection patterns
|
|
19
|
+
const MARKERS = {
|
|
20
|
+
aha_moment: [/oh!/i, /YES/, /now I see/i, /did we just discover/i, /that's it/i],
|
|
21
|
+
pushback: [/^no$/i, /^wait$/i, /for real\?/i, /nooo+/i, /^but /i, /hold on/i],
|
|
22
|
+
strong_opinion: [/\bmust\b/i, /\bnever\b/i, /\balways\b/i, /\bneed to\b/i],
|
|
23
|
+
clarification: [/you mean/i, /so basically/i, /in other words/i],
|
|
24
|
+
decision: /^[A-D](,\s*[A-D])*$/i
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function readJsonl(filePath) {
|
|
28
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
+
return content.trim().split('\n').map((line) => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(line);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}).filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isToolResult(msg) {
|
|
39
|
+
if (!msg.message?.content) return false;
|
|
40
|
+
const content = msg.message.content;
|
|
41
|
+
if (Array.isArray(content)) {
|
|
42
|
+
return content.some(c => c.type === 'tool_result');
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isShellCommand(text) {
|
|
48
|
+
if (typeof text !== 'string') return false;
|
|
49
|
+
return SHELL_PREFIXES.some(prefix => text.startsWith(prefix));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getAssistantText(msg) {
|
|
53
|
+
if (!msg.message?.content) return null;
|
|
54
|
+
const content = msg.message.content;
|
|
55
|
+
if (!Array.isArray(content)) return null;
|
|
56
|
+
|
|
57
|
+
const textBlocks = content.filter(c => c.type === 'text');
|
|
58
|
+
if (textBlocks.length === 0) return null;
|
|
59
|
+
return textBlocks.map(t => t.text).join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getUserText(msg) {
|
|
63
|
+
if (!msg.message?.content) return null;
|
|
64
|
+
const content = msg.message.content;
|
|
65
|
+
if (typeof content === 'string') return content;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function detectMarker(text) {
|
|
70
|
+
if (!text) return null;
|
|
71
|
+
|
|
72
|
+
for (const [marker, patterns] of Object.entries(MARKERS)) {
|
|
73
|
+
if (Array.isArray(patterns)) {
|
|
74
|
+
if (patterns.some(p => p.test(text))) return marker;
|
|
75
|
+
} else if (patterns.test(text)) {
|
|
76
|
+
return marker;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isDiscourse(msg) {
|
|
83
|
+
if (NOISE_TYPES.includes(msg.type)) return false;
|
|
84
|
+
|
|
85
|
+
if (msg.type === 'user') {
|
|
86
|
+
if (isToolResult(msg)) return false;
|
|
87
|
+
const text = getUserText(msg);
|
|
88
|
+
if (!text) return false;
|
|
89
|
+
if (text.length > MAX_USER_MSG_LENGTH) return false;
|
|
90
|
+
if (isShellCommand(text)) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (msg.type === 'assistant') {
|
|
95
|
+
const text = getAssistantText(msg);
|
|
96
|
+
return text !== null && text.length > 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildToolResultMeta(messages) {
|
|
103
|
+
const meta = {};
|
|
104
|
+
const toolUseInfo = {};
|
|
105
|
+
|
|
106
|
+
// First pass: collect tool_use timestamps, model, agent info, and Task details
|
|
107
|
+
for (const msg of messages) {
|
|
108
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
109
|
+
const model = msg.message?.model || null;
|
|
110
|
+
const agentId = msg.isSidechain ? msg.agentId : null;
|
|
111
|
+
for (const block of msg.message.content) {
|
|
112
|
+
if (block.type === 'tool_use' && block.id) {
|
|
113
|
+
const info = {
|
|
114
|
+
timestamp: msg.timestamp ? new Date(msg.timestamp).getTime() : null,
|
|
115
|
+
model: model,
|
|
116
|
+
agentId: agentId
|
|
117
|
+
};
|
|
118
|
+
// Capture Task tool details
|
|
119
|
+
if (block.name === 'Task' && block.input) {
|
|
120
|
+
info.taskDescription = block.input.description || null;
|
|
121
|
+
info.taskPrompt = block.input.prompt || null;
|
|
122
|
+
}
|
|
123
|
+
toolUseInfo[block.id] = info;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Second pass: collect tool_result sizes and calculate duration
|
|
130
|
+
for (const msg of messages) {
|
|
131
|
+
if (msg.type === 'user' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
132
|
+
for (const block of msg.message.content) {
|
|
133
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
134
|
+
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
135
|
+
const resultTs = msg.timestamp ? new Date(msg.timestamp).getTime() : null;
|
|
136
|
+
const info = toolUseInfo[block.tool_use_id] || {};
|
|
137
|
+
const durationMs = (resultTs && info.timestamp) ? resultTs - info.timestamp : null;
|
|
138
|
+
|
|
139
|
+
const metaEntry = {
|
|
140
|
+
size: content?.length || 0,
|
|
141
|
+
durationMs: durationMs,
|
|
142
|
+
model: info.model,
|
|
143
|
+
agentId: info.agentId,
|
|
144
|
+
taskDescription: info.taskDescription,
|
|
145
|
+
taskPrompt: info.taskPrompt
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Parse Task tool result for agentId and output_file
|
|
149
|
+
if (info.taskDescription && content) {
|
|
150
|
+
const agentMatch = content.match(/agentId:\s*(\w+)/);
|
|
151
|
+
const outputMatch = content.match(/output_file:\s*([^\n]+)/);
|
|
152
|
+
if (agentMatch) metaEntry.taskAgentId = agentMatch[1];
|
|
153
|
+
if (outputMatch) metaEntry.taskOutputFile = outputMatch[1].trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
meta[block.tool_use_id] = metaEntry;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return meta;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatSize(bytes) {
|
|
165
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
166
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
167
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatDuration(ms) {
|
|
171
|
+
if (ms === null || ms === undefined) return '';
|
|
172
|
+
if (ms < 1000) return `${ms}ms`;
|
|
173
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function shortModelName(model) {
|
|
177
|
+
if (!model) return '';
|
|
178
|
+
if (model.includes('opus')) return 'opus';
|
|
179
|
+
if (model.includes('sonnet')) return 'sonnet';
|
|
180
|
+
if (model.includes('haiku')) return 'haiku';
|
|
181
|
+
if (model.includes('gemini')) return 'gemini';
|
|
182
|
+
return model.substring(0, 10);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function collapseToolCall(msg, resultMeta = {}) {
|
|
186
|
+
if (msg.type !== 'assistant') return null;
|
|
187
|
+
if (!msg.message?.content || !Array.isArray(msg.message.content)) return null;
|
|
188
|
+
|
|
189
|
+
const toolUses = msg.message.content.filter(c => c.type === 'tool_use');
|
|
190
|
+
if (toolUses.length === 0) return null;
|
|
191
|
+
|
|
192
|
+
return toolUses.map(t => {
|
|
193
|
+
const name = t.name;
|
|
194
|
+
const input = t.input || {};
|
|
195
|
+
const shortId = t.id ? t.id.substring(5, 17) : '';
|
|
196
|
+
const meta = resultMeta[t.id] || {};
|
|
197
|
+
const size = meta.size !== undefined ? formatSize(meta.size) : '';
|
|
198
|
+
const duration = meta.durationMs ? formatDuration(meta.durationMs) : '';
|
|
199
|
+
const model = meta.model ? shortModelName(meta.model) : '';
|
|
200
|
+
const agent = meta.agentId ? `@${meta.agentId}` : '';
|
|
201
|
+
const suffix = size || duration || model || agent ? ` (${[model, size, duration, agent].filter(Boolean).join(', ')})` : '';
|
|
202
|
+
|
|
203
|
+
if (name === 'Task') {
|
|
204
|
+
const desc = meta.taskDescription || input.description || 'task';
|
|
205
|
+
const taskAgent = meta.taskAgentId ? `@${meta.taskAgentId}` : '';
|
|
206
|
+
const taskOutput = meta.taskOutputFile || '';
|
|
207
|
+
const taskInfo = [taskAgent, taskOutput].filter(Boolean).join(' → ');
|
|
208
|
+
const taskSuffix = [model, size, duration, taskInfo].filter(Boolean).join(', ');
|
|
209
|
+
return `[${shortId} Task: ${desc}] (${taskSuffix})`;
|
|
210
|
+
} else if (name === 'Read') {
|
|
211
|
+
return `[${shortId} Read: ${input.file_path}]${suffix}`;
|
|
212
|
+
} else if (name === 'Bash') {
|
|
213
|
+
const cmd = input.command?.substring(0, 60) || 'command';
|
|
214
|
+
return `[${shortId} Ran: ${cmd}${input.command?.length > 60 ? '...' : ''}]${suffix}`;
|
|
215
|
+
} else if (name === 'Write') {
|
|
216
|
+
return `[${shortId} Write: ${input.file_path}]${suffix}`;
|
|
217
|
+
} else if (name === 'Edit') {
|
|
218
|
+
return `[${shortId} Edit: ${input.file_path}]${suffix}`;
|
|
219
|
+
} else if (name === 'Glob') {
|
|
220
|
+
return `[${shortId} Glob: ${input.pattern}]${suffix}`;
|
|
221
|
+
} else if (name === 'Grep') {
|
|
222
|
+
return `[${shortId} Grep: ${input.pattern}]${suffix}`;
|
|
223
|
+
} else {
|
|
224
|
+
return `[${shortId} ${name}]${suffix}`;
|
|
225
|
+
}
|
|
226
|
+
}).join(' ');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ========== COMMANDS ==========
|
|
230
|
+
|
|
231
|
+
function cmdParse(jsonlPath, opts) {
|
|
232
|
+
const messages = readJsonl(jsonlPath);
|
|
233
|
+
const discourse = messages.filter(isDiscourse);
|
|
234
|
+
|
|
235
|
+
// Calculate totals for dry-run
|
|
236
|
+
let totalChars = 0;
|
|
237
|
+
const contents = discourse.map(msg => {
|
|
238
|
+
const text = msg.type === 'user' ? getUserText(msg) : getAssistantText(msg);
|
|
239
|
+
totalChars += (text?.length || 0);
|
|
240
|
+
return { msg, text };
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (opts.dryRun) {
|
|
244
|
+
const userCount = discourse.filter(m => m.type === 'user').length;
|
|
245
|
+
const assistantCount = discourse.filter(m => m.type === 'assistant').length;
|
|
246
|
+
console.log(`--- DRY RUN: parse ---`);
|
|
247
|
+
console.log(`Messages: ${discourse.length} (${userCount} user, ${assistantCount} assistant)`);
|
|
248
|
+
console.log(`Characters: ${totalChars.toLocaleString()}`);
|
|
249
|
+
console.log(`Estimated tokens: ~${estimateTokens({length: totalChars}).toLocaleString()}`);
|
|
250
|
+
console.log(`Filtered out: ${messages.length - discourse.length} noise messages`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (opts.format === 'text') {
|
|
255
|
+
contents.forEach(({ msg, text }) => {
|
|
256
|
+
const role = msg.type === 'user' ? 'USER' : 'ASSISTANT';
|
|
257
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
258
|
+
console.log(`[${ts}] ${role}: ${text?.substring(0, 150)}${text?.length > 150 ? '...' : ''}`);
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
contents.forEach(({ msg, text }) => {
|
|
262
|
+
const output = {
|
|
263
|
+
type: msg.type,
|
|
264
|
+
timestamp: msg.timestamp,
|
|
265
|
+
content: text
|
|
266
|
+
};
|
|
267
|
+
console.log(JSON.stringify(output));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (opts.stats) {
|
|
272
|
+
const userCount = discourse.filter(m => m.type === 'user').length;
|
|
273
|
+
const assistantCount = discourse.filter(m => m.type === 'assistant').length;
|
|
274
|
+
console.error(`\n--- Stats ---`);
|
|
275
|
+
console.error(`Total discourse: ${discourse.length} (${userCount} user, ${assistantCount} assistant)`);
|
|
276
|
+
console.error(`Filtered out: ${messages.length - discourse.length} noise messages`);
|
|
277
|
+
console.error(`Estimated tokens: ~${estimateTokens({length: totalChars}).toLocaleString()}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function cmdHot(jsonlPath, opts) {
|
|
282
|
+
const messages = readJsonl(jsonlPath);
|
|
283
|
+
const discourse = messages.filter(isDiscourse);
|
|
284
|
+
const lastN = parseInt(opts.last) || 30;
|
|
285
|
+
|
|
286
|
+
const hot = discourse.slice(-lastN);
|
|
287
|
+
|
|
288
|
+
const exchanges = hot.map(msg => {
|
|
289
|
+
const text = msg.type === 'user' ? getUserText(msg) : getAssistantText(msg);
|
|
290
|
+
return {
|
|
291
|
+
ts: msg.timestamp,
|
|
292
|
+
role: msg.type === 'user' ? 'user' : 'assistant',
|
|
293
|
+
content: text,
|
|
294
|
+
marker: detectMarker(text)
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const totalChars = exchanges.reduce((sum, e) => sum + (e.content?.length || 0), 0);
|
|
299
|
+
|
|
300
|
+
if (opts.dryRun) {
|
|
301
|
+
const userCount = exchanges.filter(e => e.role === 'user').length;
|
|
302
|
+
const assistantCount = exchanges.filter(e => e.role === 'assistant').length;
|
|
303
|
+
const markedCount = exchanges.filter(e => e.marker).length;
|
|
304
|
+
console.log(`--- DRY RUN: hot --last ${lastN} ---`);
|
|
305
|
+
console.log(`Exchanges: ${exchanges.length} (${userCount} user, ${assistantCount} assistant)`);
|
|
306
|
+
console.log(`Marked moments: ${markedCount}`);
|
|
307
|
+
console.log(`Characters: ${totalChars.toLocaleString()}`);
|
|
308
|
+
console.log(`Estimated tokens: ~${estimateTokens({length: totalChars}).toLocaleString()}`);
|
|
309
|
+
console.log(`Available discourse: ${discourse.length} total`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const insights = exchanges.filter(e => e.marker === 'aha_moment' || e.marker === 'strong_opinion');
|
|
314
|
+
const lastInsight = insights.length > 0 ? insights[insights.length - 1].content : null;
|
|
315
|
+
|
|
316
|
+
const output = {
|
|
317
|
+
source: path.resolve(jsonlPath),
|
|
318
|
+
extracted: new Date().toISOString(),
|
|
319
|
+
message_count: exchanges.length,
|
|
320
|
+
thread: 'extracted from ' + path.basename(jsonlPath),
|
|
321
|
+
last_insight: lastInsight?.substring(0, 200) || 'none detected',
|
|
322
|
+
momentum: exchanges.length > 20 ? 'high' : exchanges.length > 10 ? 'medium' : 'low',
|
|
323
|
+
exchanges
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
console.log(JSON.stringify(output, null, 2));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getToolResultText(msg) {
|
|
330
|
+
if (!msg.message?.content || !Array.isArray(msg.message.content)) return null;
|
|
331
|
+
const result = msg.message.content.find(c => c.type === 'tool_result');
|
|
332
|
+
if (!result) return null;
|
|
333
|
+
return typeof result.content === 'string' ? result.content : JSON.stringify(result.content);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cmdRinse(inputPath, opts) {
|
|
337
|
+
// Check if input is a directory or file
|
|
338
|
+
const stat = fs.statSync(inputPath);
|
|
339
|
+
const isDir = stat.isDirectory();
|
|
340
|
+
|
|
341
|
+
let allMessages = [];
|
|
342
|
+
if (isDir) {
|
|
343
|
+
const jsonlFiles = fs.readdirSync(inputPath).filter(f => f.endsWith('.jsonl'));
|
|
344
|
+
// Sort by modification time to get chronological order
|
|
345
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
346
|
+
const filePath = path.join(inputPath, f);
|
|
347
|
+
const msgs = readJsonl(filePath);
|
|
348
|
+
const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
349
|
+
const firstTs = timestamps.length > 0 ? Math.min(...timestamps) : 0;
|
|
350
|
+
return { filePath, msgs, firstTs };
|
|
351
|
+
});
|
|
352
|
+
filesWithStats.sort((a, b) => a.firstTs - b.firstTs);
|
|
353
|
+
for (const f of filesWithStats) {
|
|
354
|
+
allMessages.push(...f.msgs);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
allMessages = readJsonl(inputPath);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const messages = allMessages;
|
|
361
|
+
const collapseTools = opts.collapseTools;
|
|
362
|
+
const mild = opts.mild;
|
|
363
|
+
const truncateLen = mild ? 200 : 500;
|
|
364
|
+
|
|
365
|
+
const output = [];
|
|
366
|
+
|
|
367
|
+
for (const msg of messages) {
|
|
368
|
+
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
369
|
+
|
|
370
|
+
if (msg.type === 'user') {
|
|
371
|
+
// Tool results handling
|
|
372
|
+
if (isToolResult(msg)) {
|
|
373
|
+
if (mild) {
|
|
374
|
+
// Mild: keep tool results but truncated
|
|
375
|
+
const resultText = getToolResultText(msg);
|
|
376
|
+
if (resultText) {
|
|
377
|
+
output.push({
|
|
378
|
+
role: 'tool_result',
|
|
379
|
+
ts: msg.timestamp,
|
|
380
|
+
content: resultText.substring(0, truncateLen) + (resultText.length > truncateLen ? '...' : '')
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Otherwise (collapse mode): skip entirely
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const text = getUserText(msg);
|
|
389
|
+
if (text) {
|
|
390
|
+
output.push({
|
|
391
|
+
role: 'user',
|
|
392
|
+
ts: msg.timestamp,
|
|
393
|
+
content: text.substring(0, 500)
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
} else if (msg.type === 'assistant') {
|
|
397
|
+
const text = getAssistantText(msg);
|
|
398
|
+
const toolSummary = collapseTools ? collapseToolCall(msg) : null;
|
|
399
|
+
|
|
400
|
+
if (text) {
|
|
401
|
+
output.push({
|
|
402
|
+
role: 'assistant',
|
|
403
|
+
ts: msg.timestamp,
|
|
404
|
+
content: text.substring(0, truncateLen)
|
|
405
|
+
});
|
|
406
|
+
} else if (toolSummary && (collapseTools || mild)) {
|
|
407
|
+
output.push({
|
|
408
|
+
role: 'assistant',
|
|
409
|
+
ts: msg.timestamp,
|
|
410
|
+
content: toolSummary
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const totalChars = output.reduce((sum, m) => sum + (m.content?.length || 0), 0);
|
|
417
|
+
|
|
418
|
+
if (opts.dryRun) {
|
|
419
|
+
const userCount = output.filter(m => m.role === 'user').length;
|
|
420
|
+
const assistantCount = output.filter(m => m.role === 'assistant').length;
|
|
421
|
+
const toolResultCount = output.filter(m => m.role === 'tool_result').length;
|
|
422
|
+
const mode = mild ? '--logistics' : (collapseTools ? '--gotime' : '');
|
|
423
|
+
console.log(`--- DRY RUN: rinse${mode ? ' ' + mode : ''} ---`);
|
|
424
|
+
|
|
425
|
+
if (isDir) {
|
|
426
|
+
const jsonlFiles = fs.readdirSync(inputPath).filter(f => f.endsWith('.jsonl'));
|
|
427
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
428
|
+
const filePath = path.join(inputPath, f);
|
|
429
|
+
const msgs = readJsonl(filePath);
|
|
430
|
+
const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
431
|
+
const firstTs = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : null;
|
|
432
|
+
return { file: f, filePath, msgs, firstTs, raw: msgs.length };
|
|
433
|
+
});
|
|
434
|
+
filesWithStats.sort((a, b) => (a.firstTs || 0) - (b.firstTs || 0));
|
|
435
|
+
|
|
436
|
+
console.log(`Source: ${inputPath}`);
|
|
437
|
+
console.log(``);
|
|
438
|
+
console.log(`SESSIONS TO MERGE:`);
|
|
439
|
+
console.log(`# DATE TIME RAW FILE`);
|
|
440
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
441
|
+
filesWithStats.forEach((s, i) => {
|
|
442
|
+
const date = s.firstTs ? s.firstTs.toISOString().substring(0, 10) : 'unknown';
|
|
443
|
+
const time = s.firstTs ? s.firstTs.toISOString().substring(11, 16) : '??:??';
|
|
444
|
+
const fileName = s.file.replace('.jsonl', '');
|
|
445
|
+
console.log(`${String(i + 1).padStart(2)} ${date} ${time} ${String(s.raw).padStart(4)} ${fileName}`);
|
|
446
|
+
});
|
|
447
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
448
|
+
} else {
|
|
449
|
+
console.log(`Source: ${path.basename(inputPath)}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const tokens = estimateTokens({length: totalChars});
|
|
453
|
+
console.log(``);
|
|
454
|
+
console.log(`OUTPUT:`);
|
|
455
|
+
console.log(`Messages: ${output.length} (${userCount} user, ${assistantCount} assistant${toolResultCount ? ', ' + toolResultCount + ' tool_result' : ''})`);
|
|
456
|
+
console.log(`Characters: ${totalChars.toLocaleString()}`);
|
|
457
|
+
console.log(`Estimated tokens: ~${tokens.toLocaleString()}`);
|
|
458
|
+
console.log(`Compression: ${messages.length} → ${output.length} (${Math.round((1 - output.length/messages.length) * 100)}% reduction)`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Check token limit before outputting (only for directories)
|
|
463
|
+
const TOKEN_LIMIT = 50000;
|
|
464
|
+
const tokens = estimateTokens({length: totalChars});
|
|
465
|
+
if (isDir && tokens > TOKEN_LIMIT && !opts.yolo) {
|
|
466
|
+
console.error(`\n⚠️ HOLD UP: ${tokens.toLocaleString()} tokens exceeds safe limit (${TOKEN_LIMIT.toLocaleString()})`);
|
|
467
|
+
console.error(` This will eat most of your context window.`);
|
|
468
|
+
console.error(`\n To proceed anyway: --yolo`);
|
|
469
|
+
console.error(` Or pick specific sessions instead of entire folder.`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (opts.format === 'text') {
|
|
474
|
+
output.forEach(m => {
|
|
475
|
+
const ts = m.ts ? new Date(m.ts).toISOString().substring(11, 19) : '';
|
|
476
|
+
console.log(`[${ts}] ${m.role.toUpperCase()}: ${m.content}`);
|
|
477
|
+
});
|
|
478
|
+
} else {
|
|
479
|
+
output.forEach(m => console.log(JSON.stringify(m)));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.error(`\n--- Rinsed ${messages.length} → ${output.length} messages (${estimateTokens({length: totalChars}).toLocaleString()} tokens) ---`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function cmdState(jsonlPath) {
|
|
486
|
+
const messages = readJsonl(jsonlPath);
|
|
487
|
+
const discourse = messages.filter(isDiscourse);
|
|
488
|
+
|
|
489
|
+
const typeCounts = {};
|
|
490
|
+
messages.forEach(m => {
|
|
491
|
+
typeCounts[m.type] = (typeCounts[m.type] || 0) + 1;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const markerCounts = { aha_moment: 0, pushback: 0, strong_opinion: 0, clarification: 0, decision: 0 };
|
|
495
|
+
discourse.forEach(msg => {
|
|
496
|
+
const text = msg.type === 'user' ? getUserText(msg) : getAssistantText(msg);
|
|
497
|
+
const marker = detectMarker(text);
|
|
498
|
+
if (marker) markerCounts[marker]++;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const timestamps = messages.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
502
|
+
const firstTs = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : null;
|
|
503
|
+
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
|
|
504
|
+
|
|
505
|
+
console.log(`Source: ${path.resolve(jsonlPath)}`);
|
|
506
|
+
console.log(`Total messages: ${messages.length}`);
|
|
507
|
+
console.log(`\nBy type:`);
|
|
508
|
+
Object.entries(typeCounts).sort((a, b) => b[1] - a[1]).forEach(([type, count]) => {
|
|
509
|
+
console.log(` ${type}: ${count}`);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
console.log(`\nFiltered discourse: ${discourse.length} messages`);
|
|
513
|
+
console.log(` user: ${discourse.filter(m => m.type === 'user').length}`);
|
|
514
|
+
console.log(` assistant: ${discourse.filter(m => m.type === 'assistant').length}`);
|
|
515
|
+
|
|
516
|
+
console.log(`\nDetected markers:`);
|
|
517
|
+
Object.entries(markerCounts).forEach(([marker, count]) => {
|
|
518
|
+
if (count > 0) console.log(` ${marker}: ${count}`);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (firstTs && lastTs) {
|
|
522
|
+
console.log(`\nTime range:`);
|
|
523
|
+
console.log(` First: ${firstTs.toISOString()}`);
|
|
524
|
+
console.log(` Last: ${lastTs.toISOString()}`);
|
|
525
|
+
const durationMs = lastTs - firstTs;
|
|
526
|
+
const hours = Math.floor(durationMs / 3600000);
|
|
527
|
+
const mins = Math.floor((durationMs % 3600000) / 60000);
|
|
528
|
+
console.log(` Duration: ${hours}h ${mins}m`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function extractToolCalls(messages) {
|
|
533
|
+
const toolCalls = [];
|
|
534
|
+
const toolResults = {};
|
|
535
|
+
|
|
536
|
+
for (const msg of messages) {
|
|
537
|
+
// Extract tool_use from assistant messages
|
|
538
|
+
if (msg.type === 'assistant' && msg.message?.content) {
|
|
539
|
+
const content = msg.message.content;
|
|
540
|
+
if (Array.isArray(content)) {
|
|
541
|
+
for (const block of content) {
|
|
542
|
+
if (block.type === 'tool_use') {
|
|
543
|
+
toolCalls.push({
|
|
544
|
+
id: block.id,
|
|
545
|
+
name: block.name,
|
|
546
|
+
input: block.input,
|
|
547
|
+
timestamp: msg.timestamp
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Extract tool_result from user messages
|
|
555
|
+
if (msg.type === 'user' && msg.message?.content) {
|
|
556
|
+
const content = msg.message.content;
|
|
557
|
+
if (Array.isArray(content)) {
|
|
558
|
+
for (const block of content) {
|
|
559
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
560
|
+
toolResults[block.tool_use_id] = block.content;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Attach results to calls
|
|
568
|
+
for (const call of toolCalls) {
|
|
569
|
+
call.result = toolResults[call.id] || null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return toolCalls;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function cmdTools(opts) {
|
|
576
|
+
const dir = cwdToProjectFolder();
|
|
577
|
+
if (!dir) {
|
|
578
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Get most recent session
|
|
583
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
584
|
+
if (jsonlFiles.length === 0) {
|
|
585
|
+
console.error('No sessions found');
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Sort by modification time to get most recent
|
|
590
|
+
const filesWithStats = jsonlFiles.map(f => {
|
|
591
|
+
const filePath = path.join(dir, f);
|
|
592
|
+
const msgs = readJsonl(filePath);
|
|
593
|
+
const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
594
|
+
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : new Date(0);
|
|
595
|
+
return { filePath, msgs, lastTs };
|
|
596
|
+
});
|
|
597
|
+
filesWithStats.sort((a, b) => b.lastTs - a.lastTs);
|
|
598
|
+
|
|
599
|
+
// Exclude currently active sessions
|
|
600
|
+
const now = Date.now();
|
|
601
|
+
const inactiveSessions = filesWithStats.filter(s => {
|
|
602
|
+
const ageMs = now - s.lastTs.getTime();
|
|
603
|
+
return ageMs > ACTIVE_SESSION_THRESHOLD_MS;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
if (inactiveSessions.length === 0) {
|
|
607
|
+
console.error('No inactive sessions found (only the current session exists)');
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const session = inactiveSessions[0];
|
|
612
|
+
const toolCalls = extractToolCalls(session.msgs);
|
|
613
|
+
|
|
614
|
+
// If --show flag, find and display specific tool result
|
|
615
|
+
if (opts.show) {
|
|
616
|
+
const searchId = opts.show;
|
|
617
|
+
const match = toolCalls.find(t => t.id.includes(searchId));
|
|
618
|
+
if (!match) {
|
|
619
|
+
console.error(`No tool call found matching: ${searchId}`);
|
|
620
|
+
console.error(`\nAvailable IDs (showing last 10):`);
|
|
621
|
+
toolCalls.slice(-10).forEach(t => {
|
|
622
|
+
console.error(` ${t.id.substring(5, 17)} - ${t.name}`);
|
|
623
|
+
});
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
console.log(`=== TOOL RESULT: ${match.id} ===`);
|
|
628
|
+
console.log(`Tool: ${match.name}`);
|
|
629
|
+
console.log(`Time: ${match.timestamp ? new Date(match.timestamp).toISOString().substring(11, 19) : 'unknown'}`);
|
|
630
|
+
if (match.input) {
|
|
631
|
+
if (match.name === 'Bash') {
|
|
632
|
+
console.log(`Command: ${match.input.command?.substring(0, 100)}${match.input.command?.length > 100 ? '...' : ''}`);
|
|
633
|
+
} else if (match.name === 'Read') {
|
|
634
|
+
console.log(`File: ${match.input.file_path}`);
|
|
635
|
+
} else if (match.name === 'Edit' || match.name === 'Write') {
|
|
636
|
+
console.log(`File: ${match.input.file_path}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
console.log(`\n--- RESULT ---`);
|
|
640
|
+
if (match.result) {
|
|
641
|
+
console.log(typeof match.result === 'string' ? match.result : JSON.stringify(match.result, null, 2));
|
|
642
|
+
} else {
|
|
643
|
+
console.log('(no result captured)');
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// List all tool calls
|
|
649
|
+
console.log(`=== TOOL CALLS (${toolCalls.length} total) ===`);
|
|
650
|
+
console.log(`Session: ${path.basename(session.filePath)}`);
|
|
651
|
+
console.log(``);
|
|
652
|
+
console.log(`# ID TIME TOOL SUMMARY`);
|
|
653
|
+
console.log(`───────────────────────────────────────────────────────────────────────────`);
|
|
654
|
+
|
|
655
|
+
toolCalls.forEach((t, i) => {
|
|
656
|
+
const shortId = t.id.substring(5, 17);
|
|
657
|
+
const ts = t.timestamp ? new Date(t.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
658
|
+
let summary = '';
|
|
659
|
+
|
|
660
|
+
if (t.name === 'Bash') {
|
|
661
|
+
summary = t.input?.command?.substring(0, 40) || '';
|
|
662
|
+
} else if (t.name === 'Read') {
|
|
663
|
+
summary = t.input?.file_path?.split('/').pop() || '';
|
|
664
|
+
} else if (t.name === 'Edit' || t.name === 'Write') {
|
|
665
|
+
summary = t.input?.file_path?.split('/').pop() || '';
|
|
666
|
+
} else if (t.name === 'Grep') {
|
|
667
|
+
summary = t.input?.pattern || '';
|
|
668
|
+
} else if (t.name === 'Glob') {
|
|
669
|
+
summary = t.input?.pattern || '';
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const num = String(i + 1).padStart(3);
|
|
673
|
+
const name = t.name.padEnd(8);
|
|
674
|
+
console.log(`${num} ${shortId} ${ts} ${name} ${summary}`);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
console.log(``);
|
|
678
|
+
const exampleId = toolCalls.length > 0 ? toolCalls[0].id.substring(5, 17) : '<id>';
|
|
679
|
+
console.log(`Use: session-recall tools --show ${exampleId} to see specific result`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function cmdCheckpoints(opts) {
|
|
683
|
+
const dir = cwdToProjectFolder();
|
|
684
|
+
if (!dir) {
|
|
685
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
690
|
+
if (jsonlFiles.length === 0) {
|
|
691
|
+
console.error('No sessions found');
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Collect all checkpoints from all sessions
|
|
696
|
+
const checkpoints = [];
|
|
697
|
+
|
|
698
|
+
for (const file of jsonlFiles) {
|
|
699
|
+
const filePath = path.join(dir, file);
|
|
700
|
+
const msgs = readJsonl(filePath);
|
|
701
|
+
|
|
702
|
+
for (const msg of msgs) {
|
|
703
|
+
// Look for Bash tool calls with session-recall --checkpoint
|
|
704
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
705
|
+
for (const block of msg.message.content) {
|
|
706
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
707
|
+
const cmd = block.input?.command || '';
|
|
708
|
+
const match = cmd.match(/session-recall\s+--checkpoint\s+["']?([^"'\n]+)["']?/);
|
|
709
|
+
if (match) {
|
|
710
|
+
checkpoints.push({
|
|
711
|
+
name: match[1].trim(),
|
|
712
|
+
timestamp: msg.timestamp,
|
|
713
|
+
toolId: block.id,
|
|
714
|
+
session: file
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (checkpoints.length === 0) {
|
|
724
|
+
console.log('No checkpoints found.');
|
|
725
|
+
console.log('Create one with: session-recall --checkpoint "description"');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Sort by timestamp
|
|
730
|
+
checkpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
731
|
+
|
|
732
|
+
console.log(`=== CHECKPOINTS (${checkpoints.length} total) ===`);
|
|
733
|
+
console.log(``);
|
|
734
|
+
console.log(`# TIME ID NAME`);
|
|
735
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
736
|
+
|
|
737
|
+
checkpoints.forEach((cp, i) => {
|
|
738
|
+
const ts = cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
739
|
+
const shortId = cp.toolId ? cp.toolId.substring(5, 17) : '????????????';
|
|
740
|
+
console.log(`${String(i + 1).padStart(2)} ${ts} ${shortId} ${cp.name}`);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
console.log(``);
|
|
744
|
+
console.log(`Use: session-recall last 1 --after "checkpoint name" to recall from checkpoint`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function calcSessionCosts(messages) {
|
|
748
|
+
// Calculate logistics (mild) output
|
|
749
|
+
const logisticsOutput = [];
|
|
750
|
+
for (const msg of messages) {
|
|
751
|
+
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
752
|
+
if (msg.type === 'user') {
|
|
753
|
+
if (isToolResult(msg)) {
|
|
754
|
+
const resultText = getToolResultText(msg);
|
|
755
|
+
if (resultText) {
|
|
756
|
+
logisticsOutput.push({ content: resultText.substring(0, 200) });
|
|
757
|
+
}
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const text = getUserText(msg);
|
|
761
|
+
if (text) logisticsOutput.push({ content: text.substring(0, 500) });
|
|
762
|
+
} else if (msg.type === 'assistant') {
|
|
763
|
+
const text = getAssistantText(msg);
|
|
764
|
+
const toolSummary = collapseToolCall(msg);
|
|
765
|
+
if (text) logisticsOutput.push({ content: text.substring(0, 200) });
|
|
766
|
+
else if (toolSummary) logisticsOutput.push({ content: toolSummary });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const logisticsChars = logisticsOutput.reduce((sum, m) => sum + (m.content?.length || 0), 0);
|
|
770
|
+
|
|
771
|
+
// Calculate gotime (collapse) output
|
|
772
|
+
const gotimeOutput = [];
|
|
773
|
+
for (const msg of messages) {
|
|
774
|
+
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
775
|
+
if (msg.type === 'user') {
|
|
776
|
+
if (isToolResult(msg)) continue;
|
|
777
|
+
const text = getUserText(msg);
|
|
778
|
+
if (text) gotimeOutput.push({ content: text.substring(0, 500) });
|
|
779
|
+
} else if (msg.type === 'assistant') {
|
|
780
|
+
const text = getAssistantText(msg);
|
|
781
|
+
const toolSummary = collapseToolCall(msg);
|
|
782
|
+
if (text) gotimeOutput.push({ content: text.substring(0, 500) });
|
|
783
|
+
else if (toolSummary) gotimeOutput.push({ content: toolSummary });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const gotimeChars = gotimeOutput.reduce((sum, m) => sum + (m.content?.length || 0), 0);
|
|
787
|
+
|
|
788
|
+
// Calculate hot context
|
|
789
|
+
const discourse = messages.filter(isDiscourse);
|
|
790
|
+
const hot30 = discourse.slice(-30);
|
|
791
|
+
const hot50 = discourse.slice(-50);
|
|
792
|
+
const hot30Chars = hot30.reduce((sum, m) => {
|
|
793
|
+
const text = m.type === 'user' ? getUserText(m) : getAssistantText(m);
|
|
794
|
+
return sum + (text?.length || 0);
|
|
795
|
+
}, 0);
|
|
796
|
+
const hot50Chars = hot50.reduce((sum, m) => {
|
|
797
|
+
const text = m.type === 'user' ? getUserText(m) : getAssistantText(m);
|
|
798
|
+
return sum + (text?.length || 0);
|
|
799
|
+
}, 0);
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
raw: messages.length,
|
|
803
|
+
hot30: { count: hot30.length, tokens: estimateTokens({ length: hot30Chars }) },
|
|
804
|
+
hot50: { count: hot50.length, tokens: estimateTokens({ length: hot50Chars }) },
|
|
805
|
+
gotime: { count: gotimeOutput.length, tokens: estimateTokens({ length: gotimeChars }) },
|
|
806
|
+
logistics: { count: logisticsOutput.length, tokens: estimateTokens({ length: logisticsChars }) }
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function cmdCost(inputPath) {
|
|
811
|
+
// Check if input is a directory or file
|
|
812
|
+
const stat = fs.statSync(inputPath);
|
|
813
|
+
const isDir = stat.isDirectory();
|
|
814
|
+
const dir = isDir ? inputPath : path.dirname(inputPath);
|
|
815
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
816
|
+
|
|
817
|
+
if (jsonlFiles.length === 0) {
|
|
818
|
+
console.error('No .jsonl files found in ' + dir);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// If directory, use first file as "current" for single-session stats
|
|
823
|
+
const jsonlPath = isDir ? path.join(dir, jsonlFiles[0]) : inputPath;
|
|
824
|
+
const messages = readJsonl(jsonlPath);
|
|
825
|
+
const costs = calcSessionCosts(messages);
|
|
826
|
+
|
|
827
|
+
// Calculate project totals if multiple sessions or if scanning directory
|
|
828
|
+
let projectCosts = null;
|
|
829
|
+
if (jsonlFiles.length > 1 || isDir) {
|
|
830
|
+
const allMessages = [];
|
|
831
|
+
for (const file of jsonlFiles) {
|
|
832
|
+
const filePath = path.join(dir, file);
|
|
833
|
+
const msgs = readJsonl(filePath);
|
|
834
|
+
allMessages.push(...msgs);
|
|
835
|
+
}
|
|
836
|
+
projectCosts = calcSessionCosts(allMessages);
|
|
837
|
+
projectCosts.sessionCount = jsonlFiles.length;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
console.log(`=== IMPORT COST COMPARISON ===`);
|
|
841
|
+
console.log(`Source: ${isDir ? dir : path.basename(jsonlPath)}`);
|
|
842
|
+
|
|
843
|
+
if (!isDir) {
|
|
844
|
+
console.log(`Raw messages: ${costs.raw}`);
|
|
845
|
+
console.log(``);
|
|
846
|
+
console.log(`THIS SESSION:`);
|
|
847
|
+
console.log(`OPTION MESSAGES TOKENS USE CASE`);
|
|
848
|
+
console.log(`─────────────────────────────────────────────────────────`);
|
|
849
|
+
console.log(`hot --last 30 ${String(costs.hot30.count).padStart(4)} ~${String(costs.hot30.tokens.toLocaleString()).padStart(6)} Quick continue`);
|
|
850
|
+
console.log(`hot --last 50 ${String(costs.hot50.count).padStart(4)} ~${String(costs.hot50.tokens.toLocaleString()).padStart(6)} More context`);
|
|
851
|
+
console.log(`rinse --gotime ${String(costs.gotime.count).padStart(4)} ~${String(costs.gotime.tokens.toLocaleString()).padStart(6)} Full session, action mode`);
|
|
852
|
+
console.log(`rinse --logistics ${String(costs.logistics.count).padStart(4)} ~${String(costs.logistics.tokens.toLocaleString()).padStart(6)} Full session, see what was done`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (projectCosts) {
|
|
856
|
+
// Get individual session stats sorted by date
|
|
857
|
+
const sessionStats = [];
|
|
858
|
+
for (const file of jsonlFiles) {
|
|
859
|
+
const filePath = path.join(dir, file);
|
|
860
|
+
const msgs = readJsonl(filePath);
|
|
861
|
+
const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
862
|
+
const firstTs = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : null;
|
|
863
|
+
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
|
|
864
|
+
const sessCosts = calcSessionCosts(msgs);
|
|
865
|
+
sessionStats.push({
|
|
866
|
+
file,
|
|
867
|
+
filePath,
|
|
868
|
+
firstTs,
|
|
869
|
+
lastTs,
|
|
870
|
+
raw: msgs.length,
|
|
871
|
+
gotime: sessCosts.gotime.tokens,
|
|
872
|
+
logistics: sessCosts.logistics.tokens
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
// Sort by first timestamp
|
|
876
|
+
sessionStats.sort((a, b) => (a.firstTs || 0) - (b.firstTs || 0));
|
|
877
|
+
|
|
878
|
+
console.log(``);
|
|
879
|
+
console.log(`ALL SESSIONS IN PROJECT (${projectCosts.sessionCount}):`);
|
|
880
|
+
console.log(`# DATE TIME RAW GOTIME LOGISTICS FILE`);
|
|
881
|
+
console.log(`──────────────────────────────────────────────────────────────────────────────`);
|
|
882
|
+
|
|
883
|
+
// Split into significant (>500 tokens) and tiny sessions
|
|
884
|
+
const TINY_THRESHOLD = 500;
|
|
885
|
+
const significant = sessionStats.filter(s => s.gotime >= TINY_THRESHOLD);
|
|
886
|
+
const tiny = sessionStats.filter(s => s.gotime < TINY_THRESHOLD);
|
|
887
|
+
|
|
888
|
+
significant.forEach((s, i) => {
|
|
889
|
+
const date = s.firstTs ? s.firstTs.toISOString().substring(0, 10) : 'unknown';
|
|
890
|
+
const time = s.firstTs ? s.firstTs.toISOString().substring(11, 16) : '??:??';
|
|
891
|
+
const isCurrent = s.filePath === path.resolve(jsonlPath) ? ' ←' : '';
|
|
892
|
+
const fileName = s.file.replace('.jsonl', '');
|
|
893
|
+
console.log(`${String(i + 1).padStart(2)} ${date} ${time} ${String(s.raw).padStart(4)} ~${String(s.gotime.toLocaleString()).padStart(6)} ~${String(s.logistics.toLocaleString()).padStart(6)} ${fileName}${isCurrent}`);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
if (tiny.length > 0) {
|
|
897
|
+
const tinyRaw = tiny.reduce((sum, s) => sum + s.raw, 0);
|
|
898
|
+
const tinyGotime = tiny.reduce((sum, s) => sum + s.gotime, 0);
|
|
899
|
+
const tinyLogistics = tiny.reduce((sum, s) => sum + s.logistics, 0);
|
|
900
|
+
console.log(` ... ${tiny.length} small sessions ${String(tinyRaw).padStart(4)} ~${String(tinyGotime.toLocaleString()).padStart(6)} ~${String(tinyLogistics.toLocaleString()).padStart(6)}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
console.log(`──────────────────────────────────────────────────────────────────────────────`);
|
|
904
|
+
console.log(` TOTAL ${String(projectCosts.raw).padStart(4)} ~${String(projectCosts.gotime.tokens.toLocaleString()).padStart(6)} ~${String(projectCosts.logistics.tokens.toLocaleString()).padStart(6)}`);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
console.log(``);
|
|
908
|
+
console.log(`Pick your path, then run the command.`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function cwdToProjectFolder() {
|
|
912
|
+
// Map CWD to Claude project folder
|
|
913
|
+
// /home/hung/.skills/scripture-keeper -> -home-hung--skills-scripture-keeper
|
|
914
|
+
// Both / and . are replaced with -
|
|
915
|
+
const cwd = process.cwd();
|
|
916
|
+
const projectName = cwd.replace(/[\/\.]/g, '-');
|
|
917
|
+
const projectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
918
|
+
const projectPath = path.join(projectsDir, projectName);
|
|
919
|
+
|
|
920
|
+
if (fs.existsSync(projectPath)) {
|
|
921
|
+
return projectPath;
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function cmdLast(arg1, arg2, opts) {
|
|
927
|
+
// Usage: last [N] [folder]
|
|
928
|
+
// arg1 could be: undefined, a number string, or a folder path
|
|
929
|
+
// arg2 could be: undefined, or a folder path (if arg1 was a number)
|
|
930
|
+
const projectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
931
|
+
|
|
932
|
+
let n = 1;
|
|
933
|
+
let dir = null;
|
|
934
|
+
let filterAfterTs = null;
|
|
935
|
+
|
|
936
|
+
// Parse arguments
|
|
937
|
+
if (arg1) {
|
|
938
|
+
if (!isNaN(parseInt(arg1))) {
|
|
939
|
+
// arg1 is a number
|
|
940
|
+
n = parseInt(arg1);
|
|
941
|
+
// Check if arg2 is a folder
|
|
942
|
+
if (arg2 && fs.existsSync(arg2)) {
|
|
943
|
+
const stat = fs.statSync(arg2);
|
|
944
|
+
dir = stat.isDirectory() ? arg2 : path.dirname(arg2);
|
|
945
|
+
}
|
|
946
|
+
} else if (fs.existsSync(arg1)) {
|
|
947
|
+
// arg1 is a folder
|
|
948
|
+
const stat = fs.statSync(arg1);
|
|
949
|
+
dir = stat.isDirectory() ? arg1 : path.dirname(arg1);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Default behavior: use CWD project unless --all specified
|
|
954
|
+
if (!dir && !opts.all) {
|
|
955
|
+
dir = cwdToProjectFolder();
|
|
956
|
+
if (!dir) {
|
|
957
|
+
console.error(`No project folder found for CWD: ${process.cwd()}`);
|
|
958
|
+
console.error(`Use --all to scan all projects.`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// If --after is specified, find the checkpoint in the current project
|
|
964
|
+
if (opts.after) {
|
|
965
|
+
if (!dir) {
|
|
966
|
+
console.error(`--after requires CWD project context (cannot use --all)`);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
971
|
+
let checkpoint = null;
|
|
972
|
+
|
|
973
|
+
for (const file of jsonlFiles) {
|
|
974
|
+
const filePath = path.join(dir, file);
|
|
975
|
+
const msgs = readJsonl(filePath);
|
|
976
|
+
|
|
977
|
+
for (const msg of msgs) {
|
|
978
|
+
// Look for Bash tool calls with session-recall --checkpoint
|
|
979
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
980
|
+
for (const block of msg.message.content) {
|
|
981
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
982
|
+
const cmd = block.input?.command || '';
|
|
983
|
+
const match = cmd.match(/session-recall\s+--checkpoint\s+["']?([^"'\n]+)["']?/);
|
|
984
|
+
if (match && match[1].trim() === opts.after) {
|
|
985
|
+
checkpoint = {
|
|
986
|
+
name: match[1].trim(),
|
|
987
|
+
timestamp: msg.timestamp,
|
|
988
|
+
session: file
|
|
989
|
+
};
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (checkpoint) break;
|
|
996
|
+
}
|
|
997
|
+
if (checkpoint) break;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (!checkpoint) {
|
|
1001
|
+
console.error(`Checkpoint not found: "${opts.after}"`);
|
|
1002
|
+
console.error(`Available checkpoints:`);
|
|
1003
|
+
|
|
1004
|
+
// List all available checkpoints
|
|
1005
|
+
const checkpoints = [];
|
|
1006
|
+
for (const file of jsonlFiles) {
|
|
1007
|
+
const filePath = path.join(dir, file);
|
|
1008
|
+
const msgs = readJsonl(filePath);
|
|
1009
|
+
|
|
1010
|
+
for (const msg of msgs) {
|
|
1011
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
1012
|
+
for (const block of msg.message.content) {
|
|
1013
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
1014
|
+
const cmd = block.input?.command || '';
|
|
1015
|
+
const match = cmd.match(/session-recall\s+--checkpoint\s+["']?([^"'\n]+)["']?/);
|
|
1016
|
+
if (match) {
|
|
1017
|
+
checkpoints.push({
|
|
1018
|
+
name: match[1].trim(),
|
|
1019
|
+
timestamp: msg.timestamp
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (checkpoints.length === 0) {
|
|
1029
|
+
console.error(' (none)');
|
|
1030
|
+
} else {
|
|
1031
|
+
checkpoints.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
1032
|
+
checkpoints.forEach(cp => {
|
|
1033
|
+
const ts = cp.timestamp ? new Date(cp.timestamp).toISOString().substring(11, 19) : '??:??:??';
|
|
1034
|
+
console.error(` "${cp.name}" (${ts})`);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
filterAfterTs = checkpoint.timestamp ? new Date(checkpoint.timestamp).getTime() : null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Collect all sessions
|
|
1044
|
+
let allSessions = [];
|
|
1045
|
+
|
|
1046
|
+
if (dir) {
|
|
1047
|
+
// Single project folder
|
|
1048
|
+
const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
|
|
1049
|
+
for (const file of jsonlFiles) {
|
|
1050
|
+
const filePath = path.join(dir, file);
|
|
1051
|
+
const msgs = readJsonl(filePath);
|
|
1052
|
+
const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
1053
|
+
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
|
|
1054
|
+
allSessions.push({ filePath, msgs, lastTs, project: path.basename(dir) });
|
|
1055
|
+
}
|
|
1056
|
+
} else {
|
|
1057
|
+
// Scan all projects
|
|
1058
|
+
if (!fs.existsSync(projectsDir)) {
|
|
1059
|
+
console.error('No projects found at ' + projectsDir);
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
const projects = fs.readdirSync(projectsDir).filter(p => {
|
|
1063
|
+
const pPath = path.join(projectsDir, p);
|
|
1064
|
+
return fs.statSync(pPath).isDirectory();
|
|
1065
|
+
});
|
|
1066
|
+
for (const proj of projects) {
|
|
1067
|
+
const projPath = path.join(projectsDir, proj);
|
|
1068
|
+
const jsonlFiles = fs.readdirSync(projPath).filter(f => f.endsWith('.jsonl'));
|
|
1069
|
+
for (const file of jsonlFiles) {
|
|
1070
|
+
const filePath = path.join(projPath, file);
|
|
1071
|
+
try {
|
|
1072
|
+
const msgs = readJsonl(filePath);
|
|
1073
|
+
const timestamps = msgs.filter(m => m.timestamp).map(m => new Date(m.timestamp));
|
|
1074
|
+
const lastTs = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null;
|
|
1075
|
+
allSessions.push({ filePath, msgs, lastTs, project: proj });
|
|
1076
|
+
} catch (e) {
|
|
1077
|
+
// Skip unreadable files
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Sort by last timestamp descending
|
|
1084
|
+
allSessions.sort((a, b) => (b.lastTs || 0) - (a.lastTs || 0));
|
|
1085
|
+
|
|
1086
|
+
// Exclude currently active sessions (activity within threshold)
|
|
1087
|
+
const now = Date.now();
|
|
1088
|
+
const inactiveSessions = allSessions.filter(s => {
|
|
1089
|
+
if (!s.lastTs) return true; // Include sessions with no timestamp
|
|
1090
|
+
const ageMs = now - s.lastTs.getTime();
|
|
1091
|
+
return ageMs > ACTIVE_SESSION_THRESHOLD_MS;
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
// Take last N from inactive sessions
|
|
1095
|
+
const selected = inactiveSessions.slice(0, n);
|
|
1096
|
+
|
|
1097
|
+
if (selected.length === 0) {
|
|
1098
|
+
console.error('No sessions found');
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Count excluded active sessions for reporting
|
|
1103
|
+
const excludedActive = allSessions.length - inactiveSessions.length;
|
|
1104
|
+
|
|
1105
|
+
if (opts.dryRun) {
|
|
1106
|
+
console.log(`--- DRY RUN: last ${n} ---`);
|
|
1107
|
+
if (excludedActive > 0) {
|
|
1108
|
+
console.log(`(Excluded ${excludedActive} active session${excludedActive > 1 ? 's' : ''} with activity in last ${ACTIVE_SESSION_THRESHOLD_MS / 1000}s)`);
|
|
1109
|
+
}
|
|
1110
|
+
console.log(`\nMOST RECENT SESSIONS:`);
|
|
1111
|
+
console.log(`# LAST ACTIVE RAW PROJECT`);
|
|
1112
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
1113
|
+
selected.forEach((s, i) => {
|
|
1114
|
+
const ts = s.lastTs ? s.lastTs.toISOString().replace('T', ' ').substring(0, 16) : 'unknown';
|
|
1115
|
+
console.log(`${String(i + 1).padStart(2)} ${ts} ${String(s.msgs.length).padStart(4)} ${s.project}`);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Calculate combined output
|
|
1119
|
+
let allMsgs = selected.flatMap(s => s.msgs);
|
|
1120
|
+
// Apply checkpoint filter if specified
|
|
1121
|
+
if (filterAfterTs) {
|
|
1122
|
+
allMsgs = allMsgs.filter(msg => {
|
|
1123
|
+
const msgTs = msg.timestamp ? new Date(msg.timestamp).getTime() : 0;
|
|
1124
|
+
return msgTs > filterAfterTs;
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
const output = [];
|
|
1128
|
+
for (const msg of allMsgs) {
|
|
1129
|
+
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
1130
|
+
if (msg.type === 'user') {
|
|
1131
|
+
if (isToolResult(msg)) continue;
|
|
1132
|
+
const text = getUserText(msg);
|
|
1133
|
+
if (text) output.push({ content: text.substring(0, 500) });
|
|
1134
|
+
} else if (msg.type === 'assistant') {
|
|
1135
|
+
const text = getAssistantText(msg);
|
|
1136
|
+
const toolSummary = collapseToolCall(msg);
|
|
1137
|
+
if (text) output.push({ content: text.substring(0, 500) });
|
|
1138
|
+
else if (toolSummary) output.push({ content: toolSummary });
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const totalChars = output.reduce((sum, m) => sum + (m.content?.length || 0), 0);
|
|
1142
|
+
console.log(`────────────────────────────────────────────────────────────────────`);
|
|
1143
|
+
console.log(`\nOUTPUT: ${output.length} messages, ~${estimateTokens({length: totalChars}).toLocaleString()} tokens`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Output combined rinse (gotime mode)
|
|
1148
|
+
let allMsgs = selected.flatMap(s => s.msgs);
|
|
1149
|
+
// Sort by timestamp
|
|
1150
|
+
allMsgs.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1151
|
+
|
|
1152
|
+
// Filter by checkpoint timestamp if --after was specified
|
|
1153
|
+
if (filterAfterTs) {
|
|
1154
|
+
allMsgs = allMsgs.filter(msg => {
|
|
1155
|
+
const msgTs = msg.timestamp ? new Date(msg.timestamp).getTime() : 0;
|
|
1156
|
+
return msgTs > filterAfterTs;
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Build tool result metadata (size, duration)
|
|
1161
|
+
const resultMeta = buildToolResultMeta(allMsgs);
|
|
1162
|
+
|
|
1163
|
+
for (const msg of allMsgs) {
|
|
1164
|
+
if (NOISE_TYPES.includes(msg.type)) continue;
|
|
1165
|
+
if (msg.type === 'user') {
|
|
1166
|
+
if (isToolResult(msg)) continue;
|
|
1167
|
+
const text = getUserText(msg);
|
|
1168
|
+
if (text) {
|
|
1169
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
1170
|
+
console.log(`[${ts}] USER: ${text.substring(0, 500)}`);
|
|
1171
|
+
}
|
|
1172
|
+
} else if (msg.type === 'assistant') {
|
|
1173
|
+
const text = getAssistantText(msg);
|
|
1174
|
+
const toolSummary = collapseToolCall(msg, resultMeta);
|
|
1175
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().substring(11, 19) : '';
|
|
1176
|
+
if (text) {
|
|
1177
|
+
console.log(`[${ts}] ASSISTANT: ${text.substring(0, 500)}`);
|
|
1178
|
+
} else if (toolSummary) {
|
|
1179
|
+
console.log(`[${ts}] ASSISTANT: ${toolSummary}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Print instructions for Claude
|
|
1185
|
+
console.log(``);
|
|
1186
|
+
console.log(`---`);
|
|
1187
|
+
console.log(`To get a specific tool result: session-recall tools --show <id>`);
|
|
1188
|
+
console.log(`Session file: ${selected[0]?.filePath || 'unknown'}`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function showHelp() {
|
|
1192
|
+
console.log(`
|
|
1193
|
+
session-recall - Extract discourse from Claude conversation JSONL files
|
|
1194
|
+
|
|
1195
|
+
USAGE:
|
|
1196
|
+
session-recall <command> <jsonl-path> [options]
|
|
1197
|
+
|
|
1198
|
+
COMMANDS:
|
|
1199
|
+
parse <jsonl> Extract clean discourse, filter noise
|
|
1200
|
+
-f, --format <type> Output: jsonl (default) or text
|
|
1201
|
+
-s, --stats Show stats after output
|
|
1202
|
+
-d, --dry-run Show token estimate without output
|
|
1203
|
+
|
|
1204
|
+
hot <jsonl> Extract last N meaningful exchanges
|
|
1205
|
+
-l, --last <n> Number of exchanges (default: 30)
|
|
1206
|
+
-d, --dry-run Show token estimate without output
|
|
1207
|
+
|
|
1208
|
+
rinse <jsonl|folder> Compress for import, filter noise
|
|
1209
|
+
--logistics Keep tool results (truncated) - see what was done
|
|
1210
|
+
--gotime Drop tool results - just discourse & decisions
|
|
1211
|
+
--yolo Override 50k token safety limit for folders
|
|
1212
|
+
-f, --format <type> Output: jsonl (default) or text
|
|
1213
|
+
-d, --dry-run Show token estimate without output
|
|
1214
|
+
|
|
1215
|
+
state <jsonl> Show what exists, counts, markers
|
|
1216
|
+
|
|
1217
|
+
cost <jsonl|folder> Compare token costs for logistics vs gotime
|
|
1218
|
+
|
|
1219
|
+
last [N] [folder] Get last N sessions (default: 1, CWD project only)
|
|
1220
|
+
-a, --all Scan all projects (not just CWD)
|
|
1221
|
+
-d, --dry-run Show what would be pulled without output
|
|
1222
|
+
|
|
1223
|
+
tools List tool calls in most recent session (CWD project)
|
|
1224
|
+
--show <id> Show specific tool result by ID (partial match)
|
|
1225
|
+
|
|
1226
|
+
checkpoints List all checkpoints in current project
|
|
1227
|
+
|
|
1228
|
+
--checkpoint "name" Create a checkpoint marker (use as standalone flag)
|
|
1229
|
+
|
|
1230
|
+
last [N] --after "name" Recall from after a checkpoint
|
|
1231
|
+
|
|
1232
|
+
EXAMPLES:
|
|
1233
|
+
session-recall last 1 -d # Check what's available
|
|
1234
|
+
session-recall last 1 -f text # Pull last session
|
|
1235
|
+
session-recall tools # List tool calls
|
|
1236
|
+
session-recall tools --show 014opBVN # Get specific tool result
|
|
1237
|
+
|
|
1238
|
+
# Checkpoints - mark progress, recall from any point
|
|
1239
|
+
session-recall --checkpoint "finished research" # Create checkpoint
|
|
1240
|
+
session-recall checkpoints # List all checkpoints
|
|
1241
|
+
session-recall last 1 --after "finished research" # Recall from checkpoint
|
|
1242
|
+
`);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// ========== ARGUMENT PARSING ==========
|
|
1246
|
+
|
|
1247
|
+
function parseArgs(args) {
|
|
1248
|
+
const opts = {};
|
|
1249
|
+
const positional = [];
|
|
1250
|
+
|
|
1251
|
+
for (let i = 0; i < args.length; i++) {
|
|
1252
|
+
const arg = args[i];
|
|
1253
|
+
if (arg === '-f' || arg === '--format') {
|
|
1254
|
+
opts.format = args[++i];
|
|
1255
|
+
} else if (arg === '-s' || arg === '--stats') {
|
|
1256
|
+
opts.stats = true;
|
|
1257
|
+
} else if (arg === '-l' || arg === '--last') {
|
|
1258
|
+
opts.last = args[++i];
|
|
1259
|
+
} else if (arg === '-c' || arg === '--collapse-tools' || arg === '--gotime') {
|
|
1260
|
+
opts.collapseTools = true;
|
|
1261
|
+
} else if (arg === '-m' || arg === '--mild' || arg === '--logistics') {
|
|
1262
|
+
opts.mild = true;
|
|
1263
|
+
} else if (arg === '--yolo') {
|
|
1264
|
+
opts.yolo = true;
|
|
1265
|
+
} else if (arg === '-d' || arg === '--dry-run') {
|
|
1266
|
+
opts.dryRun = true;
|
|
1267
|
+
} else if (arg === '-a' || arg === '--all') {
|
|
1268
|
+
opts.all = true;
|
|
1269
|
+
} else if (arg === '--show') {
|
|
1270
|
+
opts.show = args[++i];
|
|
1271
|
+
} else if (arg === '--checkpoint') {
|
|
1272
|
+
opts.checkpoint = args[++i];
|
|
1273
|
+
} else if (arg === '--after') {
|
|
1274
|
+
opts.after = args[++i];
|
|
1275
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
1276
|
+
opts.help = true;
|
|
1277
|
+
} else if (!arg.startsWith('-')) {
|
|
1278
|
+
positional.push(arg);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return { opts, positional };
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ========== MAIN ==========
|
|
1286
|
+
|
|
1287
|
+
const args = process.argv.slice(2);
|
|
1288
|
+
const { opts, positional } = parseArgs(args);
|
|
1289
|
+
|
|
1290
|
+
// Handle --checkpoint flag first (no-op marker that gets logged)
|
|
1291
|
+
if (opts.checkpoint) {
|
|
1292
|
+
console.log(`✓ Checkpoint: ${opts.checkpoint}`);
|
|
1293
|
+
console.log(` Time: ${new Date().toISOString()}`);
|
|
1294
|
+
console.log(` Use 'session-recall checkpoints' to list all checkpoints`);
|
|
1295
|
+
process.exit(0);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (opts.help || positional.length === 0) {
|
|
1299
|
+
showHelp();
|
|
1300
|
+
process.exit(0);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const command = positional[0];
|
|
1304
|
+
const jsonlPath = positional[1];
|
|
1305
|
+
|
|
1306
|
+
// 'last', 'tools', and 'checkpoints' commands don't require a path
|
|
1307
|
+
if (!jsonlPath && command !== 'help' && command !== 'last' && command !== 'tools' && command !== 'checkpoints') {
|
|
1308
|
+
console.error('Error: JSONL path required');
|
|
1309
|
+
showHelp();
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// For 'last' command, the second arg might be a number, not a path
|
|
1314
|
+
if (command !== 'last' && jsonlPath && !fs.existsSync(jsonlPath)) {
|
|
1315
|
+
console.error(`Error: File not found: ${jsonlPath}`);
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
switch (command) {
|
|
1320
|
+
case 'parse':
|
|
1321
|
+
cmdParse(jsonlPath, opts);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'hot':
|
|
1324
|
+
cmdHot(jsonlPath, opts);
|
|
1325
|
+
break;
|
|
1326
|
+
case 'rinse':
|
|
1327
|
+
cmdRinse(jsonlPath, opts);
|
|
1328
|
+
break;
|
|
1329
|
+
case 'state':
|
|
1330
|
+
cmdState(jsonlPath);
|
|
1331
|
+
break;
|
|
1332
|
+
case 'cost':
|
|
1333
|
+
cmdCost(jsonlPath);
|
|
1334
|
+
break;
|
|
1335
|
+
case 'last':
|
|
1336
|
+
cmdLast(jsonlPath, positional[2], opts);
|
|
1337
|
+
break;
|
|
1338
|
+
case 'tools':
|
|
1339
|
+
cmdTools(opts);
|
|
1340
|
+
break;
|
|
1341
|
+
case 'checkpoints':
|
|
1342
|
+
cmdCheckpoints(opts);
|
|
1343
|
+
break;
|
|
1344
|
+
case 'help':
|
|
1345
|
+
showHelp();
|
|
1346
|
+
break;
|
|
1347
|
+
default:
|
|
1348
|
+
console.error(`Unknown command: ${command}`);
|
|
1349
|
+
showHelp();
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
}
|