@aladac/hu 0.1.0-a1 → 0.1.0-a2

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.
@@ -0,0 +1,877 @@
1
+ /**
2
+ * hu data - Data access commands for Claude Code data
3
+ */
4
+
5
+ import { defineCommand } from 'citty';
6
+ import { getDb, closeDb } from '../lib/db.ts';
7
+ import { initializeSchema } from '../lib/schema.ts';
8
+ import { syncAll, syncIfNeeded } from '../lib/sync.ts';
9
+ import { getConfig, getConfigPath, getDatabasePath, getClaudeDir } from '../lib/config.ts';
10
+ import { getDebugDir } from '../lib/claude-paths.ts';
11
+ import { c } from '../lib/colors.ts';
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+
15
+ // Ensure DB is initialized on first access
16
+ function getInitializedDb() {
17
+ const db = getDb();
18
+ initializeSchema(db);
19
+ return db;
20
+ }
21
+
22
+ // Sync subcommand
23
+ const syncCommand = defineCommand({
24
+ meta: {
25
+ name: 'sync',
26
+ description: 'Sync Claude Code data to local database',
27
+ },
28
+ args: {
29
+ force: {
30
+ type: 'boolean',
31
+ alias: 'f',
32
+ description: 'Force full sync even if recent',
33
+ default: false,
34
+ },
35
+ quiet: {
36
+ type: 'boolean',
37
+ alias: 'q',
38
+ description: 'Suppress output',
39
+ default: false,
40
+ },
41
+ },
42
+ run: ({ args }) => {
43
+ const db = getInitializedDb();
44
+
45
+ if (!args.quiet) {
46
+ console.log(`${c.dim}Syncing Claude Code data...${c.reset}`);
47
+ }
48
+ const result = syncAll(db);
49
+
50
+ if (!args.quiet) {
51
+ console.log(`${c.green}✓${c.reset} Synced:`);
52
+ console.log(` Sessions: ${result.history}`);
53
+ console.log(` Messages: ${result.messages}`);
54
+ console.log(` Todos: ${result.todos}`);
55
+ }
56
+
57
+ closeDb();
58
+ },
59
+ });
60
+
61
+ // Config subcommand
62
+ const configCommand = defineCommand({
63
+ meta: {
64
+ name: 'config',
65
+ description: 'Show current configuration',
66
+ },
67
+ args: {
68
+ json: {
69
+ type: 'boolean',
70
+ alias: 'j',
71
+ description: 'Output as JSON',
72
+ default: false,
73
+ },
74
+ },
75
+ run: ({ args }) => {
76
+ const config = getConfig();
77
+
78
+ if (args.json) {
79
+ console.log(JSON.stringify(config, null, 2));
80
+ return;
81
+ }
82
+
83
+ console.log(`${c.bold}hu Configuration${c.reset}\n`);
84
+ console.log(`${c.dim}Config file:${c.reset} ${getConfigPath()}`);
85
+ console.log(`${c.dim}Database:${c.reset} ${getDatabasePath()}`);
86
+ console.log(`${c.dim}Claude dir:${c.reset} ${getClaudeDir()}`);
87
+ console.log();
88
+ console.log(`${c.bold}[general]${c.reset}`);
89
+ console.log(` claude_dir = "${config.general.claude_dir}"`);
90
+ console.log(` database = "${config.general.database}"`);
91
+ console.log();
92
+ console.log(`${c.bold}[sync]${c.reset}`);
93
+ console.log(` auto_sync_interval = ${config.sync.auto_sync_interval}`);
94
+ console.log(` sync_on_start = ${config.sync.sync_on_start}`);
95
+ console.log();
96
+ console.log(`${c.bold}[hooks]${c.reset}`);
97
+ console.log(` enabled = ${config.hooks.enabled}`);
98
+ console.log(` temp_dir = "${config.hooks.temp_dir}"`);
99
+ console.log(` temp_file_ttl = ${config.hooks.temp_file_ttl}`);
100
+ console.log();
101
+ console.log(`${c.bold}[search]${c.reset}`);
102
+ console.log(` default_limit = ${config.search.default_limit}`);
103
+ console.log(` show_snippets = ${config.search.show_snippets}`);
104
+ console.log();
105
+ console.log(`${c.bold}[output]${c.reset}`);
106
+ console.log(` default_format = "${config.output.default_format}"`);
107
+ console.log(` colors = ${config.output.colors}`);
108
+ console.log(` date_format = "${config.output.date_format}"`);
109
+ },
110
+ });
111
+
112
+ // Session list subcommand
113
+ const sessionListCommand = defineCommand({
114
+ meta: {
115
+ name: 'list',
116
+ description: 'List sessions',
117
+ },
118
+ args: {
119
+ project: {
120
+ type: 'string',
121
+ alias: 'p',
122
+ description: 'Filter by project path',
123
+ },
124
+ limit: {
125
+ type: 'string',
126
+ alias: 'n',
127
+ description: 'Number of sessions to show',
128
+ default: '20',
129
+ },
130
+ json: {
131
+ type: 'boolean',
132
+ alias: 'j',
133
+ description: 'Output as JSON',
134
+ default: false,
135
+ },
136
+ output: {
137
+ type: 'string',
138
+ alias: 'o',
139
+ description: 'Write output to file',
140
+ },
141
+ },
142
+ run: ({ args }) => {
143
+ const db = getInitializedDb();
144
+ syncIfNeeded(db);
145
+
146
+ let query = 'SELECT * FROM sessions';
147
+ const params: unknown[] = [];
148
+
149
+ if (args.project) {
150
+ query += ' WHERE project LIKE ?';
151
+ params.push(`%${args.project}%`);
152
+ }
153
+
154
+ query += ' ORDER BY started_at DESC LIMIT ?';
155
+ params.push(parseInt(args.limit, 10));
156
+
157
+ const sessions = db.prepare(query).all(...params) as Array<{
158
+ id: string;
159
+ project: string;
160
+ display: string;
161
+ started_at: number;
162
+ message_count: number;
163
+ total_cost_usd: number;
164
+ }>;
165
+
166
+ if (args.json || args.output) {
167
+ const output = JSON.stringify(sessions, null, 2);
168
+ if (args.output) {
169
+ fs.writeFileSync(args.output, output);
170
+ console.log(`Written to ${args.output}`);
171
+ } else {
172
+ console.log(output);
173
+ }
174
+ } else {
175
+ console.log(`${c.bold}Sessions${c.reset} (${sessions.length})\n`);
176
+ for (const session of sessions) {
177
+ const date = new Date(session.started_at).toLocaleString();
178
+ const preview = session.display?.slice(0, 60) || '(no preview)';
179
+ console.log(`${c.cyan}${session.id.slice(0, 8)}${c.reset} ${c.dim}${date}${c.reset}`);
180
+ console.log(` ${c.dim}Project:${c.reset} ${session.project}`);
181
+ console.log(` ${c.dim}Messages:${c.reset} ${session.message_count} ${c.dim}Cost:${c.reset} $${(session.total_cost_usd || 0).toFixed(4)}`);
182
+ console.log(` ${preview}${session.display?.length > 60 ? '...' : ''}`);
183
+ console.log();
184
+ }
185
+ }
186
+
187
+ closeDb();
188
+ },
189
+ });
190
+
191
+ // Session read subcommand
192
+ const sessionReadCommand = defineCommand({
193
+ meta: {
194
+ name: 'read',
195
+ description: 'Read session transcript',
196
+ },
197
+ args: {
198
+ id: {
199
+ type: 'positional',
200
+ description: 'Session ID (or prefix)',
201
+ required: true,
202
+ },
203
+ json: {
204
+ type: 'boolean',
205
+ alias: 'j',
206
+ description: 'Output as JSON',
207
+ default: false,
208
+ },
209
+ output: {
210
+ type: 'string',
211
+ alias: 'o',
212
+ description: 'Write output to file',
213
+ },
214
+ },
215
+ run: ({ args }) => {
216
+ const db = getInitializedDb();
217
+ syncIfNeeded(db);
218
+
219
+ // Find session by ID or prefix
220
+ const session = db.prepare(
221
+ 'SELECT * FROM sessions WHERE id LIKE ? ORDER BY started_at DESC LIMIT 1'
222
+ ).get(`${args.id}%`) as { id: string; project: string; started_at: number } | undefined;
223
+
224
+ if (!session) {
225
+ console.error(`Session not found: ${args.id}`);
226
+ process.exit(1);
227
+ }
228
+
229
+ const messages = db.prepare(
230
+ 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC'
231
+ ).all(session.id) as Array<{
232
+ id: string;
233
+ role: string;
234
+ content: string;
235
+ model: string;
236
+ created_at: number;
237
+ }>;
238
+
239
+ if (args.json || args.output) {
240
+ const output = JSON.stringify({ session, messages }, null, 2);
241
+ if (args.output) {
242
+
243
+ fs.writeFileSync(args.output, output);
244
+ console.log(`Written to ${args.output}`);
245
+ } else {
246
+ console.log(output);
247
+ }
248
+ } else {
249
+ console.log(`${c.bold}Session: ${session.id}${c.reset}`);
250
+ console.log(`${c.dim}Project:${c.reset} ${session.project}`);
251
+ console.log(`${c.dim}Started:${c.reset} ${new Date(session.started_at).toLocaleString()}`);
252
+ console.log(`${c.dim}Messages:${c.reset} ${messages.length}\n`);
253
+ console.log('─'.repeat(60) + '\n');
254
+
255
+ for (const msg of messages) {
256
+ const roleColor = msg.role === 'user' ? c.green : c.blue;
257
+ console.log(`${roleColor}[${msg.role}]${c.reset} ${c.dim}${msg.model || ''}${c.reset}`);
258
+ console.log(msg.content.slice(0, 500) + (msg.content.length > 500 ? '...' : ''));
259
+ console.log();
260
+ }
261
+ }
262
+
263
+ closeDb();
264
+ },
265
+ });
266
+
267
+ // Session current subcommand
268
+ const sessionCurrentCommand = defineCommand({
269
+ meta: {
270
+ name: 'current',
271
+ description: 'Show current session (from $SESSION_ID env)',
272
+ },
273
+ args: {
274
+ json: {
275
+ type: 'boolean',
276
+ alias: 'j',
277
+ description: 'Output as JSON',
278
+ default: false,
279
+ },
280
+ output: {
281
+ type: 'string',
282
+ alias: 'o',
283
+ description: 'Write output to file',
284
+ },
285
+ },
286
+ run: ({ args }) => {
287
+ const sessionId = process.env.SESSION_ID;
288
+
289
+ if (!sessionId) {
290
+ console.error('No current session. SESSION_ID environment variable not set.');
291
+ console.error('This command is designed to be used within Claude Code hooks.');
292
+ process.exit(1);
293
+ }
294
+
295
+ const db = getInitializedDb();
296
+ syncIfNeeded(db);
297
+
298
+ const session = db.prepare(
299
+ 'SELECT * FROM sessions WHERE id = ?'
300
+ ).get(sessionId) as { id: string; project: string; started_at: number } | undefined;
301
+
302
+ if (!session) {
303
+ console.error(`Session not found: ${sessionId}`);
304
+ process.exit(1);
305
+ }
306
+
307
+ const messages = db.prepare(
308
+ 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC'
309
+ ).all(session.id) as Array<{
310
+ id: string;
311
+ role: string;
312
+ content: string;
313
+ model: string;
314
+ created_at: number;
315
+ }>;
316
+
317
+ if (args.json || args.output) {
318
+ const output = JSON.stringify({ session, messages }, null, 2);
319
+ if (args.output) {
320
+
321
+ fs.writeFileSync(args.output, output);
322
+ console.log(`Written to ${args.output}`);
323
+ } else {
324
+ console.log(output);
325
+ }
326
+ } else {
327
+ console.log(`${c.bold}Current Session: ${session.id}${c.reset}`);
328
+ console.log(`${c.dim}Project:${c.reset} ${session.project}`);
329
+ console.log(`${c.dim}Started:${c.reset} ${new Date(session.started_at).toLocaleString()}`);
330
+ console.log(`${c.dim}Messages:${c.reset} ${messages.length}`);
331
+ }
332
+
333
+ closeDb();
334
+ },
335
+ });
336
+
337
+ // Session subcommand group
338
+ const sessionCommand = defineCommand({
339
+ meta: {
340
+ name: 'session',
341
+ description: 'Session management commands',
342
+ },
343
+ subCommands: {
344
+ list: sessionListCommand,
345
+ read: sessionReadCommand,
346
+ current: sessionCurrentCommand,
347
+ },
348
+ });
349
+
350
+ // Stats subcommand
351
+ const statsCommand = defineCommand({
352
+ meta: {
353
+ name: 'stats',
354
+ description: 'Show usage statistics',
355
+ },
356
+ args: {
357
+ json: {
358
+ type: 'boolean',
359
+ alias: 'j',
360
+ description: 'Output as JSON',
361
+ default: false,
362
+ },
363
+ today: {
364
+ type: 'boolean',
365
+ alias: 't',
366
+ description: 'Show today\'s stats only',
367
+ default: false,
368
+ },
369
+ },
370
+ run: ({ args }) => {
371
+ const db = getInitializedDb();
372
+ syncIfNeeded(db);
373
+
374
+ let whereClause = '';
375
+ if (args.today) {
376
+ const todayStart = new Date();
377
+ todayStart.setHours(0, 0, 0, 0);
378
+ whereClause = ` WHERE created_at >= ${todayStart.getTime()}`;
379
+ }
380
+
381
+ const stats = {
382
+ totalSessions: (db.prepare(`SELECT COUNT(*) as count FROM sessions`).get() as { count: number }).count,
383
+ totalMessages: (db.prepare(`SELECT COUNT(*) as count FROM messages${whereClause}`).get() as { count: number }).count,
384
+ totalCost: (db.prepare(`SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages${whereClause}`).get() as { total: number }).total,
385
+ totalInputTokens: (db.prepare(`SELECT COALESCE(SUM(input_tokens), 0) as total FROM messages${whereClause}`).get() as { total: number }).total,
386
+ totalOutputTokens: (db.prepare(`SELECT COALESCE(SUM(output_tokens), 0) as total FROM messages${whereClause}`).get() as { total: number }).total,
387
+ modelUsage: db.prepare(`
388
+ SELECT model, COUNT(*) as count, SUM(cost_usd) as cost
389
+ FROM messages
390
+ WHERE model IS NOT NULL ${args.today ? `AND created_at >= ${new Date().setHours(0,0,0,0)}` : ''}
391
+ GROUP BY model
392
+ ORDER BY count DESC
393
+ `).all() as Array<{ model: string; count: number; cost: number }>,
394
+ };
395
+
396
+ if (args.json) {
397
+ console.log(JSON.stringify(stats, null, 2));
398
+ } else {
399
+ console.log(`${c.bold}Usage Statistics${args.today ? ' (Today)' : ''}${c.reset}\n`);
400
+ console.log(`${c.dim}Sessions:${c.reset} ${stats.totalSessions}`);
401
+ console.log(`${c.dim}Messages:${c.reset} ${stats.totalMessages}`);
402
+ console.log(`${c.dim}Total Cost:${c.reset} $${stats.totalCost.toFixed(4)}`);
403
+ console.log(`${c.dim}Input Tokens:${c.reset} ${stats.totalInputTokens.toLocaleString()}`);
404
+ console.log(`${c.dim}Output Tokens:${c.reset} ${stats.totalOutputTokens.toLocaleString()}`);
405
+
406
+ if (stats.modelUsage.length > 0) {
407
+ console.log(`\n${c.bold}By Model${c.reset}`);
408
+ for (const model of stats.modelUsage) {
409
+ console.log(` ${model.model}: ${model.count} messages, $${(model.cost || 0).toFixed(4)}`);
410
+ }
411
+ }
412
+ }
413
+
414
+ closeDb();
415
+ },
416
+ });
417
+
418
+ // Todos list subcommand
419
+ const todosListCommand = defineCommand({
420
+ meta: {
421
+ name: 'list',
422
+ description: 'List todos',
423
+ },
424
+ args: {
425
+ status: {
426
+ type: 'string',
427
+ alias: 's',
428
+ description: 'Filter by status (pending, in_progress, completed)',
429
+ },
430
+ json: {
431
+ type: 'boolean',
432
+ alias: 'j',
433
+ description: 'Output as JSON',
434
+ default: false,
435
+ },
436
+ output: {
437
+ type: 'string',
438
+ alias: 'o',
439
+ description: 'Write output to file',
440
+ },
441
+ },
442
+ run: ({ args }) => {
443
+ const db = getInitializedDb();
444
+ syncIfNeeded(db);
445
+
446
+ let query = 'SELECT * FROM todos';
447
+ const params: unknown[] = [];
448
+
449
+ if (args.status) {
450
+ query += ' WHERE status = ?';
451
+ params.push(args.status);
452
+ }
453
+
454
+ query += ' ORDER BY id DESC';
455
+
456
+ const todos = db.prepare(query).all(...params) as Array<{
457
+ id: number;
458
+ session_id: string;
459
+ content: string;
460
+ status: string;
461
+ active_form: string;
462
+ }>;
463
+
464
+ if (args.json || args.output) {
465
+ const output = JSON.stringify(todos, null, 2);
466
+ if (args.output) {
467
+
468
+ fs.writeFileSync(args.output, output);
469
+ console.log(`Written to ${args.output}`);
470
+ } else {
471
+ console.log(output);
472
+ }
473
+ } else {
474
+ console.log(`${c.bold}Todos${c.reset} (${todos.length})\n`);
475
+ for (const todo of todos) {
476
+ const statusColor =
477
+ todo.status === 'completed' ? c.green :
478
+ todo.status === 'in_progress' ? c.yellow : c.dim;
479
+ console.log(`${statusColor}[${todo.status}]${c.reset} ${todo.content}`);
480
+ console.log(` ${c.dim}Session: ${todo.session_id.slice(0, 8)}${c.reset}`);
481
+ }
482
+ }
483
+
484
+ closeDb();
485
+ },
486
+ });
487
+
488
+ // Todos pending subcommand
489
+ const todosPendingCommand = defineCommand({
490
+ meta: {
491
+ name: 'pending',
492
+ description: 'List pending todos',
493
+ },
494
+ args: {
495
+ project: {
496
+ type: 'string',
497
+ alias: 'p',
498
+ description: 'Filter by project path',
499
+ },
500
+ json: {
501
+ type: 'boolean',
502
+ alias: 'j',
503
+ description: 'Output as JSON',
504
+ default: false,
505
+ },
506
+ output: {
507
+ type: 'string',
508
+ alias: 'o',
509
+ description: 'Write output to file',
510
+ },
511
+ },
512
+ run: ({ args }) => {
513
+ const db = getInitializedDb();
514
+ syncIfNeeded(db);
515
+
516
+ let query = `
517
+ SELECT t.*, s.project
518
+ FROM todos t
519
+ JOIN sessions s ON t.session_id = s.id
520
+ WHERE t.status != 'completed'
521
+ `;
522
+ const params: unknown[] = [];
523
+
524
+ if (args.project) {
525
+ query += ' AND s.project LIKE ?';
526
+ params.push(`%${args.project}%`);
527
+ }
528
+
529
+ query += ' ORDER BY t.id DESC';
530
+
531
+ const todos = db.prepare(query).all(...params) as Array<{
532
+ id: number;
533
+ session_id: string;
534
+ content: string;
535
+ status: string;
536
+ project: string;
537
+ }>;
538
+
539
+ if (args.json || args.output) {
540
+ const output = JSON.stringify(todos, null, 2);
541
+ if (args.output) {
542
+
543
+ fs.writeFileSync(args.output, output);
544
+ console.log(`Written to ${args.output}`);
545
+ } else {
546
+ console.log(output);
547
+ }
548
+ } else {
549
+ console.log(`${c.bold}Pending Todos${c.reset} (${todos.length})\n`);
550
+
551
+ // Group by project
552
+ const byProject = new Map<string, typeof todos>();
553
+ for (const todo of todos) {
554
+ const list = byProject.get(todo.project) || [];
555
+ list.push(todo);
556
+ byProject.set(todo.project, list);
557
+ }
558
+
559
+ for (const [project, projectTodos] of byProject) {
560
+ console.log(`${c.cyan}${project}${c.reset}`);
561
+ for (const todo of projectTodos) {
562
+ const statusColor = todo.status === 'in_progress' ? c.yellow : c.dim;
563
+ console.log(` ${statusColor}[${todo.status}]${c.reset} ${todo.content}`);
564
+ }
565
+ console.log();
566
+ }
567
+ }
568
+
569
+ closeDb();
570
+ },
571
+ });
572
+
573
+ // Todos subcommand group
574
+ const todosCommand = defineCommand({
575
+ meta: {
576
+ name: 'todos',
577
+ description: 'Todo management commands',
578
+ },
579
+ subCommands: {
580
+ list: todosListCommand,
581
+ pending: todosPendingCommand,
582
+ },
583
+ });
584
+
585
+ // Search subcommand
586
+ const searchCommand = defineCommand({
587
+ meta: {
588
+ name: 'search',
589
+ description: 'Search messages',
590
+ },
591
+ args: {
592
+ query: {
593
+ type: 'positional',
594
+ description: 'Search query',
595
+ required: true,
596
+ },
597
+ limit: {
598
+ type: 'string',
599
+ alias: 'n',
600
+ description: 'Number of results',
601
+ default: '20',
602
+ },
603
+ json: {
604
+ type: 'boolean',
605
+ alias: 'j',
606
+ description: 'Output as JSON',
607
+ default: false,
608
+ },
609
+ output: {
610
+ type: 'string',
611
+ alias: 'o',
612
+ description: 'Write output to file',
613
+ },
614
+ },
615
+ run: ({ args }) => {
616
+ const db = getInitializedDb();
617
+ syncIfNeeded(db);
618
+
619
+ const results = db.prepare(`
620
+ SELECT m.*, s.project
621
+ FROM messages m
622
+ JOIN sessions s ON m.session_id = s.id
623
+ WHERE m.content LIKE ?
624
+ ORDER BY m.created_at DESC
625
+ LIMIT ?
626
+ `).all(`%${args.query}%`, parseInt(args.limit, 10)) as Array<{
627
+ id: string;
628
+ session_id: string;
629
+ role: string;
630
+ content: string;
631
+ project: string;
632
+ created_at: number;
633
+ }>;
634
+
635
+ if (args.json || args.output) {
636
+ const output = JSON.stringify(results, null, 2);
637
+ if (args.output) {
638
+
639
+ fs.writeFileSync(args.output, output);
640
+ console.log(`Written to ${args.output}`);
641
+ } else {
642
+ console.log(output);
643
+ }
644
+ } else {
645
+ console.log(`${c.bold}Search Results for "${args.query}"${c.reset} (${results.length})\n`);
646
+
647
+ for (const result of results) {
648
+ const date = new Date(result.created_at).toLocaleString();
649
+ const roleColor = result.role === 'user' ? c.green : c.blue;
650
+
651
+ console.log(`${roleColor}[${result.role}]${c.reset} ${c.dim}${date}${c.reset}`);
652
+ console.log(`${c.dim}Project:${c.reset} ${result.project}`);
653
+
654
+ // Show snippet around match
655
+ const content = result.content;
656
+ const matchIndex = content.toLowerCase().indexOf(args.query.toLowerCase());
657
+ if (matchIndex >= 0) {
658
+ const start = Math.max(0, matchIndex - 50);
659
+ const end = Math.min(content.length, matchIndex + args.query.length + 50);
660
+ const snippet = (start > 0 ? '...' : '') +
661
+ content.slice(start, end) +
662
+ (end < content.length ? '...' : '');
663
+ console.log(snippet);
664
+ } else {
665
+ console.log(content.slice(0, 100) + (content.length > 100 ? '...' : ''));
666
+ }
667
+ console.log();
668
+ }
669
+ }
670
+
671
+ closeDb();
672
+ },
673
+ });
674
+
675
+ // Tools subcommand
676
+ const toolsCommand = defineCommand({
677
+ meta: {
678
+ name: 'tools',
679
+ description: 'Show tool usage statistics',
680
+ },
681
+ args: {
682
+ tool: {
683
+ type: 'string',
684
+ alias: 't',
685
+ description: 'Filter by tool name',
686
+ },
687
+ json: {
688
+ type: 'boolean',
689
+ alias: 'j',
690
+ description: 'Output as JSON',
691
+ default: false,
692
+ },
693
+ },
694
+ run: ({ args }) => {
695
+ const db = getInitializedDb();
696
+ syncIfNeeded(db);
697
+
698
+ if (args.tool) {
699
+ // Show details for specific tool
700
+ const usage = db.prepare(`
701
+ SELECT tu.*, s.project
702
+ FROM tool_usage tu
703
+ JOIN sessions s ON tu.session_id = s.id
704
+ WHERE tu.tool_name = ?
705
+ ORDER BY tu.created_at DESC
706
+ LIMIT 20
707
+ `).all(args.tool) as Array<{
708
+ tool_name: string;
709
+ session_id: string;
710
+ project: string;
711
+ created_at: number;
712
+ }>;
713
+
714
+ if (args.json) {
715
+ console.log(JSON.stringify(usage, null, 2));
716
+ } else {
717
+ console.log(`${c.bold}Tool: ${args.tool}${c.reset} (${usage.length} recent uses)\n`);
718
+ for (const u of usage) {
719
+ const date = new Date(u.created_at).toLocaleString();
720
+ console.log(`${c.dim}${date}${c.reset} ${u.project}`);
721
+ }
722
+ }
723
+ } else {
724
+ // Show aggregate stats
725
+ const stats = db.prepare(`
726
+ SELECT tool_name, COUNT(*) as count, MAX(created_at) as last_used
727
+ FROM tool_usage
728
+ GROUP BY tool_name
729
+ ORDER BY count DESC
730
+ `).all() as Array<{ tool_name: string; count: number; last_used: number }>;
731
+
732
+ if (args.json) {
733
+ console.log(JSON.stringify(stats, null, 2));
734
+ } else {
735
+ console.log(`${c.bold}Tool Usage${c.reset}\n`);
736
+ for (const stat of stats) {
737
+ const lastUsed = new Date(stat.last_used).toLocaleDateString();
738
+ console.log(`${c.cyan}${stat.tool_name}${c.reset}: ${stat.count} uses (last: ${lastUsed})`);
739
+ }
740
+ }
741
+ }
742
+
743
+ closeDb();
744
+ },
745
+ });
746
+
747
+ // Errors subcommand
748
+ const errorsCommand = defineCommand({
749
+ meta: {
750
+ name: 'errors',
751
+ description: 'Extract errors from debug logs',
752
+ },
753
+ args: {
754
+ recent: {
755
+ type: 'string',
756
+ alias: 'r',
757
+ description: 'Show errors from last N days',
758
+ default: '7',
759
+ },
760
+ json: {
761
+ type: 'boolean',
762
+ alias: 'j',
763
+ description: 'Output as JSON',
764
+ default: false,
765
+ },
766
+ output: {
767
+ type: 'string',
768
+ alias: 'o',
769
+ description: 'Write output to file',
770
+ },
771
+ },
772
+ run: ({ args }) => {
773
+ const debugDir = getDebugDir();
774
+
775
+ if (!fs.existsSync(debugDir)) {
776
+ console.log('No debug logs found.');
777
+ return;
778
+ }
779
+
780
+ const days = parseInt(args.recent, 10);
781
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
782
+
783
+ const errorPatterns = [
784
+ /error/i,
785
+ /failed/i,
786
+ /exception/i,
787
+ /warning/i,
788
+ /ENOENT/i,
789
+ /EACCES/i,
790
+ /EPERM/i,
791
+ ];
792
+
793
+ const errors: Array<{ file: string; line: number; content: string; timestamp: number }> = [];
794
+
795
+ const logFiles = fs.readdirSync(debugDir)
796
+ .filter(f => f.endsWith('.txt'))
797
+ .map(f => ({
798
+ name: f,
799
+ path: path.join(debugDir, f),
800
+ stat: fs.statSync(path.join(debugDir, f)),
801
+ }))
802
+ .filter(f => f.stat.mtimeMs > cutoff);
803
+
804
+ for (const logFile of logFiles) {
805
+ try {
806
+ const content = fs.readFileSync(logFile.path, 'utf-8');
807
+ const lines = content.split('\n');
808
+
809
+ lines.forEach((line, idx) => {
810
+ if (errorPatterns.some(p => p.test(line))) {
811
+ errors.push({
812
+ file: logFile.name,
813
+ line: idx + 1,
814
+ content: line.trim(),
815
+ timestamp: logFile.stat.mtimeMs,
816
+ });
817
+ }
818
+ });
819
+ } catch {
820
+ // Skip files that can't be read
821
+ }
822
+ }
823
+
824
+ // Sort by timestamp descending
825
+ errors.sort((a, b) => b.timestamp - a.timestamp);
826
+
827
+ if (args.json || args.output) {
828
+ const output = JSON.stringify(errors, null, 2);
829
+ if (args.output) {
830
+ fs.writeFileSync(args.output, output);
831
+ console.log(`Written to ${args.output}`);
832
+ } else {
833
+ console.log(output);
834
+ }
835
+ } else {
836
+ console.log(`${c.bold}Errors from Debug Logs${c.reset} (last ${days} days)\n`);
837
+ console.log(`Found ${errors.length} error/warning entries in ${logFiles.length} log files\n`);
838
+
839
+ // Show unique errors (dedupe by content)
840
+ const seen = new Set<string>();
841
+ let shown = 0;
842
+
843
+ for (const err of errors) {
844
+ if (shown >= 50) break;
845
+ if (seen.has(err.content)) continue;
846
+ seen.add(err.content);
847
+
848
+ console.log(`${c.dim}${err.file}:${err.line}${c.reset}`);
849
+ console.log(` ${c.red}${err.content.slice(0, 200)}${err.content.length > 200 ? '...' : ''}${c.reset}`);
850
+ console.log();
851
+ shown++;
852
+ }
853
+
854
+ if (errors.length > shown) {
855
+ console.log(`${c.dim}... and ${errors.length - shown} more (use --json for full output)${c.reset}`);
856
+ }
857
+ }
858
+ },
859
+ });
860
+
861
+ // Main data command
862
+ export const dataCommand = defineCommand({
863
+ meta: {
864
+ name: 'data',
865
+ description: 'Access Claude Code session data',
866
+ },
867
+ subCommands: {
868
+ sync: syncCommand,
869
+ config: configCommand,
870
+ session: sessionCommand,
871
+ stats: statsCommand,
872
+ todos: todosCommand,
873
+ search: searchCommand,
874
+ tools: toolsCommand,
875
+ errors: errorsCommand,
876
+ },
877
+ });