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.
Files changed (3) hide show
  1. package/README.md +80 -0
  2. package/dist/cli.js +836 -417
  3. 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 { Command } from 'commander';
5
- // src/reader.ts
6
- import { createReadStream } from 'fs';
7
- import { readdir, readFile } from 'fs/promises';
8
- import { homedir } from 'os';
9
- import { join, join as join4 } from 'path';
10
- import { createInterface } from 'readline';
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, 'utf-8');
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, 'utf-8');
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, 'sessions');
63
+ const sessionsDir = join(claudeDir, "sessions");
28
64
  try {
29
65
  const files2 = await readdir(sessionsDir);
30
- return files2.filter((f) => f.endsWith('.json')).map((f) => join(sessionsDir, f));
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, 'projects');
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('.jsonl'));
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, 'history.jsonl');
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/output.ts
70
- import chalk from 'chalk';
71
- import Table from 'cli-table3';
72
-
73
- function writeJson(data) {
74
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
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
- process.stdout.write(table.toString() + '\n');
113
+ return "";
85
114
  }
86
- function writeError(message) {
87
- process.stderr.write(
88
- chalk.red(`error: ${message}
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 formatTimestamp(ms) {
93
- return new Date(ms)
94
- .toISOString()
95
- .replace('T', ' ')
96
- .replace(/\.\d+Z$/, '');
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
- // src/commands/list.ts
100
- async function list(opts) {
101
- const files2 = await listSessionFiles(opts.claudeDir);
102
- const sessions = [];
103
- for (const file of files2) {
104
- const meta = await readSessionMeta(file);
105
- if (!meta) continue;
106
- if (opts.project && !meta.cwd.startsWith(opts.project)) continue;
107
- if (opts.kind && meta.kind !== opts.kind) continue;
108
- if (opts.entrypoint && meta.entrypoint !== opts.entrypoint) continue;
109
- const startedAt = meta.startedAt;
110
- if (opts.since && startedAt < new Date(opts.since).getTime()) continue;
111
- if (opts.until && startedAt > new Date(opts.until).getTime()) continue;
112
- if (opts.today) {
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 (opts.thisWeek) {
118
- const now = /* @__PURE__ */ new Date();
119
- const weekStart = new Date(now);
120
- weekStart.setDate(now.getDate() - now.getDay());
121
- weekStart.setHours(0, 0, 0, 0);
122
- if (startedAt < weekStart.getTime()) continue;
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
- sessions.push({
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
- sessions.sort((a, b) => b.startedAt - a.startedAt);
134
- if (opts.reverse) sessions.reverse();
135
- const limited = sessions.slice(0, opts.limit);
136
- if (opts.pretty) {
137
- writeTable(
138
- ['ID', 'Date', 'Project', 'Kind', 'Entrypoint'],
139
- limited.map((s) => [s.id, s.date, s.project, s.kind, s.entrypoint]),
140
- );
141
- } else {
142
- writeJson(limited);
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
- // src/commands/show.ts
147
- async function show(sessionId, opts) {
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
- writeError(`Session not found: ${sessionId}`);
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.raw) {
158
- for (const entry of entries) {
159
- process.stdout.write(JSON.stringify(entry) + '\n');
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
- writeJson({ sessionId, file, entryCount: entries.length });
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/search.ts
167
- import { readdir as readdir2 } from 'fs/promises';
168
- import { join as join2 } from 'path';
169
-
170
- async function search(query, opts) {
171
- if (!query) {
172
- writeError('Search query required');
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 matcher = opts.regex
176
- ? (text) => new RegExp(query).test(text)
177
- : (text) => text.includes(query);
178
- const results = [];
179
- const projectDirs = await listProjectDirs(opts.claudeDir);
180
- for (const dir of projectDirs) {
181
- if (opts.project && !dir.includes(opts.project)) continue;
182
- const files2 = await readdir2(dir);
183
- const jsonlFiles = files2.filter((f) => f.endsWith('.jsonl'));
184
- for (const file of jsonlFiles) {
185
- const sessionId = file.replace('.jsonl', '');
186
- let messageIndex = 0;
187
- for await (const entry of readJsonlFile(join2(dir, file))) {
188
- const record = entry;
189
- const content = extractText(record, opts.all, opts.tools);
190
- if (content && matcher(content)) {
191
- results.push({
192
- sessionId,
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
- ['Path', 'Slug', 'Sessions'],
279
- results.map((p) => [p.path, p.slug, String(p.sessionCount)]),
272
+ ["File", "Op", "Index"],
273
+ operations.map((o) => [o.filePath, o.operation, String(o.messageIndex)])
280
274
  );
281
275
  } else {
282
- writeJson(results);
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.prompt.includes(opts.search)) continue;
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(entry);
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
- ['Timestamp', 'Project', 'Prompt'],
300
- limited.map((e) => [formatTimestamp(e.timestamp), e.project, e.prompt.slice(0, 80)]),
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/files.ts
308
- var TOOL_OP_MAP = {
309
- Read: 'read',
310
- Write: 'write',
311
- Edit: 'edit',
312
- };
313
- async function files(sessionId, opts) {
314
- const file = await findConversationFile(opts.claudeDir, sessionId);
315
- if (!file) {
316
- writeError(`Session not found: ${sessionId}`);
317
- process.exit(1);
318
- }
319
- const operations = [];
320
- let messageIndex = 0;
321
- for await (const entry of readJsonlFile(file)) {
322
- const record = entry;
323
- if (record.type === 'assistant' && Array.isArray(record.content)) {
324
- for (const block of record.content) {
325
- if (block.type !== 'tool_use') continue;
326
- const toolName = block.name;
327
- const operation = TOOL_OP_MAP[toolName];
328
- if (!operation) continue;
329
- if (opts.reads && operation !== 'read') continue;
330
- if (opts.writes && operation !== 'write') continue;
331
- if (opts.edits && operation !== 'edit') continue;
332
- const input = block.input;
333
- const filePath = input.file_path ?? input.path ?? '';
334
- operations.push({
335
- filePath,
336
- operation,
337
- timestamp: record.timestamp ?? 0,
338
- messageIndex,
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
- messageIndex++;
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
- ['File', 'Op', 'Index'],
347
- operations.map((o) => [o.filePath, o.operation, String(o.messageIndex)]),
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(operations);
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
- writeError(`Session not found: ${sessionId}`);
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 !== 'user' && type !== 'assistant') {
420
+ if (type !== "user" && type !== "assistant") {
367
421
  index++;
368
422
  continue;
369
423
  }
370
- if (opts.user && type !== 'user') {
424
+ if (opts.user && type !== "user") {
371
425
  index++;
372
426
  continue;
373
427
  }
374
- if (opts.assistant && type !== 'assistant') {
428
+ if (opts.assistant && type !== "assistant") {
375
429
  index++;
376
430
  continue;
377
431
  }
378
- const content = extractTextContent(record, opts.raw);
379
- results.push({
380
- index,
381
- type,
382
- timestamp: record.timestamp ?? 0,
383
- content,
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(':').map(Number);
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
- function extractTextContent(record, raw) {
398
- const content = record.content;
399
- if (typeof content === 'string') return content;
400
- if (raw) return JSON.stringify(content);
401
- if (Array.isArray(content)) {
402
- return content
403
- .filter((b) => b.type === 'text')
404
- .map((b) => b.text)
405
- .join('\n');
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/tokens.ts
411
- async function tokens(sessionId, opts) {
412
- if (!sessionId) {
413
- writeError('Session ID required (project-level aggregation not yet implemented)');
414
- process.exit(1);
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
- const file = await findConversationFile(opts.claudeDir, sessionId);
417
- if (!file) {
418
- writeError(`Session not found: ${sessionId}`);
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 turns = [];
422
- let turnIndex = 0;
423
- for await (const entry of readJsonlFile(file)) {
424
- const record = entry;
425
- const usage = record.usage;
426
- if (usage) {
427
- turns.push({
428
- turnIndex,
429
- inputTokens: usage.input_tokens ?? 0,
430
- outputTokens: usage.output_tokens ?? 0,
431
- cacheReadTokens: usage.cache_read_input_tokens ?? 0,
432
- cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
433
- model: record.model,
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
- turnIndex++;
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
- // src/commands/export.ts
442
- import { mkdir, writeFile } from 'fs/promises';
443
- import { join as join3 } from 'path';
444
-
445
- async function exportSession(sessionId, opts) {
446
- if (!sessionId) {
447
- writeError('Session ID required (--all not yet implemented)');
448
- process.exit(1);
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
- writeError(`Session not found: ${sessionId}`);
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.stdout) {
460
- writeJson(entries);
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
- await mkdir(opts.outDir, { recursive: true });
464
- const outPath = join3(opts.outDir, `${sessionId}.${opts.format}`);
465
- if (opts.format === 'json') {
466
- await writeFile(outPath, JSON.stringify(entries, null, 2));
467
- } else if (opts.format === 'md') {
468
- const md = entries
469
- .filter((e) => {
470
- const r = e;
471
- return r.type === 'user' || r.type === 'assistant';
472
- })
473
- .map((e) => {
474
- const r = e;
475
- const role = r.type === 'user' ? 'User' : 'Assistant';
476
- const content = typeof r.content === 'string' ? r.content : JSON.stringify(r.content);
477
- return `## ${role}
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
- ${content}`;
480
- })
481
- .join('\n\n---\n\n');
482
- await writeFile(outPath, md);
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
- process.stderr.write(`Exported to ${outPath}
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
- .command('list')
506
- .description('List sessions')
507
- .option('--project <path>', 'filter by project directory')
508
- .option('--since <date>', 'filter by start date')
509
- .option('--until <date>', 'filter by end date')
510
- .option('--today', "shorthand for today's sessions")
511
- .option('--this-week', 'shorthand for current week')
512
- .option('--kind <kind>', 'filter by session kind')
513
- .option('--entrypoint <ep>', 'filter by entrypoint (cli/ide/web)')
514
- .option('--limit <n>', 'limit results', '50')
515
- .option('--sort <field>', 'sort: date, duration, tokens, messages', 'date')
516
- .option('--reverse', 'reverse sort order')
517
- .action((opts) => list({ ...globals(), ...opts, limit: Number(opts.limit) }));
518
- program
519
- .command('show <session-id>')
520
- .description('Show session detail')
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();