claude-session-explorer 0.1.0 → 0.2.3
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/README.md +80 -0
- package/dist/cli.js +836 -417
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join as join7 } from "path";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/commands/export.ts
|
|
9
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
10
|
+
import { join as join2 } from "path";
|
|
11
11
|
|
|
12
|
+
// src/output.ts
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import Table from "cli-table3";
|
|
15
|
+
function writeJson(data) {
|
|
16
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
function writeTable(headers, rows) {
|
|
20
|
+
const table = new Table({
|
|
21
|
+
head: headers.map((h) => chalk.bold(h)),
|
|
22
|
+
style: { head: [], border: [] }
|
|
23
|
+
});
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
table.push(row);
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(`${table.toString()}
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function formatTimestamp(ms) {
|
|
31
|
+
return new Date(ms).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
32
|
+
}
|
|
33
|
+
function formatDuration(ms) {
|
|
34
|
+
const seconds = Math.floor(ms / 1e3);
|
|
35
|
+
if (seconds < 60) return `${seconds}s`;
|
|
36
|
+
const minutes = Math.floor(seconds / 60);
|
|
37
|
+
const remaining = seconds % 60;
|
|
38
|
+
if (minutes < 60) return `${minutes}m ${remaining}s`;
|
|
39
|
+
const hours = Math.floor(minutes / 60);
|
|
40
|
+
return `${hours}h ${minutes % 60}m`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/reader.ts
|
|
44
|
+
import { createReadStream } from "fs";
|
|
45
|
+
import { readdir, readFile } from "fs/promises";
|
|
46
|
+
import { join } from "path";
|
|
47
|
+
import { createInterface } from "readline";
|
|
12
48
|
async function readJsonFile(path) {
|
|
13
|
-
const content = await readFile(path,
|
|
49
|
+
const content = await readFile(path, "utf-8");
|
|
14
50
|
return JSON.parse(content);
|
|
15
51
|
}
|
|
16
52
|
async function* readJsonlFile(path) {
|
|
17
|
-
const stream = createReadStream(path,
|
|
53
|
+
const stream = createReadStream(path, "utf-8");
|
|
18
54
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
19
55
|
for await (const line of rl) {
|
|
20
56
|
const trimmed = line.trim();
|
|
@@ -24,16 +60,16 @@ async function* readJsonlFile(path) {
|
|
|
24
60
|
}
|
|
25
61
|
}
|
|
26
62
|
async function listSessionFiles(claudeDir) {
|
|
27
|
-
const sessionsDir = join(claudeDir,
|
|
63
|
+
const sessionsDir = join(claudeDir, "sessions");
|
|
28
64
|
try {
|
|
29
65
|
const files2 = await readdir(sessionsDir);
|
|
30
|
-
return files2.filter((f) => f.endsWith(
|
|
66
|
+
return files2.filter((f) => f.endsWith(".json")).map((f) => join(sessionsDir, f));
|
|
31
67
|
} catch {
|
|
32
68
|
return [];
|
|
33
69
|
}
|
|
34
70
|
}
|
|
35
71
|
async function listProjectDirs(claudeDir) {
|
|
36
|
-
const projectsDir = join(claudeDir,
|
|
72
|
+
const projectsDir = join(claudeDir, "projects");
|
|
37
73
|
try {
|
|
38
74
|
const entries = await readdir(projectsDir, { withFileTypes: true });
|
|
39
75
|
return entries.filter((e) => e.isDirectory()).map((e) => join(projectsDir, e.name));
|
|
@@ -45,7 +81,7 @@ async function findConversationFile(claudeDir, sessionId) {
|
|
|
45
81
|
const projectDirs = await listProjectDirs(claudeDir);
|
|
46
82
|
for (const dir of projectDirs) {
|
|
47
83
|
const files2 = await readdir(dir);
|
|
48
|
-
const match = files2.find((f) => f.includes(sessionId) && f.endsWith(
|
|
84
|
+
const match = files2.find((f) => f.includes(sessionId) && f.endsWith(".jsonl"));
|
|
49
85
|
if (match) return join(dir, match);
|
|
50
86
|
}
|
|
51
87
|
return null;
|
|
@@ -58,228 +94,186 @@ async function readSessionMeta(path) {
|
|
|
58
94
|
}
|
|
59
95
|
}
|
|
60
96
|
async function* readHistory(claudeDir) {
|
|
61
|
-
const historyPath = join(claudeDir,
|
|
97
|
+
const historyPath = join(claudeDir, "history.jsonl");
|
|
62
98
|
try {
|
|
63
99
|
for await (const entry of readJsonlFile(historyPath)) {
|
|
64
100
|
yield entry;
|
|
65
101
|
}
|
|
66
|
-
} catch {
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
67
104
|
}
|
|
68
105
|
|
|
69
|
-
// src/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
function writeTable(headers, rows) {
|
|
77
|
-
const table = new Table({
|
|
78
|
-
head: headers.map((h) => chalk.bold(h)),
|
|
79
|
-
style: { head: [], border: [] },
|
|
80
|
-
});
|
|
81
|
-
for (const row of rows) {
|
|
82
|
-
table.push(row);
|
|
106
|
+
// src/commands/export.ts
|
|
107
|
+
function extractText(message) {
|
|
108
|
+
const content = message.content;
|
|
109
|
+
if (typeof content === "string") return content;
|
|
110
|
+
if (Array.isArray(content)) {
|
|
111
|
+
return content.filter((b) => b.type === "text").map((b) => b.text).filter(Boolean).join("\n");
|
|
83
112
|
}
|
|
84
|
-
|
|
113
|
+
return "";
|
|
85
114
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
115
|
+
function extractToolCalls(message) {
|
|
116
|
+
const content = message.content;
|
|
117
|
+
if (!Array.isArray(content)) return [];
|
|
118
|
+
return content.filter((b) => b.type === "tool_use").map((b) => `\`${b.name}\``);
|
|
91
119
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
async function writeOutput(content, outDir, filename, stdout) {
|
|
121
|
+
if (stdout) {
|
|
122
|
+
process.stdout.write(content);
|
|
123
|
+
} else {
|
|
124
|
+
await mkdir(outDir, { recursive: true });
|
|
125
|
+
const outPath = join2(outDir, filename);
|
|
126
|
+
await writeFile(outPath, content);
|
|
127
|
+
process.stderr.write(`exported to ${outPath}
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
97
130
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const todayStart = /* @__PURE__ */ new Date();
|
|
114
|
-
todayStart.setHours(0, 0, 0, 0);
|
|
115
|
-
if (startedAt < todayStart.getTime()) continue;
|
|
131
|
+
function exportAsJson(entries, sessionId, opts) {
|
|
132
|
+
if (opts.stdout) {
|
|
133
|
+
writeJson(entries);
|
|
134
|
+
} else {
|
|
135
|
+
return writeOutput(JSON.stringify(entries, null, 2), opts.outDir, `${sessionId}.json`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function buildMarkdown(entries) {
|
|
139
|
+
let title = "";
|
|
140
|
+
const sections = [];
|
|
141
|
+
for (const raw of entries) {
|
|
142
|
+
const entry = raw;
|
|
143
|
+
if (entry.type === "ai-title") {
|
|
144
|
+
title = entry.aiTitle;
|
|
145
|
+
continue;
|
|
116
146
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
if (entry.type !== "user" && entry.type !== "assistant") continue;
|
|
148
|
+
const message = entry.message;
|
|
149
|
+
if (!message) continue;
|
|
150
|
+
const role = entry.type === "user" ? "User" : "Assistant";
|
|
151
|
+
const text = extractText(message);
|
|
152
|
+
const tools = extractToolCalls(message);
|
|
153
|
+
const timestamp = entry.timestamp ?? "";
|
|
154
|
+
if (!text && tools.length === 0) continue;
|
|
155
|
+
let section = `## ${role}`;
|
|
156
|
+
if (timestamp) section += ` <sub>${timestamp}</sub>`;
|
|
157
|
+
section += "\n\n";
|
|
158
|
+
if (text) section += text;
|
|
159
|
+
if (tools.length > 0) {
|
|
160
|
+
if (text) section += "\n\n";
|
|
161
|
+
section += `*Tools: ${tools.join(", ")}*`;
|
|
123
162
|
}
|
|
124
|
-
|
|
125
|
-
id: meta.sessionId,
|
|
126
|
-
date: formatTimestamp(startedAt),
|
|
127
|
-
project: meta.cwd,
|
|
128
|
-
kind: meta.kind,
|
|
129
|
-
entrypoint: meta.entrypoint,
|
|
130
|
-
startedAt,
|
|
131
|
-
});
|
|
163
|
+
sections.push(section);
|
|
132
164
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
165
|
+
return `# ${title || "Session"}
|
|
166
|
+
|
|
167
|
+
${sections.join("\n\n---\n\n")}`;
|
|
168
|
+
}
|
|
169
|
+
function buildCsv(entries) {
|
|
170
|
+
const rows = ["index,type,timestamp,content_length"];
|
|
171
|
+
let index = 0;
|
|
172
|
+
for (const raw of entries) {
|
|
173
|
+
const entry = raw;
|
|
174
|
+
if (entry.type !== "user" && entry.type !== "assistant") {
|
|
175
|
+
index++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const message = entry.message;
|
|
179
|
+
if (!message) {
|
|
180
|
+
index++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const text = extractText(message);
|
|
184
|
+
const ts = entry.timestamp ?? "";
|
|
185
|
+
rows.push(`${index},${entry.type},${ts},${text.length}`);
|
|
186
|
+
index++;
|
|
143
187
|
}
|
|
188
|
+
return rows.join("\n");
|
|
144
189
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
async function exportSession(sessionId, opts) {
|
|
191
|
+
if (!sessionId) {
|
|
192
|
+
process.stderr.write("error: session ID required\n");
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
148
195
|
const file = await findConversationFile(opts.claudeDir, sessionId);
|
|
149
196
|
if (!file) {
|
|
150
|
-
|
|
197
|
+
process.stderr.write(`error: session not found: ${sessionId}
|
|
198
|
+
`);
|
|
151
199
|
process.exit(1);
|
|
152
200
|
}
|
|
153
201
|
const entries = [];
|
|
154
202
|
for await (const entry of readJsonlFile(file)) {
|
|
155
203
|
entries.push(entry);
|
|
156
204
|
}
|
|
157
|
-
if (opts.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
205
|
+
if (opts.format === "json") {
|
|
206
|
+
await exportAsJson(entries, sessionId, opts);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (opts.format === "md") {
|
|
210
|
+
await writeOutput(buildMarkdown(entries), opts.outDir, `${sessionId}.md`, opts.stdout);
|
|
161
211
|
return;
|
|
162
212
|
}
|
|
163
|
-
|
|
213
|
+
if (opts.format === "csv") {
|
|
214
|
+
await writeOutput(buildCsv(entries), opts.outDir, `${sessionId}.csv`, opts.stdout);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
process.stderr.write(`error: unknown format: ${opts.format}
|
|
218
|
+
`);
|
|
219
|
+
process.exit(1);
|
|
164
220
|
}
|
|
165
221
|
|
|
166
|
-
// src/commands/
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
222
|
+
// src/commands/files.ts
|
|
223
|
+
var TOOL_OP_MAP = {
|
|
224
|
+
Read: "read",
|
|
225
|
+
Write: "write",
|
|
226
|
+
Edit: "edit"
|
|
227
|
+
};
|
|
228
|
+
function extractFileOps(blocks, messageIndex, timestamp) {
|
|
229
|
+
const ops = [];
|
|
230
|
+
for (const block of blocks) {
|
|
231
|
+
if (block.type !== "tool_use") continue;
|
|
232
|
+
const operation = TOOL_OP_MAP[block.name];
|
|
233
|
+
if (!operation) continue;
|
|
234
|
+
const input = block.input;
|
|
235
|
+
const filePath = input.file_path ?? input.path ?? "";
|
|
236
|
+
ops.push({ filePath, operation, timestamp, messageIndex });
|
|
237
|
+
}
|
|
238
|
+
return ops;
|
|
239
|
+
}
|
|
240
|
+
async function files(sessionId, opts) {
|
|
241
|
+
const file = await findConversationFile(opts.claudeDir, sessionId);
|
|
242
|
+
if (!file) {
|
|
243
|
+
process.stderr.write(`error: session not found: ${sessionId}
|
|
244
|
+
`);
|
|
173
245
|
process.exit(1);
|
|
174
246
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
messageIndex,
|
|
194
|
-
timestamp: record.timestamp ?? 0,
|
|
195
|
-
matchingText: content.slice(0, 200),
|
|
196
|
-
});
|
|
247
|
+
const operations = [];
|
|
248
|
+
let messageIndex = 0;
|
|
249
|
+
for await (const entry of readJsonlFile(file)) {
|
|
250
|
+
const record = entry;
|
|
251
|
+
if (record.type === "assistant") {
|
|
252
|
+
const message = record.message;
|
|
253
|
+
const content = message?.content;
|
|
254
|
+
if (Array.isArray(content)) {
|
|
255
|
+
const ops = extractFileOps(
|
|
256
|
+
content,
|
|
257
|
+
messageIndex,
|
|
258
|
+
record.timestamp ?? 0
|
|
259
|
+
);
|
|
260
|
+
for (const op of ops) {
|
|
261
|
+
if (opts.reads && op.operation !== "read") continue;
|
|
262
|
+
if (opts.writes && op.operation !== "write") continue;
|
|
263
|
+
if (opts.edits && op.operation !== "edit") continue;
|
|
264
|
+
operations.push(op);
|
|
197
265
|
}
|
|
198
|
-
messageIndex++;
|
|
199
266
|
}
|
|
200
267
|
}
|
|
201
|
-
|
|
202
|
-
writeJson(results);
|
|
203
|
-
}
|
|
204
|
-
function extractText(record, includeAssistant, includeTools) {
|
|
205
|
-
const type = record.type;
|
|
206
|
-
if (type === 'user' || (includeAssistant && type === 'assistant')) {
|
|
207
|
-
const content = record.content;
|
|
208
|
-
if (typeof content === 'string') return content;
|
|
209
|
-
if (Array.isArray(content)) {
|
|
210
|
-
return content
|
|
211
|
-
.filter((b) => b.type === 'text' || (includeTools && b.type === 'tool_use'))
|
|
212
|
-
.map((b) => (b.type === 'text' ? b.text : JSON.stringify(b.input)))
|
|
213
|
-
.join('\n');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// src/commands/stats.ts
|
|
220
|
-
async function stats(opts) {
|
|
221
|
-
const files2 = await listSessionFiles(opts.claudeDir);
|
|
222
|
-
let sessionCount = 0;
|
|
223
|
-
const projectCounts = /* @__PURE__ */ new Map();
|
|
224
|
-
const hourCounts = new Array(24).fill(0);
|
|
225
|
-
for (const file of files2) {
|
|
226
|
-
const meta = await readSessionMeta(file);
|
|
227
|
-
if (!meta) continue;
|
|
228
|
-
if (opts.project && !meta.cwd.startsWith(opts.project)) continue;
|
|
229
|
-
sessionCount++;
|
|
230
|
-
projectCounts.set(meta.cwd, (projectCounts.get(meta.cwd) ?? 0) + 1);
|
|
231
|
-
hourCounts[new Date(meta.startedAt).getHours()]++;
|
|
232
|
-
}
|
|
233
|
-
const result = {
|
|
234
|
-
sessionCount,
|
|
235
|
-
projectBreakdown: [...projectCounts.entries()]
|
|
236
|
-
.map(([path, count]) => ({ path, sessionCount: count }))
|
|
237
|
-
.sort((a, b) => b.sessionCount - a.sessionCount),
|
|
238
|
-
hourBreakdown: hourCounts.map((count, hour) => ({ hour, sessionCount: count })),
|
|
239
|
-
};
|
|
240
|
-
if (opts.pretty) {
|
|
241
|
-
writeTable(
|
|
242
|
-
['Metric', 'Value'],
|
|
243
|
-
[
|
|
244
|
-
['Sessions', String(sessionCount)],
|
|
245
|
-
['Projects', String(projectCounts.size)],
|
|
246
|
-
],
|
|
247
|
-
);
|
|
248
|
-
} else {
|
|
249
|
-
writeJson(result);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// src/commands/projects.ts
|
|
254
|
-
import { readdir as readdir3 } from 'fs/promises';
|
|
255
|
-
import { basename } from 'path';
|
|
256
|
-
|
|
257
|
-
async function projects(opts) {
|
|
258
|
-
const dirs = await listProjectDirs(opts.claudeDir);
|
|
259
|
-
const results = [];
|
|
260
|
-
for (const dir of dirs) {
|
|
261
|
-
const slug = basename(dir);
|
|
262
|
-
const files2 = await readdir3(dir);
|
|
263
|
-
const sessionFiles = files2.filter((f) => f.endsWith('.jsonl'));
|
|
264
|
-
results.push({
|
|
265
|
-
path: slug.replaceAll('-', '/'),
|
|
266
|
-
slug,
|
|
267
|
-
sessionCount: sessionFiles.length,
|
|
268
|
-
totalTokens: 0,
|
|
269
|
-
firstSession: 0,
|
|
270
|
-
lastSession: 0,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (opts.sort === 'sessions') {
|
|
274
|
-
results.sort((a, b) => b.sessionCount - a.sessionCount);
|
|
268
|
+
messageIndex++;
|
|
275
269
|
}
|
|
276
270
|
if (opts.pretty) {
|
|
277
271
|
writeTable(
|
|
278
|
-
[
|
|
279
|
-
|
|
272
|
+
["File", "Op", "Index"],
|
|
273
|
+
operations.map((o) => [o.filePath, o.operation, String(o.messageIndex)])
|
|
280
274
|
);
|
|
281
275
|
} else {
|
|
282
|
-
writeJson(
|
|
276
|
+
writeJson(operations);
|
|
283
277
|
}
|
|
284
278
|
}
|
|
285
279
|
|
|
@@ -287,75 +281,135 @@ async function projects(opts) {
|
|
|
287
281
|
async function history(opts) {
|
|
288
282
|
const entries = [];
|
|
289
283
|
for await (const entry of readHistory(opts.claudeDir)) {
|
|
290
|
-
if (opts.search && !entry.
|
|
284
|
+
if (opts.search && !entry.display.includes(opts.search)) continue;
|
|
291
285
|
if (opts.project && !entry.project.startsWith(opts.project)) continue;
|
|
292
286
|
if (opts.since && entry.timestamp < new Date(opts.since).getTime()) continue;
|
|
293
|
-
entries.push(
|
|
287
|
+
entries.push({
|
|
288
|
+
display: entry.display,
|
|
289
|
+
timestamp: entry.timestamp,
|
|
290
|
+
project: entry.project,
|
|
291
|
+
sessionId: entry.sessionId
|
|
292
|
+
});
|
|
294
293
|
}
|
|
295
294
|
entries.sort((a, b) => b.timestamp - a.timestamp);
|
|
296
295
|
const limited = entries.slice(0, opts.limit);
|
|
297
296
|
if (opts.pretty) {
|
|
298
297
|
writeTable(
|
|
299
|
-
[
|
|
300
|
-
limited.map((e) => [formatTimestamp(e.timestamp), e.project, e.
|
|
298
|
+
["Timestamp", "Project", "Prompt"],
|
|
299
|
+
limited.map((e) => [formatTimestamp(e.timestamp), e.project, e.display.slice(0, 80)])
|
|
301
300
|
);
|
|
302
301
|
} else {
|
|
303
302
|
writeJson(limited);
|
|
304
303
|
}
|
|
305
304
|
}
|
|
306
305
|
|
|
307
|
-
// src/commands/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
306
|
+
// src/commands/list.ts
|
|
307
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
308
|
+
import { join as join3 } from "path";
|
|
309
|
+
function matchesFilters(meta, opts) {
|
|
310
|
+
if (opts.project && !meta.cwd.startsWith(opts.project)) return false;
|
|
311
|
+
if (opts.kind && meta.kind !== opts.kind) return false;
|
|
312
|
+
if (opts.entrypoint && meta.entrypoint !== opts.entrypoint) return false;
|
|
313
|
+
if (opts.since && meta.startedAt < new Date(opts.since).getTime()) return false;
|
|
314
|
+
if (opts.until && meta.startedAt > new Date(opts.until).getTime()) return false;
|
|
315
|
+
if (opts.today && meta.startedAt < todayStart()) return false;
|
|
316
|
+
if (opts.yesterday) {
|
|
317
|
+
const ys = yesterdayStart();
|
|
318
|
+
if (meta.startedAt < ys || meta.startedAt >= ys + 864e5) return false;
|
|
319
|
+
}
|
|
320
|
+
if (opts.thisWeek && meta.startedAt < weekStart()) return false;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
function todayStart() {
|
|
324
|
+
const d = /* @__PURE__ */ new Date();
|
|
325
|
+
d.setHours(0, 0, 0, 0);
|
|
326
|
+
return d.getTime();
|
|
327
|
+
}
|
|
328
|
+
function yesterdayStart() {
|
|
329
|
+
return todayStart() - 864e5;
|
|
330
|
+
}
|
|
331
|
+
function weekStart() {
|
|
332
|
+
const now = /* @__PURE__ */ new Date();
|
|
333
|
+
const d = new Date(now);
|
|
334
|
+
d.setDate(now.getDate() - now.getDay());
|
|
335
|
+
d.setHours(0, 0, 0, 0);
|
|
336
|
+
return d.getTime();
|
|
337
|
+
}
|
|
338
|
+
async function findTitle(claudeDir, sessionId) {
|
|
339
|
+
let title = "";
|
|
340
|
+
let firstTs = 0;
|
|
341
|
+
let lastTs = 0;
|
|
342
|
+
const projectDirs = await listProjectDirs(claudeDir);
|
|
343
|
+
for (const dir of projectDirs) {
|
|
344
|
+
const files2 = await readdir2(dir);
|
|
345
|
+
const match = files2.find((f) => f.includes(sessionId) && f.endsWith(".jsonl"));
|
|
346
|
+
if (!match) continue;
|
|
347
|
+
for await (const entry of readJsonlFile(join3(dir, match))) {
|
|
348
|
+
const record = entry;
|
|
349
|
+
if (record.type === "ai-title") {
|
|
350
|
+
title = record.aiTitle;
|
|
351
|
+
}
|
|
352
|
+
const ts = record.timestamp;
|
|
353
|
+
if (ts) {
|
|
354
|
+
const ms = new Date(ts).getTime();
|
|
355
|
+
if (!firstTs || ms < firstTs) firstTs = ms;
|
|
356
|
+
if (ms > lastTs) lastTs = ms;
|
|
340
357
|
}
|
|
341
358
|
}
|
|
342
|
-
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
return { title, durationMs: lastTs - firstTs };
|
|
362
|
+
}
|
|
363
|
+
async function list(opts) {
|
|
364
|
+
const files2 = await listSessionFiles(opts.claudeDir);
|
|
365
|
+
const sessions = [];
|
|
366
|
+
for (const file of files2) {
|
|
367
|
+
const meta = await readSessionMeta(file);
|
|
368
|
+
if (!meta || !matchesFilters(meta, opts)) continue;
|
|
369
|
+
const { title, durationMs } = await findTitle(opts.claudeDir, meta.sessionId);
|
|
370
|
+
sessions.push({
|
|
371
|
+
id: meta.sessionId,
|
|
372
|
+
title,
|
|
373
|
+
date: formatTimestamp(meta.startedAt),
|
|
374
|
+
project: meta.cwd,
|
|
375
|
+
duration: durationMs > 0 ? formatDuration(durationMs) : "",
|
|
376
|
+
kind: meta.kind,
|
|
377
|
+
entrypoint: meta.entrypoint,
|
|
378
|
+
startedAt: meta.startedAt,
|
|
379
|
+
durationMs
|
|
380
|
+
});
|
|
343
381
|
}
|
|
382
|
+
if (opts.sort === "duration") {
|
|
383
|
+
sessions.sort((a, b) => b.durationMs - a.durationMs);
|
|
384
|
+
} else {
|
|
385
|
+
sessions.sort((a, b) => b.startedAt - a.startedAt);
|
|
386
|
+
}
|
|
387
|
+
if (opts.reverse) sessions.reverse();
|
|
388
|
+
const limited = sessions.slice(0, opts.limit);
|
|
344
389
|
if (opts.pretty) {
|
|
345
390
|
writeTable(
|
|
346
|
-
[
|
|
347
|
-
|
|
391
|
+
["ID", "Title", "Date", "Duration", "Project"],
|
|
392
|
+
limited.map((s) => [s.id.slice(0, 8), s.title.slice(0, 40), s.date, s.duration, s.project])
|
|
348
393
|
);
|
|
349
394
|
} else {
|
|
350
|
-
writeJson(
|
|
395
|
+
writeJson(limited);
|
|
351
396
|
}
|
|
352
397
|
}
|
|
353
398
|
|
|
354
399
|
// src/commands/messages.ts
|
|
400
|
+
function extractText2(content, raw) {
|
|
401
|
+
if (typeof content === "string") return content;
|
|
402
|
+
if (raw) return JSON.stringify(content);
|
|
403
|
+
if (Array.isArray(content)) {
|
|
404
|
+
return content.filter((b) => b.type === "text").map((b) => b.text).filter(Boolean).join("\n");
|
|
405
|
+
}
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
355
408
|
async function messages(sessionId, opts) {
|
|
356
409
|
const file = await findConversationFile(opts.claudeDir, sessionId);
|
|
357
410
|
if (!file) {
|
|
358
|
-
|
|
411
|
+
process.stderr.write(`error: session not found: ${sessionId}
|
|
412
|
+
`);
|
|
359
413
|
process.exit(1);
|
|
360
414
|
}
|
|
361
415
|
let results = [];
|
|
@@ -363,29 +417,30 @@ async function messages(sessionId, opts) {
|
|
|
363
417
|
for await (const entry of readJsonlFile(file)) {
|
|
364
418
|
const record = entry;
|
|
365
419
|
const type = record.type;
|
|
366
|
-
if (type !==
|
|
420
|
+
if (type !== "user" && type !== "assistant") {
|
|
367
421
|
index++;
|
|
368
422
|
continue;
|
|
369
423
|
}
|
|
370
|
-
if (opts.user && type !==
|
|
424
|
+
if (opts.user && type !== "user") {
|
|
371
425
|
index++;
|
|
372
426
|
continue;
|
|
373
427
|
}
|
|
374
|
-
if (opts.assistant && type !==
|
|
428
|
+
if (opts.assistant && type !== "assistant") {
|
|
375
429
|
index++;
|
|
376
430
|
continue;
|
|
377
431
|
}
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
index
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
432
|
+
const message = record.message;
|
|
433
|
+
if (!message) {
|
|
434
|
+
index++;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const content = extractText2(message.content, opts.raw);
|
|
438
|
+
const timestamp = record.timestamp ?? "";
|
|
439
|
+
results.push({ index, type, timestamp, content });
|
|
385
440
|
index++;
|
|
386
441
|
}
|
|
387
442
|
if (opts.slice) {
|
|
388
|
-
const [start, end] = opts.slice.split(
|
|
443
|
+
const [start, end] = opts.slice.split(":").map(Number);
|
|
389
444
|
results = results.slice(start, end);
|
|
390
445
|
} else if (opts.first) {
|
|
391
446
|
results = results.slice(0, opts.first);
|
|
@@ -394,202 +449,566 @@ async function messages(sessionId, opts) {
|
|
|
394
449
|
}
|
|
395
450
|
writeJson(results);
|
|
396
451
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
452
|
+
|
|
453
|
+
// src/commands/projects.ts
|
|
454
|
+
import { readdir as readdir3, stat } from "fs/promises";
|
|
455
|
+
import { basename, join as join4 } from "path";
|
|
456
|
+
async function projects(opts) {
|
|
457
|
+
const dirs = await listProjectDirs(opts.claudeDir);
|
|
458
|
+
const results = [];
|
|
459
|
+
for (const dir of dirs) {
|
|
460
|
+
const slug = basename(dir);
|
|
461
|
+
const files2 = await readdir3(dir);
|
|
462
|
+
const sessionFiles = files2.filter((f) => f.endsWith(".jsonl"));
|
|
463
|
+
let totalTokens = 0;
|
|
464
|
+
let firstSession = Number.MAX_SAFE_INTEGER;
|
|
465
|
+
let lastSession = 0;
|
|
466
|
+
for (const f of sessionFiles) {
|
|
467
|
+
const filePath = join4(dir, f);
|
|
468
|
+
const fileStat = await stat(filePath);
|
|
469
|
+
const mtimeMs = fileStat.mtimeMs;
|
|
470
|
+
const ctimeMs = fileStat.birthtimeMs || fileStat.ctimeMs;
|
|
471
|
+
if (ctimeMs < firstSession) firstSession = ctimeMs;
|
|
472
|
+
if (mtimeMs > lastSession) lastSession = mtimeMs;
|
|
473
|
+
for await (const entry of readJsonlFile(filePath)) {
|
|
474
|
+
const record = entry;
|
|
475
|
+
if (record.type !== "assistant") continue;
|
|
476
|
+
const message = record.message;
|
|
477
|
+
const usage = message?.usage;
|
|
478
|
+
if (usage) {
|
|
479
|
+
totalTokens += (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
results.push({
|
|
484
|
+
path: slug.replaceAll("-", "/"),
|
|
485
|
+
slug,
|
|
486
|
+
sessionCount: sessionFiles.length,
|
|
487
|
+
totalTokens,
|
|
488
|
+
firstSession: firstSession === Number.MAX_SAFE_INTEGER ? 0 : Math.floor(firstSession),
|
|
489
|
+
lastSession: Math.floor(lastSession)
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (opts.sort === "recent") {
|
|
493
|
+
results.sort((a, b) => b.lastSession - a.lastSession);
|
|
494
|
+
} else if (opts.sort === "tokens") {
|
|
495
|
+
results.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
496
|
+
} else {
|
|
497
|
+
results.sort((a, b) => b.sessionCount - a.sessionCount);
|
|
498
|
+
}
|
|
499
|
+
if (opts.pretty) {
|
|
500
|
+
writeTable(
|
|
501
|
+
["Path", "Sessions", "Tokens"],
|
|
502
|
+
results.map((p) => [p.path, String(p.sessionCount), String(p.totalTokens)])
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
writeJson(results);
|
|
406
506
|
}
|
|
407
|
-
return '';
|
|
408
507
|
}
|
|
409
508
|
|
|
410
|
-
// src/commands/
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
509
|
+
// src/commands/search.ts
|
|
510
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
511
|
+
import { join as join5 } from "path";
|
|
512
|
+
function buildMatcher(query, useRegex) {
|
|
513
|
+
if (useRegex) {
|
|
514
|
+
const re = new RegExp(query, "i");
|
|
515
|
+
return (text) => re.test(text);
|
|
516
|
+
}
|
|
517
|
+
const lower = query.toLowerCase();
|
|
518
|
+
return (text) => text.toLowerCase().includes(lower);
|
|
519
|
+
}
|
|
520
|
+
function extractText3(record, includeAssistant, includeTools) {
|
|
521
|
+
const type = record.type;
|
|
522
|
+
if (type !== "user" && !(includeAssistant && type === "assistant")) return null;
|
|
523
|
+
const message = record.message;
|
|
524
|
+
if (!message) return null;
|
|
525
|
+
const content = message.content;
|
|
526
|
+
if (typeof content === "string") return content;
|
|
527
|
+
if (Array.isArray(content)) {
|
|
528
|
+
const parts = [];
|
|
529
|
+
for (const block of content) {
|
|
530
|
+
if (block.type === "text" && block.text) {
|
|
531
|
+
parts.push(block.text);
|
|
532
|
+
} else if (includeTools && block.type === "tool_use" && block.input) {
|
|
533
|
+
parts.push(JSON.stringify(block.input));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
415
537
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
function extractMessageText(record) {
|
|
541
|
+
const message = record.message;
|
|
542
|
+
if (!message) return "";
|
|
543
|
+
const content = message.content;
|
|
544
|
+
if (typeof content === "string") return content;
|
|
545
|
+
if (Array.isArray(content)) {
|
|
546
|
+
return content.filter((b) => b.type === "text").map((b) => b.text).filter(Boolean).join("\n");
|
|
547
|
+
}
|
|
548
|
+
return "";
|
|
549
|
+
}
|
|
550
|
+
async function searchFile(filePath, sessionId, matcher, opts) {
|
|
551
|
+
const entries = [];
|
|
552
|
+
for await (const entry of readJsonlFile(filePath)) {
|
|
553
|
+
entries.push(entry);
|
|
554
|
+
}
|
|
555
|
+
const results = [];
|
|
556
|
+
const contextN = opts.context ? Number(opts.context) : 0;
|
|
557
|
+
for (let i = 0; i < entries.length; i++) {
|
|
558
|
+
const record = entries[i];
|
|
559
|
+
const text = extractText3(record, opts.all, opts.tools);
|
|
560
|
+
if (!text || !matcher(text)) continue;
|
|
561
|
+
const result = {
|
|
562
|
+
sessionId,
|
|
563
|
+
messageIndex: i,
|
|
564
|
+
type: record.type,
|
|
565
|
+
timestamp: record.timestamp ?? "",
|
|
566
|
+
matchingText: text.slice(0, 300)
|
|
567
|
+
};
|
|
568
|
+
if (contextN > 0) {
|
|
569
|
+
const ctx = [];
|
|
570
|
+
const start = Math.max(0, i - contextN);
|
|
571
|
+
const end = Math.min(entries.length, i + contextN + 1);
|
|
572
|
+
for (let j = start; j < end; j++) {
|
|
573
|
+
if (j === i) continue;
|
|
574
|
+
const e = entries[j];
|
|
575
|
+
const t = e.type;
|
|
576
|
+
if (t !== "user" && t !== "assistant") continue;
|
|
577
|
+
ctx.push({
|
|
578
|
+
index: j,
|
|
579
|
+
type: t,
|
|
580
|
+
text: extractMessageText(e).slice(0, 200)
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
result.context = ctx;
|
|
584
|
+
}
|
|
585
|
+
results.push(result);
|
|
586
|
+
}
|
|
587
|
+
return results;
|
|
588
|
+
}
|
|
589
|
+
async function search(query, opts) {
|
|
590
|
+
if (!query) {
|
|
591
|
+
process.stderr.write("error: search query required\n");
|
|
419
592
|
process.exit(1);
|
|
420
593
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
594
|
+
const matcher = buildMatcher(query, opts.regex);
|
|
595
|
+
const results = [];
|
|
596
|
+
const projectDirs = await listProjectDirs(opts.claudeDir);
|
|
597
|
+
for (const dir of projectDirs) {
|
|
598
|
+
if (opts.project && !dir.includes(opts.project.replaceAll("/", "-"))) continue;
|
|
599
|
+
const files2 = await readdir4(dir);
|
|
600
|
+
for (const file of files2.filter((f) => f.endsWith(".jsonl"))) {
|
|
601
|
+
const sessionId = file.replace(".jsonl", "");
|
|
602
|
+
const matches = await searchFile(join5(dir, file), sessionId, matcher, opts);
|
|
603
|
+
results.push(...matches);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
writeJson(results);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/commands/show.ts
|
|
610
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set(["Read", "Write", "Edit"]);
|
|
611
|
+
function extractMessageText2(msg) {
|
|
612
|
+
const content = msg.content;
|
|
613
|
+
if (typeof content === "string") return content;
|
|
614
|
+
if (Array.isArray(content)) {
|
|
615
|
+
return content.filter((b) => b.type === "text").map((b) => b.text).filter(Boolean).join("\n");
|
|
616
|
+
}
|
|
617
|
+
return "";
|
|
618
|
+
}
|
|
619
|
+
function showMessages(entries) {
|
|
620
|
+
const msgs = entries.filter((e) => e.type === "user" || e.type === "assistant").map((e, i) => {
|
|
621
|
+
const msg = e.message;
|
|
622
|
+
const text = msg ? extractMessageText2(msg) : "";
|
|
623
|
+
return {
|
|
624
|
+
index: i,
|
|
625
|
+
type: e.type,
|
|
626
|
+
timestamp: e.timestamp,
|
|
627
|
+
contentLength: text.length,
|
|
628
|
+
preview: text.slice(0, 150)
|
|
629
|
+
};
|
|
630
|
+
});
|
|
631
|
+
writeJson(msgs);
|
|
632
|
+
}
|
|
633
|
+
function showTools(entries) {
|
|
634
|
+
const toolCalls = [];
|
|
635
|
+
for (const entry of entries) {
|
|
636
|
+
if (entry.type !== "assistant") continue;
|
|
637
|
+
const msg = entry.message;
|
|
638
|
+
const content = msg?.content;
|
|
639
|
+
if (!Array.isArray(content)) continue;
|
|
640
|
+
for (const block of content) {
|
|
641
|
+
if (block.type !== "tool_use") continue;
|
|
642
|
+
const input = block.input;
|
|
643
|
+
toolCalls.push({
|
|
644
|
+
name: block.name,
|
|
645
|
+
id: block.id,
|
|
646
|
+
timestamp: entry.timestamp,
|
|
647
|
+
inputKeys: input ? Object.keys(input) : [],
|
|
648
|
+
inputPreview: input ? JSON.stringify(input).slice(0, 200) : ""
|
|
434
649
|
});
|
|
435
650
|
}
|
|
436
|
-
|
|
651
|
+
}
|
|
652
|
+
writeJson(toolCalls);
|
|
653
|
+
}
|
|
654
|
+
function showFiles(entries) {
|
|
655
|
+
const fileOps = [];
|
|
656
|
+
for (const entry of entries) {
|
|
657
|
+
if (entry.type !== "assistant") continue;
|
|
658
|
+
const msg = entry.message;
|
|
659
|
+
const content = msg?.content;
|
|
660
|
+
if (!Array.isArray(content)) continue;
|
|
661
|
+
for (const block of content) {
|
|
662
|
+
if (block.type !== "tool_use") continue;
|
|
663
|
+
if (!FILE_TOOLS.has(block.name)) continue;
|
|
664
|
+
const input = block.input;
|
|
665
|
+
fileOps.push({
|
|
666
|
+
tool: block.name,
|
|
667
|
+
filePath: input.file_path ?? input.path ?? "",
|
|
668
|
+
timestamp: entry.timestamp
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
writeJson(fileOps);
|
|
673
|
+
}
|
|
674
|
+
function showTokens(entries) {
|
|
675
|
+
const turns = [];
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
const msg = entry.message;
|
|
678
|
+
const usage = msg?.usage;
|
|
679
|
+
if (!usage) continue;
|
|
680
|
+
turns.push({
|
|
681
|
+
timestamp: entry.timestamp,
|
|
682
|
+
model: msg?.model ?? "unknown",
|
|
683
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
684
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
685
|
+
cacheReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
686
|
+
cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
|
|
687
|
+
});
|
|
437
688
|
}
|
|
438
689
|
writeJson(turns);
|
|
439
690
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
691
|
+
function showSummary(sessionId, file, entries) {
|
|
692
|
+
let title = "";
|
|
693
|
+
let firstTimestamp = 0;
|
|
694
|
+
let lastTimestamp = 0;
|
|
695
|
+
let userMessages = 0;
|
|
696
|
+
let assistantMessages = 0;
|
|
697
|
+
let totalInput = 0;
|
|
698
|
+
let totalOutput = 0;
|
|
699
|
+
let model = "";
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
if (entry.type === "ai-title") title = entry.aiTitle;
|
|
702
|
+
if (entry.type === "user") userMessages++;
|
|
703
|
+
if (entry.type === "assistant") assistantMessages++;
|
|
704
|
+
const ts = entry.timestamp;
|
|
705
|
+
if (ts) {
|
|
706
|
+
const ms = new Date(ts).getTime();
|
|
707
|
+
if (!firstTimestamp || ms < firstTimestamp) firstTimestamp = ms;
|
|
708
|
+
if (ms > lastTimestamp) lastTimestamp = ms;
|
|
709
|
+
}
|
|
710
|
+
const msg = entry.message;
|
|
711
|
+
if (msg?.usage) {
|
|
712
|
+
const usage = msg.usage;
|
|
713
|
+
totalInput += (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
714
|
+
totalOutput += usage.output_tokens ?? 0;
|
|
715
|
+
if (!model && msg.model) model = msg.model;
|
|
716
|
+
}
|
|
449
717
|
}
|
|
718
|
+
writeJson({
|
|
719
|
+
sessionId,
|
|
720
|
+
title,
|
|
721
|
+
model,
|
|
722
|
+
file,
|
|
723
|
+
startedAt: firstTimestamp ? formatTimestamp(firstTimestamp) : null,
|
|
724
|
+
duration: firstTimestamp && lastTimestamp ? formatDuration(lastTimestamp - firstTimestamp) : null,
|
|
725
|
+
entryCount: entries.length,
|
|
726
|
+
userMessages,
|
|
727
|
+
assistantMessages,
|
|
728
|
+
totalInputTokens: totalInput,
|
|
729
|
+
totalOutputTokens: totalOutput
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
async function show(sessionId, opts) {
|
|
450
733
|
const file = await findConversationFile(opts.claudeDir, sessionId);
|
|
451
734
|
if (!file) {
|
|
452
|
-
|
|
735
|
+
process.stderr.write(`error: session not found: ${sessionId}
|
|
736
|
+
`);
|
|
453
737
|
process.exit(1);
|
|
454
738
|
}
|
|
455
739
|
const entries = [];
|
|
456
740
|
for await (const entry of readJsonlFile(file)) {
|
|
457
741
|
entries.push(entry);
|
|
458
742
|
}
|
|
459
|
-
if (opts.
|
|
460
|
-
|
|
743
|
+
if (opts.raw) {
|
|
744
|
+
for (const entry of entries) {
|
|
745
|
+
process.stdout.write(`${JSON.stringify(entry)}
|
|
746
|
+
`);
|
|
747
|
+
}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (opts.messages) return showMessages(entries);
|
|
751
|
+
if (opts.tools) return showTools(entries);
|
|
752
|
+
if (opts.files) return showFiles(entries);
|
|
753
|
+
if (opts.tokens) return showTokens(entries);
|
|
754
|
+
showSummary(sessionId, file, entries);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/stats.ts
|
|
758
|
+
async function scanConversationTokens(filePath) {
|
|
759
|
+
let input = 0;
|
|
760
|
+
let output = 0;
|
|
761
|
+
const tools = /* @__PURE__ */ new Map();
|
|
762
|
+
for await (const entry of readJsonlFile(filePath)) {
|
|
763
|
+
const record = entry;
|
|
764
|
+
if (record.type !== "assistant") continue;
|
|
765
|
+
const message = record.message;
|
|
766
|
+
if (!message) continue;
|
|
767
|
+
const usage = message.usage;
|
|
768
|
+
if (usage) {
|
|
769
|
+
input += (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
770
|
+
output += usage.output_tokens ?? 0;
|
|
771
|
+
}
|
|
772
|
+
const content = message.content;
|
|
773
|
+
if (Array.isArray(content)) {
|
|
774
|
+
for (const block of content) {
|
|
775
|
+
if (block.type === "tool_use") {
|
|
776
|
+
const name = block.name;
|
|
777
|
+
tools.set(name, (tools.get(name) ?? 0) + 1);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { input, output, tools };
|
|
783
|
+
}
|
|
784
|
+
async function stats(opts) {
|
|
785
|
+
const metaFiles = await listSessionFiles(opts.claudeDir);
|
|
786
|
+
if (opts.daily || opts.weekly) {
|
|
787
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
788
|
+
for (const metaFile of metaFiles) {
|
|
789
|
+
const meta = await readSessionMeta(metaFile);
|
|
790
|
+
if (!meta) continue;
|
|
791
|
+
if (opts.project && !meta.cwd.startsWith(opts.project)) continue;
|
|
792
|
+
const date = formatTimestamp(meta.startedAt).split(" ")[0] ?? "";
|
|
793
|
+
const key = opts.weekly ? weekKey(meta.startedAt) : date;
|
|
794
|
+
const existing = buckets.get(key) ?? { sessions: 0, inputTokens: 0, outputTokens: 0 };
|
|
795
|
+
existing.sessions++;
|
|
796
|
+
const file = await findConversationFile(opts.claudeDir, meta.sessionId);
|
|
797
|
+
if (file) {
|
|
798
|
+
const scan = await scanConversationTokens(file);
|
|
799
|
+
existing.inputTokens += scan.input;
|
|
800
|
+
existing.outputTokens += scan.output;
|
|
801
|
+
}
|
|
802
|
+
buckets.set(key, existing);
|
|
803
|
+
}
|
|
804
|
+
const result = [...buckets.entries()].sort(([a], [b]) => b.localeCompare(a)).map(([date, s]) => ({
|
|
805
|
+
date,
|
|
806
|
+
sessions: s.sessions,
|
|
807
|
+
inputTokens: s.inputTokens,
|
|
808
|
+
outputTokens: s.outputTokens,
|
|
809
|
+
totalTokens: s.inputTokens + s.outputTokens
|
|
810
|
+
}));
|
|
811
|
+
writeJson(result);
|
|
461
812
|
return;
|
|
462
813
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
814
|
+
let sessionCount = 0;
|
|
815
|
+
let totalInput = 0;
|
|
816
|
+
let totalOutput = 0;
|
|
817
|
+
const projectCounts = /* @__PURE__ */ new Map();
|
|
818
|
+
const hourCounts = new Array(24).fill(0);
|
|
819
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
820
|
+
for (const metaFile of metaFiles) {
|
|
821
|
+
const meta = await readSessionMeta(metaFile);
|
|
822
|
+
if (!meta) continue;
|
|
823
|
+
if (opts.project && !meta.cwd.startsWith(opts.project)) continue;
|
|
824
|
+
sessionCount++;
|
|
825
|
+
projectCounts.set(meta.cwd, (projectCounts.get(meta.cwd) ?? 0) + 1);
|
|
826
|
+
const hour = new Date(meta.startedAt).getHours();
|
|
827
|
+
hourCounts[hour] = (hourCounts[hour] ?? 0) + 1;
|
|
828
|
+
const file = await findConversationFile(opts.claudeDir, meta.sessionId);
|
|
829
|
+
if (file) {
|
|
830
|
+
const scan = await scanConversationTokens(file);
|
|
831
|
+
totalInput += scan.input;
|
|
832
|
+
totalOutput += scan.output;
|
|
833
|
+
for (const [tool, count] of scan.tools) {
|
|
834
|
+
toolCounts.set(tool, (toolCounts.get(tool) ?? 0) + count);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
writeJson({
|
|
839
|
+
sessionCount,
|
|
840
|
+
totalInputTokens: totalInput,
|
|
841
|
+
totalOutputTokens: totalOutput,
|
|
842
|
+
totalTokens: totalInput + totalOutput,
|
|
843
|
+
projectBreakdown: [...projectCounts.entries()].sort((a, b) => b[1] - a[1]).map(([path, count]) => ({ path, sessionCount: count })),
|
|
844
|
+
toolBreakdown: [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).map(([name, callCount]) => ({ name, callCount })),
|
|
845
|
+
hourBreakdown: hourCounts.map((count, hour) => ({ hour, sessionCount: count }))
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
function weekKey(ms) {
|
|
849
|
+
const d = new Date(ms);
|
|
850
|
+
const day = d.getDay();
|
|
851
|
+
const monday = new Date(d);
|
|
852
|
+
monday.setDate(d.getDate() - (day + 6) % 7);
|
|
853
|
+
return formatTimestamp(monday.getTime()).split(" ")[0] ?? "";
|
|
854
|
+
}
|
|
478
855
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
856
|
+
// src/commands/tokens.ts
|
|
857
|
+
import { readdir as readdir5 } from "fs/promises";
|
|
858
|
+
import { join as join6 } from "path";
|
|
859
|
+
function extractUsage(record) {
|
|
860
|
+
const message = record.message;
|
|
861
|
+
if (!message) return null;
|
|
862
|
+
const usage = message.usage;
|
|
863
|
+
if (!usage) return null;
|
|
864
|
+
return {
|
|
865
|
+
turnIndex: 0,
|
|
866
|
+
inputTokens: usage.input_tokens ?? 0,
|
|
867
|
+
outputTokens: usage.output_tokens ?? 0,
|
|
868
|
+
cacheReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
869
|
+
cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
|
|
870
|
+
model: message.model ?? "unknown"
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
async function sessionTokens(filePath) {
|
|
874
|
+
const turns = [];
|
|
875
|
+
let turnIndex = 0;
|
|
876
|
+
for await (const entry of readJsonlFile(filePath)) {
|
|
877
|
+
const record = entry;
|
|
878
|
+
const usage = extractUsage(record);
|
|
879
|
+
if (usage) {
|
|
880
|
+
usage.turnIndex = turnIndex;
|
|
881
|
+
turns.push(usage);
|
|
882
|
+
}
|
|
883
|
+
turnIndex++;
|
|
884
|
+
}
|
|
885
|
+
return turns;
|
|
886
|
+
}
|
|
887
|
+
function newBucket() {
|
|
888
|
+
return { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
889
|
+
}
|
|
890
|
+
function addTurnToBucket(bucket, t) {
|
|
891
|
+
bucket.input += t.inputTokens;
|
|
892
|
+
bucket.output += t.outputTokens;
|
|
893
|
+
bucket.cacheRead += t.cacheReadTokens;
|
|
894
|
+
bucket.cacheCreation += t.cacheCreationTokens;
|
|
895
|
+
}
|
|
896
|
+
function bucketTotal(b) {
|
|
897
|
+
return b.input + b.output + b.cacheRead + b.cacheCreation;
|
|
898
|
+
}
|
|
899
|
+
async function tokensByModel(opts) {
|
|
900
|
+
const modelTotals = /* @__PURE__ */ new Map();
|
|
901
|
+
const projectDirs = await listProjectDirs(opts.claudeDir);
|
|
902
|
+
for (const dir of projectDirs) {
|
|
903
|
+
if (opts.project && !dir.includes(opts.project.replaceAll("/", "-"))) continue;
|
|
904
|
+
const files2 = await readdir5(dir);
|
|
905
|
+
for (const f of files2.filter((x) => x.endsWith(".jsonl"))) {
|
|
906
|
+
const turns = await sessionTokens(join6(dir, f));
|
|
907
|
+
for (const t of turns) {
|
|
908
|
+
const existing = modelTotals.get(t.model) ?? newBucket();
|
|
909
|
+
addTurnToBucket(existing, t);
|
|
910
|
+
modelTotals.set(t.model, existing);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
writeJson(
|
|
915
|
+
[...modelTotals.entries()].map(([model, b]) => ({
|
|
916
|
+
model,
|
|
917
|
+
inputTokens: b.input,
|
|
918
|
+
outputTokens: b.output,
|
|
919
|
+
cacheReadTokens: b.cacheRead,
|
|
920
|
+
cacheCreationTokens: b.cacheCreation,
|
|
921
|
+
totalTokens: bucketTotal(b)
|
|
922
|
+
}))
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
async function tokensDaily(opts) {
|
|
926
|
+
const dailyTotals = /* @__PURE__ */ new Map();
|
|
927
|
+
const metaFiles = await listSessionFiles(opts.claudeDir);
|
|
928
|
+
for (const metaFile of metaFiles) {
|
|
929
|
+
const meta = await readSessionMeta(metaFile);
|
|
930
|
+
if (!meta) continue;
|
|
931
|
+
if (opts.project && !meta.cwd.startsWith(opts.project)) continue;
|
|
932
|
+
const file = await findConversationFile(opts.claudeDir, meta.sessionId);
|
|
933
|
+
if (!file) continue;
|
|
934
|
+
const dateKey = formatTimestamp(meta.startedAt).split(" ")[0] ?? "";
|
|
935
|
+
const turns = await sessionTokens(file);
|
|
936
|
+
const existing = dailyTotals.get(dateKey) ?? newBucket();
|
|
937
|
+
for (const t of turns) addTurnToBucket(existing, t);
|
|
938
|
+
dailyTotals.set(dateKey, existing);
|
|
939
|
+
}
|
|
940
|
+
writeJson(
|
|
941
|
+
[...dailyTotals.entries()].sort(([a], [b]) => b.localeCompare(a)).map(([date, b]) => ({
|
|
942
|
+
date,
|
|
943
|
+
inputTokens: b.input,
|
|
944
|
+
outputTokens: b.output,
|
|
945
|
+
cacheReadTokens: b.cacheRead,
|
|
946
|
+
cacheCreationTokens: b.cacheCreation,
|
|
947
|
+
totalTokens: bucketTotal(b)
|
|
948
|
+
}))
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
async function tokensByProject(project, opts) {
|
|
952
|
+
const metaFiles = await listSessionFiles(opts.claudeDir);
|
|
953
|
+
const summaries = [];
|
|
954
|
+
for (const metaFile of metaFiles) {
|
|
955
|
+
const meta = await readSessionMeta(metaFile);
|
|
956
|
+
if (!meta?.cwd.startsWith(project)) continue;
|
|
957
|
+
const file = await findConversationFile(opts.claudeDir, meta.sessionId);
|
|
958
|
+
if (!file) continue;
|
|
959
|
+
const turns = await sessionTokens(file);
|
|
960
|
+
const b = newBucket();
|
|
961
|
+
for (const t of turns) addTurnToBucket(b, t);
|
|
962
|
+
summaries.push({
|
|
963
|
+
sessionId: meta.sessionId,
|
|
964
|
+
inputTokens: b.input,
|
|
965
|
+
outputTokens: b.output,
|
|
966
|
+
cacheReadTokens: b.cacheRead,
|
|
967
|
+
cacheCreationTokens: b.cacheCreation,
|
|
968
|
+
totalTokens: bucketTotal(b)
|
|
969
|
+
});
|
|
483
970
|
}
|
|
484
|
-
|
|
971
|
+
writeJson(summaries);
|
|
972
|
+
}
|
|
973
|
+
async function tokens(sessionId, opts) {
|
|
974
|
+
if (sessionId) {
|
|
975
|
+
const file = await findConversationFile(opts.claudeDir, sessionId);
|
|
976
|
+
if (!file) {
|
|
977
|
+
process.stderr.write(`error: session not found: ${sessionId}
|
|
485
978
|
`);
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
writeJson(await sessionTokens(file));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (opts.byModel) return tokensByModel(opts);
|
|
985
|
+
if (opts.daily) return tokensDaily(opts);
|
|
986
|
+
if (opts.project) return tokensByProject(opts.project, opts);
|
|
987
|
+
process.stderr.write("error: provide a session ID, --project, --daily, or --by-model\n");
|
|
988
|
+
process.exit(1);
|
|
486
989
|
}
|
|
487
990
|
|
|
488
991
|
// src/cli.ts
|
|
489
992
|
var program = new Command();
|
|
490
|
-
program
|
|
491
|
-
.name('cse')
|
|
492
|
-
.description('Deterministic CLI for Claude Code session history')
|
|
493
|
-
.version('0.1.0')
|
|
494
|
-
.option('--claude-dir <path>', 'override ~/.claude location', join4(homedir(), '.claude'))
|
|
495
|
-
.option('--out-dir <path>', 'override output directory', '.cse')
|
|
496
|
-
.option('--stdout', 'write to stdout instead of file')
|
|
497
|
-
.option('--json', 'JSON output (default)', true)
|
|
498
|
-
.option('--pretty', 'human-readable table output')
|
|
499
|
-
.option('--no-color', 'disable colored output')
|
|
500
|
-
.option('--verbose', 'debug info to stderr');
|
|
993
|
+
program.name("cse").description("Deterministic CLI for Claude Code session history").version("0.1.0").option("--claude-dir <path>", "override ~/.claude location", join7(homedir(), ".claude")).option("--out-dir <path>", "override output directory", ".cse").option("--stdout", "write to stdout instead of file").option("--json", "JSON output (default)", true).option("--pretty", "human-readable table output").option("--no-color", "disable colored output").option("--verbose", "debug info to stderr");
|
|
501
994
|
function globals() {
|
|
502
995
|
return program.opts();
|
|
503
996
|
}
|
|
504
|
-
program
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
.option('--messages', 'all messages with type, timestamp, content length')
|
|
522
|
-
.option('--tools', 'all tool_use blocks')
|
|
523
|
-
.option('--files', 'all file paths from tool_use inputs')
|
|
524
|
-
.option('--tokens', 'per-turn token usage')
|
|
525
|
-
.option('--raw', 'raw JSONL lines')
|
|
526
|
-
.action((id, opts) => show(id, { ...globals(), ...opts }));
|
|
527
|
-
program
|
|
528
|
-
.command('search <query>')
|
|
529
|
-
.description('Full-text search across sessions')
|
|
530
|
-
.option('--all', 'search user + assistant content')
|
|
531
|
-
.option('--tools', 'search tool_use inputs/outputs')
|
|
532
|
-
.option('--project <path>', 'scope to project')
|
|
533
|
-
.option('--since <date>', 'filter by date')
|
|
534
|
-
.option('--context <n>', 'include N messages around match')
|
|
535
|
-
.option('--regex', 'treat query as regex')
|
|
536
|
-
.action((query, opts) => search(query, { ...globals(), ...opts }));
|
|
537
|
-
program
|
|
538
|
-
.command('stats')
|
|
539
|
-
.description('Aggregate statistics')
|
|
540
|
-
.option('--project <path>', 'per-project stats')
|
|
541
|
-
.option('--daily', 'daily breakdown')
|
|
542
|
-
.option('--weekly', 'weekly breakdown')
|
|
543
|
-
.action((opts) => stats({ ...globals(), ...opts }));
|
|
544
|
-
program
|
|
545
|
-
.command('projects')
|
|
546
|
-
.description('List all projects')
|
|
547
|
-
.option('--sort <field>', 'sort: sessions, tokens, recent', 'sessions')
|
|
548
|
-
.action((opts) => projects({ ...globals(), ...opts }));
|
|
549
|
-
program
|
|
550
|
-
.command('history')
|
|
551
|
-
.description('Prompt history')
|
|
552
|
-
.option('--search <text>', 'exact substring match on prompt text')
|
|
553
|
-
.option('--project <path>', 'filter by project')
|
|
554
|
-
.option('--since <date>', 'filter by date')
|
|
555
|
-
.option('--limit <n>', 'number of entries', '50')
|
|
556
|
-
.action((opts) => history({ ...globals(), ...opts, limit: Number(opts.limit) }));
|
|
557
|
-
program
|
|
558
|
-
.command('files <session-id>')
|
|
559
|
-
.description('Extract file operations')
|
|
560
|
-
.option('--reads', 'only Read tool calls')
|
|
561
|
-
.option('--writes', 'only Write tool calls')
|
|
562
|
-
.option('--edits', 'only Edit tool calls')
|
|
563
|
-
.action((id, opts) => files(id, { ...globals(), ...opts }));
|
|
564
|
-
program
|
|
565
|
-
.command('messages <session-id>')
|
|
566
|
-
.description('Extract messages')
|
|
567
|
-
.option('--user', 'only user messages')
|
|
568
|
-
.option('--assistant', 'only assistant messages')
|
|
569
|
-
.option('--first <n>', 'first N messages')
|
|
570
|
-
.option('--last <n>', 'last N messages')
|
|
571
|
-
.option('--slice <range>', 'message range by index (e.g. 5:15)')
|
|
572
|
-
.option('--raw', 'include tool blocks')
|
|
573
|
-
.action((id, opts) =>
|
|
574
|
-
messages(id, {
|
|
575
|
-
...globals(),
|
|
576
|
-
...opts,
|
|
577
|
-
first: opts.first ? Number(opts.first) : void 0,
|
|
578
|
-
last: opts.last ? Number(opts.last) : void 0,
|
|
579
|
-
}),
|
|
580
|
-
);
|
|
581
|
-
program
|
|
582
|
-
.command('tokens [session-id]')
|
|
583
|
-
.description('Token usage')
|
|
584
|
-
.option('--project <path>', 'aggregate per-session for a project')
|
|
585
|
-
.option('--daily', 'daily token totals')
|
|
586
|
-
.option('--by-model', 'grouped by model')
|
|
587
|
-
.action((id, opts) => tokens(id, { ...globals(), ...opts }));
|
|
588
|
-
program
|
|
589
|
-
.command('export [session-id]')
|
|
590
|
-
.description('Export data')
|
|
591
|
-
.option('--format <fmt>', 'output format: json, md, csv', 'json')
|
|
592
|
-
.option('--all', 'export all sessions')
|
|
593
|
-
.option('--project <path>', 'export project sessions')
|
|
594
|
-
.action((id, opts) => exportSession(id, { ...globals(), ...opts }));
|
|
997
|
+
program.command("list").description("List sessions").option("--project <path>", "filter by project directory").option("--since <date>", "filter by start date").option("--until <date>", "filter by end date").option("--today", "shorthand for today's sessions").option("--yesterday", "shorthand for yesterday's sessions").option("--this-week", "shorthand for current week").option("--kind <kind>", "filter by session kind").option("--entrypoint <ep>", "filter by entrypoint (cli/ide/web)").option("--limit <n>", "limit results", "50").option("--sort <field>", "sort: date, duration, tokens, messages", "date").option("--reverse", "reverse sort order").action((opts) => list({ ...globals(), ...opts, limit: Number(opts.limit) }));
|
|
998
|
+
program.command("show <session-id>").description("Show session detail").option("--messages", "all messages with type, timestamp, content length").option("--tools", "all tool_use blocks").option("--files", "all file paths from tool_use inputs").option("--tokens", "per-turn token usage").option("--raw", "raw JSONL lines").action((id, opts) => show(id, { ...globals(), ...opts }));
|
|
999
|
+
program.command("search <query>").description("Full-text search across sessions").option("--all", "search user + assistant content").option("--tools", "search tool_use inputs/outputs").option("--project <path>", "scope to project").option("--since <date>", "filter by date").option("--context <n>", "include N messages around match").option("--regex", "treat query as regex").action((query, opts) => search(query, { ...globals(), ...opts }));
|
|
1000
|
+
program.command("stats").description("Aggregate statistics").option("--project <path>", "per-project stats").option("--daily", "daily breakdown").option("--weekly", "weekly breakdown").action((opts) => stats({ ...globals(), ...opts }));
|
|
1001
|
+
program.command("projects").description("List all projects").option("--sort <field>", "sort: sessions, tokens, recent", "sessions").action((opts) => projects({ ...globals(), ...opts }));
|
|
1002
|
+
program.command("history").description("Prompt history").option("--search <text>", "exact substring match on prompt text").option("--project <path>", "filter by project").option("--since <date>", "filter by date").option("--limit <n>", "number of entries", "50").action((opts) => history({ ...globals(), ...opts, limit: Number(opts.limit) }));
|
|
1003
|
+
program.command("files <session-id>").description("Extract file operations").option("--reads", "only Read tool calls").option("--writes", "only Write tool calls").option("--edits", "only Edit tool calls").action((id, opts) => files(id, { ...globals(), ...opts }));
|
|
1004
|
+
program.command("messages <session-id>").description("Extract messages").option("--user", "only user messages").option("--assistant", "only assistant messages").option("--first <n>", "first N messages").option("--last <n>", "last N messages").option("--slice <range>", "message range by index (e.g. 5:15)").option("--raw", "include tool blocks").action(
|
|
1005
|
+
(id, opts) => messages(id, {
|
|
1006
|
+
...globals(),
|
|
1007
|
+
...opts,
|
|
1008
|
+
first: opts.first ? Number(opts.first) : void 0,
|
|
1009
|
+
last: opts.last ? Number(opts.last) : void 0
|
|
1010
|
+
})
|
|
1011
|
+
);
|
|
1012
|
+
program.command("tokens [session-id]").description("Token usage").option("--project <path>", "aggregate per-session for a project").option("--daily", "daily token totals").option("--by-model", "grouped by model").action((id, opts) => tokens(id, { ...globals(), ...opts }));
|
|
1013
|
+
program.command("export [session-id]").description("Export data").option("--format <fmt>", "output format: json, md, csv", "json").option("--all", "export all sessions").option("--project <path>", "export project sessions").action((id, opts) => exportSession(id, { ...globals(), ...opts }));
|
|
595
1014
|
program.parse();
|