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 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.1.1",
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": "./bin/cc-query.js"
18
+ "cc-query": "bin/cc-query.js"
19
19
  },
20
20
  "files": [
21
21
  "index.js",
@@ -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
- '${this.#filePattern}',
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('${this.#filePattern}', ignore_errors=true)
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 table schema (runs DESCRIBE messages)
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
- await executeQuery(qs, "DESCRIBE messages");
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
 
@@ -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
- ? join(base, "*", `**/${sessionFilter}*.jsonl`)
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 with ** for recursive matching
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
- ? join(claudeProjectsDir, `**/${sessionFilter}*.jsonl`)
105
+ ? [
106
+ join(claudeProjectsDir, `${sessionFilter}*.jsonl`),
107
+ join(claudeProjectsDir, `${sessionFilter}*/subagents/*.jsonl`),
108
+ ]
99
109
  : join(claudeProjectsDir, "**/*.jsonl");
100
110
 
101
111
  return {