cc-query 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/package.json +2 -2
- package/src/query-session.js +114 -4
- package/src/repl.js +25 -2
- package/src/session-loader.js +14 -4
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# cc-query
|
|
2
|
+
|
|
3
|
+
SQL REPL for querying Claude Code session data using DuckDB.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g cc-query
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 24+.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Query all projects
|
|
17
|
+
cc-query
|
|
18
|
+
|
|
19
|
+
# Query a specific project
|
|
20
|
+
cc-query ~/code/my-project
|
|
21
|
+
|
|
22
|
+
# Filter by session ID prefix
|
|
23
|
+
cc-query -s abc123 .
|
|
24
|
+
|
|
25
|
+
# Pipe queries (like psql)
|
|
26
|
+
echo "SELECT count(*) FROM messages;" | cc-query .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Available Views
|
|
30
|
+
|
|
31
|
+
- `messages` - All messages with parsed fields
|
|
32
|
+
- `user_messages` - User messages only
|
|
33
|
+
- `assistant_messages` - Assistant responses only
|
|
34
|
+
- `tool_calls` - Tool invocations from assistant messages
|
|
35
|
+
- `raw_messages` - Unparsed JSONL data
|
|
36
|
+
|
|
37
|
+
## REPL Commands
|
|
38
|
+
|
|
39
|
+
- `.help` - Show tables and example queries
|
|
40
|
+
- `.schema` - Show table schema
|
|
41
|
+
- `.quit` - Exit
|
|
42
|
+
|
|
43
|
+
## Skill (experimental)
|
|
44
|
+
|
|
45
|
+
This [skill](https://gist.github.com/dannycoates/b4436fb77c9cfd2763028eee42d1d320) gives claude the ability and slash command `/reflect` to work with claude session history.
|
|
46
|
+
|
|
47
|
+
For example you can ask questions like:
|
|
48
|
+
- Across all projects what bash commands return the most errors?
|
|
49
|
+
- Let's analyze the last session and identify how we might improve the claude.md file
|
|
50
|
+
- Gimme a summary of what we worked on this past week
|
|
51
|
+
- Let's go though our whole session history and identify repeated patterns that we could extract into skills
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "SQL REPL for querying Claude Code session data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"node": ">=24"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"cc-query": "
|
|
18
|
+
"cc-query": "bin/cc-query.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"index.js",
|
package/src/query-session.js
CHANGED
|
@@ -91,13 +91,13 @@ function formatResults(result) {
|
|
|
91
91
|
export class QuerySession {
|
|
92
92
|
/** @type {import("@duckdb/node-api").DuckDBConnection | undefined} */
|
|
93
93
|
#connection;
|
|
94
|
-
/** @type {string} */
|
|
94
|
+
/** @type {string | string[]} */
|
|
95
95
|
#filePattern;
|
|
96
96
|
/** @type {QuerySessionInfo} */
|
|
97
97
|
#info;
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* @param {string} filePattern - Glob pattern for JSONL files
|
|
100
|
+
* @param {string | string[]} filePattern - Glob pattern(s) for JSONL files
|
|
101
101
|
* @param {QuerySessionInfo} info - Session counts
|
|
102
102
|
*/
|
|
103
103
|
constructor(filePattern, info) {
|
|
@@ -105,6 +105,19 @@ export class QuerySession {
|
|
|
105
105
|
this.#info = info;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Format file pattern for use in DuckDB SQL
|
|
110
|
+
* @returns {string} SQL expression for the file pattern
|
|
111
|
+
*/
|
|
112
|
+
#formatFilePatternForSql() {
|
|
113
|
+
if (Array.isArray(this.#filePattern)) {
|
|
114
|
+
// DuckDB accepts a list of patterns: ['pattern1', 'pattern2']
|
|
115
|
+
const patterns = this.#filePattern.map((p) => `'${p}'`).join(", ");
|
|
116
|
+
return `[${patterns}]`;
|
|
117
|
+
}
|
|
118
|
+
return `'${this.#filePattern}'`;
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
/**
|
|
109
122
|
* Create a QuerySession from a project path
|
|
110
123
|
* @param {string | null} projectDir - Claude projects dir, or null for all
|
|
@@ -290,7 +303,7 @@ export class QuerySession {
|
|
|
290
303
|
regexp_extract(filename, '/projects/([^/]+)/', 1) as project,
|
|
291
304
|
ordinality as rownum
|
|
292
305
|
FROM read_ndjson(
|
|
293
|
-
|
|
306
|
+
${this.#formatFilePatternForSql()},
|
|
294
307
|
filename=true,
|
|
295
308
|
ignore_errors=true,
|
|
296
309
|
columns={${columnsDef}}
|
|
@@ -342,8 +355,105 @@ export class QuerySession {
|
|
|
342
355
|
SELECT
|
|
343
356
|
(json->>'uuid')::UUID as uuid,
|
|
344
357
|
json as raw
|
|
345
|
-
FROM read_ndjson_objects(
|
|
358
|
+
FROM read_ndjson_objects(${this.#formatFilePatternForSql()}, ignore_errors=true)
|
|
346
359
|
WHERE json->>'uuid' IS NOT NULL AND length(json->>'uuid') > 0;
|
|
360
|
+
|
|
361
|
+
-- Tool uses: All tool calls with unnested content blocks
|
|
362
|
+
CREATE OR REPLACE VIEW tool_uses AS
|
|
363
|
+
SELECT
|
|
364
|
+
m.uuid,
|
|
365
|
+
m.timestamp,
|
|
366
|
+
m.sessionId,
|
|
367
|
+
m.isAgent,
|
|
368
|
+
m.agentId,
|
|
369
|
+
m.project,
|
|
370
|
+
m.rownum,
|
|
371
|
+
block->>'name' as tool_name,
|
|
372
|
+
block->>'id' as tool_id,
|
|
373
|
+
block->'input' as tool_input,
|
|
374
|
+
row_number() OVER (PARTITION BY m.uuid ORDER BY (SELECT NULL)) - 1 as block_index
|
|
375
|
+
FROM assistant_messages m,
|
|
376
|
+
LATERAL UNNEST(CAST(message->'content' AS JSON[])) as t(block)
|
|
377
|
+
WHERE block->>'type' = 'tool_use';
|
|
378
|
+
|
|
379
|
+
-- Tool results: All tool results with duration
|
|
380
|
+
CREATE OR REPLACE VIEW tool_results AS
|
|
381
|
+
WITH array_messages AS (
|
|
382
|
+
SELECT * FROM user_messages
|
|
383
|
+
WHERE json_type(message->'content') = 'ARRAY'
|
|
384
|
+
)
|
|
385
|
+
SELECT
|
|
386
|
+
m.uuid,
|
|
387
|
+
m.timestamp,
|
|
388
|
+
m.sessionId,
|
|
389
|
+
m.isAgent,
|
|
390
|
+
m.agentId,
|
|
391
|
+
m.project,
|
|
392
|
+
m.rownum,
|
|
393
|
+
block->>'tool_use_id' as tool_use_id,
|
|
394
|
+
CAST(block->>'is_error' AS BOOLEAN) as is_error,
|
|
395
|
+
block->>'content' as result_content,
|
|
396
|
+
CAST(m.toolUseResult->>'durationMs' AS INTEGER) as duration_ms,
|
|
397
|
+
m.sourceToolAssistantUUID
|
|
398
|
+
FROM array_messages m,
|
|
399
|
+
LATERAL UNNEST(CAST(message->'content' AS JSON[])) as t(block)
|
|
400
|
+
WHERE block->>'type' = 'tool_result';
|
|
401
|
+
|
|
402
|
+
-- Token usage: Pre-cast token counts
|
|
403
|
+
CREATE OR REPLACE VIEW token_usage AS
|
|
404
|
+
SELECT
|
|
405
|
+
uuid,
|
|
406
|
+
timestamp,
|
|
407
|
+
sessionId,
|
|
408
|
+
isAgent,
|
|
409
|
+
agentId,
|
|
410
|
+
project,
|
|
411
|
+
message->>'model' as model,
|
|
412
|
+
message->>'stop_reason' as stop_reason,
|
|
413
|
+
CAST(message->'usage'->>'input_tokens' AS BIGINT) as input_tokens,
|
|
414
|
+
CAST(message->'usage'->>'output_tokens' AS BIGINT) as output_tokens,
|
|
415
|
+
CAST(message->'usage'->>'cache_read_input_tokens' AS BIGINT) as cache_read_tokens,
|
|
416
|
+
CAST(message->'usage'->>'cache_creation_input_tokens' AS BIGINT) as cache_creation_tokens
|
|
417
|
+
FROM assistant_messages
|
|
418
|
+
WHERE (message->'usage') IS NOT NULL;
|
|
419
|
+
|
|
420
|
+
-- Bash commands: Bash tool uses with extracted command
|
|
421
|
+
CREATE OR REPLACE VIEW bash_commands AS
|
|
422
|
+
SELECT
|
|
423
|
+
uuid,
|
|
424
|
+
timestamp,
|
|
425
|
+
sessionId,
|
|
426
|
+
isAgent,
|
|
427
|
+
agentId,
|
|
428
|
+
project,
|
|
429
|
+
rownum,
|
|
430
|
+
tool_id,
|
|
431
|
+
tool_input->>'command' as command,
|
|
432
|
+
tool_input->>'description' as description,
|
|
433
|
+
CAST(tool_input->>'timeout' AS INTEGER) as timeout,
|
|
434
|
+
CAST(tool_input->>'run_in_background' AS BOOLEAN) as run_in_background
|
|
435
|
+
FROM tool_uses
|
|
436
|
+
WHERE tool_name = 'Bash';
|
|
437
|
+
|
|
438
|
+
-- File operations: Read/Write/Edit/Glob/Grep with extracted paths
|
|
439
|
+
CREATE OR REPLACE VIEW file_operations AS
|
|
440
|
+
SELECT
|
|
441
|
+
uuid,
|
|
442
|
+
timestamp,
|
|
443
|
+
sessionId,
|
|
444
|
+
isAgent,
|
|
445
|
+
agentId,
|
|
446
|
+
project,
|
|
447
|
+
rownum,
|
|
448
|
+
tool_id,
|
|
449
|
+
tool_name,
|
|
450
|
+
COALESCE(
|
|
451
|
+
tool_input->>'file_path',
|
|
452
|
+
tool_input->>'path'
|
|
453
|
+
) as file_path,
|
|
454
|
+
tool_input->>'pattern' as pattern
|
|
455
|
+
FROM tool_uses
|
|
456
|
+
WHERE tool_name IN ('Read', 'Write', 'Edit', 'Glob', 'Grep');
|
|
347
457
|
`;
|
|
348
458
|
}
|
|
349
459
|
}
|
package/src/repl.js
CHANGED
|
@@ -52,15 +52,22 @@ function getHelpText() {
|
|
|
52
52
|
return `
|
|
53
53
|
Commands:
|
|
54
54
|
.help, .h Show this help
|
|
55
|
-
.schema, .s Show
|
|
55
|
+
.schema, .s Show schemas for all views
|
|
56
56
|
.schema <view> Show schema for a specific view
|
|
57
57
|
.quit, .q Exit
|
|
58
58
|
|
|
59
59
|
Views:
|
|
60
60
|
messages All messages (user, assistant, system)
|
|
61
61
|
user_messages User messages with user-specific fields
|
|
62
|
+
human_messages Human-typed messages (excludes tool results)
|
|
62
63
|
assistant_messages Assistant messages with error, requestId, etc.
|
|
63
64
|
system_messages System messages with hooks, retry info, etc.
|
|
65
|
+
raw_messages Raw JSON for each message by uuid
|
|
66
|
+
tool_uses All tool calls with unnested content blocks
|
|
67
|
+
tool_results Tool results with duration and error status
|
|
68
|
+
token_usage Token counts per assistant message
|
|
69
|
+
bash_commands Bash tool calls with extracted command
|
|
70
|
+
file_operations Read/Write/Edit/Glob/Grep with file paths
|
|
64
71
|
|
|
65
72
|
Example queries:
|
|
66
73
|
-- Count messages by type
|
|
@@ -137,7 +144,23 @@ async function handleDotCommand(command, qs) {
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
if (cmd === ".schema" || cmd === ".s") {
|
|
140
|
-
|
|
147
|
+
const views = [
|
|
148
|
+
"messages",
|
|
149
|
+
"user_messages",
|
|
150
|
+
"human_messages",
|
|
151
|
+
"assistant_messages",
|
|
152
|
+
"system_messages",
|
|
153
|
+
"raw_messages",
|
|
154
|
+
"tool_uses",
|
|
155
|
+
"tool_results",
|
|
156
|
+
"token_usage",
|
|
157
|
+
"bash_commands",
|
|
158
|
+
"file_operations",
|
|
159
|
+
];
|
|
160
|
+
for (const view of views) {
|
|
161
|
+
console.log(`\n=== ${view} ===`);
|
|
162
|
+
await executeQuery(qs, `DESCRIBE ${view}`);
|
|
163
|
+
}
|
|
141
164
|
return false;
|
|
142
165
|
}
|
|
143
166
|
|
package/src/session-loader.js
CHANGED
|
@@ -42,7 +42,7 @@ function countSessionsAndAgents(files, sessionFilter = "") {
|
|
|
42
42
|
* Get session info and file pattern for querying
|
|
43
43
|
* @param {string | null} claudeProjectsDir - Path to ~/.claude/projects/{slug}, or null for all projects
|
|
44
44
|
* @param {string} [sessionFilter] - Optional session ID prefix
|
|
45
|
-
* @returns {Promise<{ sessionCount: number, agentCount: number, projectCount: number, filePattern: string }>}
|
|
45
|
+
* @returns {Promise<{ sessionCount: number, agentCount: number, projectCount: number, filePattern: string | string[] }>}
|
|
46
46
|
*/
|
|
47
47
|
export async function getSessionFiles(claudeProjectsDir, sessionFilter = "") {
|
|
48
48
|
// If no specific project, use all projects
|
|
@@ -72,8 +72,13 @@ export async function getSessionFiles(claudeProjectsDir, sessionFilter = "") {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// Use glob pattern for all projects (** for recursive matching)
|
|
75
|
+
// When session filter is provided, include both the filtered session AND its subagents
|
|
76
|
+
// Subagents are stored in {session_id}/subagents/*.jsonl
|
|
75
77
|
const filePattern = sessionFilter
|
|
76
|
-
?
|
|
78
|
+
? [
|
|
79
|
+
join(base, "*", `${sessionFilter}*.jsonl`),
|
|
80
|
+
join(base, "*", `${sessionFilter}*/subagents/*.jsonl`),
|
|
81
|
+
]
|
|
77
82
|
: join(base, "*", "**/*.jsonl");
|
|
78
83
|
|
|
79
84
|
return {
|
|
@@ -93,9 +98,14 @@ export async function getSessionFiles(claudeProjectsDir, sessionFilter = "") {
|
|
|
93
98
|
return { sessionCount: 0, agentCount: 0, projectCount: 1, filePattern: "" };
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
// Use glob pattern
|
|
101
|
+
// Use glob pattern for matching
|
|
102
|
+
// When session filter is provided, include both the filtered session AND its subagents
|
|
103
|
+
// Subagents are stored in {session_id}/subagents/*.jsonl
|
|
97
104
|
const filePattern = sessionFilter
|
|
98
|
-
?
|
|
105
|
+
? [
|
|
106
|
+
join(claudeProjectsDir, `${sessionFilter}*.jsonl`),
|
|
107
|
+
join(claudeProjectsDir, `${sessionFilter}*/subagents/*.jsonl`),
|
|
108
|
+
]
|
|
99
109
|
: join(claudeProjectsDir, "**/*.jsonl");
|
|
100
110
|
|
|
101
111
|
return {
|