@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.
- package/CLAUDE.md +54 -29
- package/HOOKS.md +146 -0
- package/commands/reinstall.md +6 -3
- package/hooks/session-start.sh +85 -0
- package/hooks/stop.sh +51 -0
- package/hooks/user-prompt-submit.sh +74 -0
- package/package.json +5 -2
- package/plans/gleaming-crunching-bear.md +179 -0
- package/src/commands/data.ts +877 -0
- package/src/commands/plugin.ts +216 -0
- package/src/index.ts +5 -1
- package/src/lib/claude-paths.ts +136 -0
- package/src/lib/config.ts +244 -0
- package/src/lib/db.ts +59 -0
- package/src/lib/hook-io.ts +128 -0
- package/src/lib/jsonl.ts +95 -0
- package/src/lib/schema.ts +164 -0
- package/src/lib/sync.ts +300 -0
- package/tests/lib/claude-paths.test.ts +73 -0
- package/tests/lib/config.test.ts +163 -0
- package/tests/lib/db.test.ts +230 -0
- package/tests/lib/escaping.test.ts +257 -0
- package/tests/lib/hook-io.test.ts +151 -0
- package/tests/lib/jsonl.test.ts +166 -0
- package/HOOKS-DATA-INTEGRATION.md +0 -457
- package/SAMPLE.md +0 -378
- package/TODO.md +0 -25
|
@@ -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
|
+
});
|