claude-session-explorer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +595 -0
  2. package/package.json +45 -0
package/dist/cli.js ADDED
@@ -0,0 +1,595 @@
1
+ #!/usr/bin/env node
2
+
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';
11
+
12
+ async function readJsonFile(path) {
13
+ const content = await readFile(path, 'utf-8');
14
+ return JSON.parse(content);
15
+ }
16
+ async function* readJsonlFile(path) {
17
+ const stream = createReadStream(path, 'utf-8');
18
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
19
+ for await (const line of rl) {
20
+ const trimmed = line.trim();
21
+ if (trimmed) {
22
+ yield JSON.parse(trimmed);
23
+ }
24
+ }
25
+ }
26
+ async function listSessionFiles(claudeDir) {
27
+ const sessionsDir = join(claudeDir, 'sessions');
28
+ try {
29
+ const files2 = await readdir(sessionsDir);
30
+ return files2.filter((f) => f.endsWith('.json')).map((f) => join(sessionsDir, f));
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+ async function listProjectDirs(claudeDir) {
36
+ const projectsDir = join(claudeDir, 'projects');
37
+ try {
38
+ const entries = await readdir(projectsDir, { withFileTypes: true });
39
+ return entries.filter((e) => e.isDirectory()).map((e) => join(projectsDir, e.name));
40
+ } catch {
41
+ return [];
42
+ }
43
+ }
44
+ async function findConversationFile(claudeDir, sessionId) {
45
+ const projectDirs = await listProjectDirs(claudeDir);
46
+ for (const dir of projectDirs) {
47
+ const files2 = await readdir(dir);
48
+ const match = files2.find((f) => f.includes(sessionId) && f.endsWith('.jsonl'));
49
+ if (match) return join(dir, match);
50
+ }
51
+ return null;
52
+ }
53
+ async function readSessionMeta(path) {
54
+ try {
55
+ return await readJsonFile(path);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ async function* readHistory(claudeDir) {
61
+ const historyPath = join(claudeDir, 'history.jsonl');
62
+ try {
63
+ for await (const entry of readJsonlFile(historyPath)) {
64
+ yield entry;
65
+ }
66
+ } catch {}
67
+ }
68
+
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);
83
+ }
84
+ process.stdout.write(table.toString() + '\n');
85
+ }
86
+ function writeError(message) {
87
+ process.stderr.write(
88
+ chalk.red(`error: ${message}
89
+ `),
90
+ );
91
+ }
92
+ function formatTimestamp(ms) {
93
+ return new Date(ms)
94
+ .toISOString()
95
+ .replace('T', ' ')
96
+ .replace(/\.\d+Z$/, '');
97
+ }
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;
116
+ }
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;
123
+ }
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
+ });
132
+ }
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);
143
+ }
144
+ }
145
+
146
+ // src/commands/show.ts
147
+ async function show(sessionId, opts) {
148
+ const file = await findConversationFile(opts.claudeDir, sessionId);
149
+ if (!file) {
150
+ writeError(`Session not found: ${sessionId}`);
151
+ process.exit(1);
152
+ }
153
+ const entries = [];
154
+ for await (const entry of readJsonlFile(file)) {
155
+ entries.push(entry);
156
+ }
157
+ if (opts.raw) {
158
+ for (const entry of entries) {
159
+ process.stdout.write(JSON.stringify(entry) + '\n');
160
+ }
161
+ return;
162
+ }
163
+ writeJson({ sessionId, file, entryCount: entries.length });
164
+ }
165
+
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');
173
+ process.exit(1);
174
+ }
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
+ });
197
+ }
198
+ messageIndex++;
199
+ }
200
+ }
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);
275
+ }
276
+ if (opts.pretty) {
277
+ writeTable(
278
+ ['Path', 'Slug', 'Sessions'],
279
+ results.map((p) => [p.path, p.slug, String(p.sessionCount)]),
280
+ );
281
+ } else {
282
+ writeJson(results);
283
+ }
284
+ }
285
+
286
+ // src/commands/history.ts
287
+ async function history(opts) {
288
+ const entries = [];
289
+ for await (const entry of readHistory(opts.claudeDir)) {
290
+ if (opts.search && !entry.prompt.includes(opts.search)) continue;
291
+ if (opts.project && !entry.project.startsWith(opts.project)) continue;
292
+ if (opts.since && entry.timestamp < new Date(opts.since).getTime()) continue;
293
+ entries.push(entry);
294
+ }
295
+ entries.sort((a, b) => b.timestamp - a.timestamp);
296
+ const limited = entries.slice(0, opts.limit);
297
+ if (opts.pretty) {
298
+ writeTable(
299
+ ['Timestamp', 'Project', 'Prompt'],
300
+ limited.map((e) => [formatTimestamp(e.timestamp), e.project, e.prompt.slice(0, 80)]),
301
+ );
302
+ } else {
303
+ writeJson(limited);
304
+ }
305
+ }
306
+
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
+ });
340
+ }
341
+ }
342
+ messageIndex++;
343
+ }
344
+ if (opts.pretty) {
345
+ writeTable(
346
+ ['File', 'Op', 'Index'],
347
+ operations.map((o) => [o.filePath, o.operation, String(o.messageIndex)]),
348
+ );
349
+ } else {
350
+ writeJson(operations);
351
+ }
352
+ }
353
+
354
+ // src/commands/messages.ts
355
+ async function messages(sessionId, opts) {
356
+ const file = await findConversationFile(opts.claudeDir, sessionId);
357
+ if (!file) {
358
+ writeError(`Session not found: ${sessionId}`);
359
+ process.exit(1);
360
+ }
361
+ let results = [];
362
+ let index = 0;
363
+ for await (const entry of readJsonlFile(file)) {
364
+ const record = entry;
365
+ const type = record.type;
366
+ if (type !== 'user' && type !== 'assistant') {
367
+ index++;
368
+ continue;
369
+ }
370
+ if (opts.user && type !== 'user') {
371
+ index++;
372
+ continue;
373
+ }
374
+ if (opts.assistant && type !== 'assistant') {
375
+ index++;
376
+ continue;
377
+ }
378
+ const content = extractTextContent(record, opts.raw);
379
+ results.push({
380
+ index,
381
+ type,
382
+ timestamp: record.timestamp ?? 0,
383
+ content,
384
+ });
385
+ index++;
386
+ }
387
+ if (opts.slice) {
388
+ const [start, end] = opts.slice.split(':').map(Number);
389
+ results = results.slice(start, end);
390
+ } else if (opts.first) {
391
+ results = results.slice(0, opts.first);
392
+ } else if (opts.last) {
393
+ results = results.slice(-opts.last);
394
+ }
395
+ writeJson(results);
396
+ }
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');
406
+ }
407
+ return '';
408
+ }
409
+
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);
415
+ }
416
+ const file = await findConversationFile(opts.claudeDir, sessionId);
417
+ if (!file) {
418
+ writeError(`Session not found: ${sessionId}`);
419
+ process.exit(1);
420
+ }
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,
434
+ });
435
+ }
436
+ turnIndex++;
437
+ }
438
+ writeJson(turns);
439
+ }
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);
449
+ }
450
+ const file = await findConversationFile(opts.claudeDir, sessionId);
451
+ if (!file) {
452
+ writeError(`Session not found: ${sessionId}`);
453
+ process.exit(1);
454
+ }
455
+ const entries = [];
456
+ for await (const entry of readJsonlFile(file)) {
457
+ entries.push(entry);
458
+ }
459
+ if (opts.stdout) {
460
+ writeJson(entries);
461
+ return;
462
+ }
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}
478
+
479
+ ${content}`;
480
+ })
481
+ .join('\n\n---\n\n');
482
+ await writeFile(outPath, md);
483
+ }
484
+ process.stderr.write(`Exported to ${outPath}
485
+ `);
486
+ }
487
+
488
+ // src/cli.ts
489
+ 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');
501
+ function globals() {
502
+ return program.opts();
503
+ }
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 }));
595
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "claude-session-explorer",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic CLI for extracting structured data from Claude Code session history",
5
+ "type": "module",
6
+ "packageManager": "pnpm@10.20.0",
7
+ "engines": {
8
+ "node": ">=22",
9
+ "pnpm": ">=10"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "bin": {
15
+ "cse": "dist/cli.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsx src/cli.ts",
20
+ "lint": "biome check .",
21
+ "lint:fix": "biome check --write .",
22
+ "format": "biome format --write .",
23
+ "typecheck": "tsc --noEmit",
24
+ "check": "biome ci . && pnpm typecheck",
25
+ "prepare": "simple-git-hooks"
26
+ },
27
+ "simple-git-hooks": {
28
+ "pre-commit": "pnpm exec biome check --staged --no-errors-on-unmatched",
29
+ "pre-push": "pnpm typecheck"
30
+ },
31
+ "license": "MIT",
32
+ "devDependencies": {
33
+ "@biomejs/biome": "^2.4.4",
34
+ "@types/node": "^22.0.0",
35
+ "simple-git-hooks": "^2.12.1",
36
+ "tsup": "^8.5.1",
37
+ "tsx": "^4.21.0",
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "dependencies": {
41
+ "chalk": "^5.6.2",
42
+ "cli-table3": "^0.6.5",
43
+ "commander": "^14.0.3"
44
+ }
45
+ }