cc-query 0.2.0 → 0.3.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.
package/README.md ADDED
@@ -0,0 +1,67 @@
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 [example skill](examples/skills/reflect/SKILL.md) gives claude the ability and slash command `/reflect` to work with claude session history.
46
+
47
+ Why not a plugin? If you copy the skill you can reflect on it to adapt to your own usage.
48
+
49
+ For example you can ask questions like:
50
+ - Across all projects what bash commands return the most errors?
51
+ - Let's analyze the last session and identify how we might improve the claude.md file
52
+ - Gimme a summary of what we worked on this past week
53
+ - Let's go though our whole session history and identify repeated patterns that we could extract into skills
54
+ - Let's look at our use of cc-query tool calls to see how we might improve the reflect skill
55
+
56
+ ### Test drive
57
+
58
+ To test drive this skill do something like this:
59
+
60
+ 1. `npm i -g cc-query`
61
+ 2. Clone this repo or otherwise fetch the `examples/skills/reflect` dir
62
+ 3. `mkdir -p ~/.claude/skills && cp -R examples/skills/reflect ~/.claude/skills/`
63
+ 4. run claude and use `/reflect [whatever you want]`
64
+
65
+ ## License
66
+
67
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-query",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "SQL REPL for querying Claude Code session data",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,6 +8,59 @@ import { getSessionFiles } from "./session-loader.js";
8
8
  * @property {number} projectCount
9
9
  */
10
10
 
11
+ /**
12
+ * Convert a result value to string for display
13
+ * @param {any} val
14
+ * @returns {string}
15
+ */
16
+ function valueToString(val) {
17
+ if (val === null || val === undefined) return "NULL";
18
+ if (typeof val === "bigint") return val.toString();
19
+ if (typeof val === "object") {
20
+ // Handle DuckDB timestamp objects (returned as {micros: bigint})
21
+ if ("micros" in val) {
22
+ const ms = Number(val.micros) / 1000;
23
+ return new Date(ms).toISOString().replace("T", " ").replace("Z", "");
24
+ }
25
+ // Handle DuckDB UUID objects (returned as {hugeint: string})
26
+ if ("hugeint" in val) {
27
+ // Convert 128-bit signed decimal to UUID hex string
28
+ // DuckDB XORs the high bit for sorting, so flip it back
29
+ let n = BigInt(val.hugeint);
30
+ if (n < 0n) n += 1n << 128n; // Convert from signed to unsigned
31
+ n ^= 1n << 127n; // Flip high bit (undo DuckDB's sort optimization)
32
+ const hex = n.toString(16).padStart(32, "0");
33
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
34
+ }
35
+ return JSON.stringify(val, (_, v) =>
36
+ typeof v === "bigint" ? v.toString() : v,
37
+ );
38
+ }
39
+ return String(val);
40
+ }
41
+
42
+ /**
43
+ * Format query results as TSV with header row
44
+ * @param {import("@duckdb/node-api").DuckDBResultReader} result
45
+ * @returns {string}
46
+ */
47
+ function formatResultsTsv(result) {
48
+ const columnCount = result.columnCount;
49
+ if (columnCount === 0) return "";
50
+
51
+ const columnNames = [];
52
+ for (let i = 0; i < columnCount; i++) {
53
+ columnNames.push(result.columnName(i));
54
+ }
55
+ const rows = result.getRows();
56
+
57
+ const lines = [columnNames.join("\t")];
58
+ for (const row of rows) {
59
+ lines.push(row.map(valueToString).join("\t"));
60
+ }
61
+ return lines.join("\n");
62
+ }
63
+
11
64
  /**
12
65
  * Format query results as a table string
13
66
  * @param {import("@duckdb/node-api").DuckDBResultReader} result
@@ -29,33 +82,7 @@ function formatResults(result) {
29
82
  }
30
83
 
31
84
  // Convert all values to strings and calculate column widths
32
- const stringRows = rows.map((row) =>
33
- row.map((val) => {
34
- if (val === null || val === undefined) return "NULL";
35
- if (typeof val === "bigint") return val.toString();
36
- if (typeof val === "object") {
37
- // Handle DuckDB timestamp objects (returned as {micros: bigint})
38
- if ("micros" in val) {
39
- const ms = Number(val.micros) / 1000;
40
- return new Date(ms).toISOString().replace("T", " ").replace("Z", "");
41
- }
42
- // Handle DuckDB UUID objects (returned as {hugeint: string})
43
- if ("hugeint" in val) {
44
- // Convert 128-bit signed decimal to UUID hex string
45
- // DuckDB XORs the high bit for sorting, so flip it back
46
- let n = BigInt(val.hugeint);
47
- if (n < 0n) n += 1n << 128n; // Convert from signed to unsigned
48
- n ^= 1n << 127n; // Flip high bit (undo DuckDB's sort optimization)
49
- const hex = n.toString(16).padStart(32, "0");
50
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
51
- }
52
- return JSON.stringify(val, (_, v) =>
53
- typeof v === "bigint" ? v.toString() : v,
54
- );
55
- }
56
- return String(val);
57
- }),
58
- );
85
+ const stringRows = rows.map((row) => row.map(valueToString));
59
86
 
60
87
  const widths = columnNames.map((name, i) => {
61
88
  const maxDataWidth = Math.max(
@@ -91,13 +118,13 @@ function formatResults(result) {
91
118
  export class QuerySession {
92
119
  /** @type {import("@duckdb/node-api").DuckDBConnection | undefined} */
93
120
  #connection;
94
- /** @type {string} */
121
+ /** @type {string | string[]} */
95
122
  #filePattern;
96
123
  /** @type {QuerySessionInfo} */
97
124
  #info;
98
125
 
99
126
  /**
100
- * @param {string} filePattern - Glob pattern for JSONL files
127
+ * @param {string | string[]} filePattern - Glob pattern(s) for JSONL files
101
128
  * @param {QuerySessionInfo} info - Session counts
102
129
  */
103
130
  constructor(filePattern, info) {
@@ -105,6 +132,19 @@ export class QuerySession {
105
132
  this.#info = info;
106
133
  }
107
134
 
135
+ /**
136
+ * Format file pattern for use in DuckDB SQL
137
+ * @returns {string} SQL expression for the file pattern
138
+ */
139
+ #formatFilePatternForSql() {
140
+ if (Array.isArray(this.#filePattern)) {
141
+ // DuckDB accepts a list of patterns: ['pattern1', 'pattern2']
142
+ const patterns = this.#filePattern.map((p) => `'${p}'`).join(", ");
143
+ return `[${patterns}]`;
144
+ }
145
+ return `'${this.#filePattern}'`;
146
+ }
147
+
108
148
  /**
109
149
  * Create a QuerySession from a project path
110
150
  * @param {string | null} projectDir - Claude projects dir, or null for all
@@ -158,6 +198,19 @@ export class QuerySession {
158
198
  return formatResults(result);
159
199
  }
160
200
 
201
+ /**
202
+ * Execute a SQL query and return TSV formatted string with header
203
+ * @param {string} sql
204
+ * @returns {Promise<string>} Query result as TSV
205
+ */
206
+ async queryTsv(sql) {
207
+ if (!this.#connection) {
208
+ throw new Error("Session not initialized - use QuerySession.create()");
209
+ }
210
+ const result = await this.#connection.runAndReadAll(sql);
211
+ return formatResultsTsv(result);
212
+ }
213
+
161
214
  /**
162
215
  * Execute a SQL query and return raw rows
163
216
  * @param {string} sql
@@ -290,7 +343,7 @@ export class QuerySession {
290
343
  regexp_extract(filename, '/projects/([^/]+)/', 1) as project,
291
344
  ordinality as rownum
292
345
  FROM read_ndjson(
293
- '${this.#filePattern}',
346
+ ${this.#formatFilePatternForSql()},
294
347
  filename=true,
295
348
  ignore_errors=true,
296
349
  columns={${columnsDef}}
@@ -342,7 +395,7 @@ export class QuerySession {
342
395
  SELECT
343
396
  (json->>'uuid')::UUID as uuid,
344
397
  json as raw
345
- FROM read_ndjson_objects('${this.#filePattern}', ignore_errors=true)
398
+ FROM read_ndjson_objects(${this.#formatFilePatternForSql()}, ignore_errors=true)
346
399
  WHERE json->>'uuid' IS NOT NULL AND length(json->>'uuid') > 0;
347
400
 
348
401
  -- Tool uses: All tool calls with unnested content blocks
package/src/repl.js CHANGED
@@ -188,6 +188,7 @@ async function readStdin() {
188
188
 
189
189
  /**
190
190
  * Run queries from piped input (non-interactive mode)
191
+ * Uses TSV output format with --- separator between queries
191
192
  * @param {QuerySession} qs
192
193
  * @param {string} input
193
194
  */
@@ -198,12 +199,25 @@ async function runPipedQueries(qs, input) {
198
199
  .map((s) => s.trim())
199
200
  .filter((s) => s && s !== ";");
200
201
 
202
+ let isFirstOutput = true;
203
+
201
204
  for (const stmt of statements) {
202
205
  if (stmt.startsWith(".")) {
203
206
  const shouldExit = await handleDotCommand(stmt, qs);
204
207
  if (shouldExit) break;
205
208
  } else {
206
- await executeQuery(qs, stmt);
209
+ try {
210
+ const result = await qs.queryTsv(stmt);
211
+ if (result) {
212
+ if (!isFirstOutput) {
213
+ console.log("---");
214
+ }
215
+ console.log(result);
216
+ isFirstOutput = false;
217
+ }
218
+ } catch (err) {
219
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
220
+ }
207
221
  }
208
222
  }
209
223
  }
@@ -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 {