build(obelisk): let Claude Code search its own session history
- SQLite + FTS5 index over ~/.claude JSONL transcripts. - Agent writes JS queries at runtime — same sandbox pattern as workflows. - Covers sessions, subagents, workflow executions, tool calls, and full-text search.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 607 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 96" width="540" height="96">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="aurora" cx="0.5" cy="0.4" r="0.5">
|
||||||
|
<stop offset="0" stop-color="#f9a8d4" stop-opacity="0.9"/>
|
||||||
|
<stop offset="0.5" stop-color="#c084fc" stop-opacity="0.7"/>
|
||||||
|
<stop offset="1" stop-color="#818cf8" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="stone" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#94a3b8"/>
|
||||||
|
<stop offset="1" stop-color="#475569"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(8 8) scale(0.333)">
|
||||||
|
<ellipse cx="120" cy="50" rx="76" ry="48" fill="url(#aurora)"/>
|
||||||
|
<polygon points="120,40 106,64 134,64" fill="url(#stone)"/>
|
||||||
|
<polygon points="106,64 120,64 122,212 100,212" fill="url(#stone)"/>
|
||||||
|
<polygon points="120,64 134,64 140,212 122,212" fill="#334155"/>
|
||||||
|
<circle cx="70" cy="30" r="1.5" fill="#f9a8d4"/>
|
||||||
|
<circle cx="155" cy="18" r="1.8" fill="#ffffff"/>
|
||||||
|
</g>
|
||||||
|
<text x="100" y="46" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="38" font-weight="500" fill="#f0f6fc" letter-spacing="-0.4">Obelisk</text>
|
||||||
|
<text x="100" y="72" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="15" font-style="italic" fill="#8b949e">Let Claude Code search its own memory.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 90" width="360" height="90">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="aurora" cx="0.5" cy="0.4" r="0.5">
|
||||||
|
<stop offset="0" stop-color="#f9a8d4" stop-opacity="0.9"/>
|
||||||
|
<stop offset="0.5" stop-color="#c084fc" stop-opacity="0.7"/>
|
||||||
|
<stop offset="1" stop-color="#818cf8" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="stone" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#94a3b8"/>
|
||||||
|
<stop offset="1" stop-color="#475569"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(8 8) scale(0.3125)">
|
||||||
|
<ellipse cx="120" cy="50" rx="76" ry="48" fill="url(#aurora)"/>
|
||||||
|
<polygon points="120,40 106,64 134,64" fill="url(#stone)"/>
|
||||||
|
<polygon points="106,64 120,64 122,212 100,212" fill="url(#stone)"/>
|
||||||
|
<polygon points="120,64 134,64 140,212 122,212" fill="#334155"/>
|
||||||
|
<circle cx="70" cy="30" r="1.5" fill="#f9a8d4"/>
|
||||||
|
<circle cx="155" cy="18" r="1.8" fill="#ffffff"/>
|
||||||
|
</g>
|
||||||
|
<text x="100" y="58" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="36" font-weight="500" fill="#f0f6fc" letter-spacing="-0.01em">Obelisk</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 96" width="540" height="96">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="aurora" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ec4899" stop-opacity="0"/>
|
||||||
|
<stop offset="0.5" stop-color="#a855f7" stop-opacity="0.55"/>
|
||||||
|
<stop offset="1" stop-color="#6366f1" stop-opacity="0.75"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="stone" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#475569"/>
|
||||||
|
<stop offset="1" stop-color="#1e293b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(8 8) scale(0.333)">
|
||||||
|
<ellipse cx="120" cy="40" rx="56" ry="40" fill="url(#aurora)"/>
|
||||||
|
<polygon points="120,40 106,64 134,64" fill="url(#stone)"/>
|
||||||
|
<polygon points="106,64 120,64 122,212 100,212" fill="url(#stone)"/>
|
||||||
|
<polygon points="120,64 134,64 140,212 122,212" fill="#1e293b"/>
|
||||||
|
<circle cx="80" cy="30" r="1.2" fill="#a855f7"/>
|
||||||
|
<circle cx="150" cy="20" r="1.5" fill="#6366f1"/>
|
||||||
|
</g>
|
||||||
|
<text x="100" y="46" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="38" font-weight="500" fill="#1f2328" letter-spacing="-0.4">Obelisk</text>
|
||||||
|
<text x="100" y="72" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="15" font-style="italic" fill="#6e7681">Let Claude Code search its own memory.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 540 96" width="540" height="96">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="aurora" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ec4899" stop-opacity="0"/>
|
||||||
|
<stop offset="0.5" stop-color="#a855f7" stop-opacity="0.55"/>
|
||||||
|
<stop offset="1" stop-color="#6366f1" stop-opacity="0.75"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="stone" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#475569"/>
|
||||||
|
<stop offset="1" stop-color="#1e293b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(92 8) scale(0.333)">
|
||||||
|
<ellipse cx="120" cy="40" rx="56" ry="40" fill="url(#aurora)"/>
|
||||||
|
<polygon points="120,40 106,64 134,64" fill="url(#stone)"/>
|
||||||
|
<polygon points="106,64 120,64 122,212 100,212" fill="url(#stone)"/>
|
||||||
|
<polygon points="120,64 134,64 140,212 122,212" fill="#1e293b"/>
|
||||||
|
<circle cx="80" cy="30" r="1.2" fill="#a855f7"/>
|
||||||
|
<circle cx="150" cy="20" r="1.5" fill="#6366f1"/>
|
||||||
|
</g>
|
||||||
|
<text x="184" y="46" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="38" font-weight="500" fill="#1f2328" letter-spacing="-0.4">Obelisk</text>
|
||||||
|
<text x="184" y="72" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="15" font-style="italic" fill="#6e7681">Let Claude Code search its own memory.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 90" width="360" height="90">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="aurora" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#ec4899" stop-opacity="0"/>
|
||||||
|
<stop offset="0.5" stop-color="#a855f7" stop-opacity="0.55"/>
|
||||||
|
<stop offset="1" stop-color="#6366f1" stop-opacity="0.75"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="stone" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0" stop-color="#475569"/>
|
||||||
|
<stop offset="1" stop-color="#1e293b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(8 8) scale(0.3125)">
|
||||||
|
<ellipse cx="120" cy="40" rx="56" ry="40" fill="url(#aurora)"/>
|
||||||
|
<polygon points="120,40 106,64 134,64" fill="url(#stone)"/>
|
||||||
|
<polygon points="106,64 120,64 122,212 100,212" fill="url(#stone)"/>
|
||||||
|
<polygon points="120,64 134,64 140,212 122,212" fill="#1e293b"/>
|
||||||
|
<circle cx="80" cy="30" r="1.2" fill="#a855f7"/>
|
||||||
|
<circle cx="150" cy="20" r="1.5" fill="#6366f1"/>
|
||||||
|
</g>
|
||||||
|
<text x="100" y="58" font-family="Iowan Old Style, Charter, Georgia, serif" font-size="36" font-weight="500" fill="#1f2328" letter-spacing="-0.01em">Obelisk</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,122 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/obelisk-wordmark-d.svg">
|
||||||
|
<img src=".github/assets/obelisk-wordmark-l2.svg" alt="Obelisk" width="540">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
Every past session, subagent, and workflow — searchable in natural language.
|
||||||
|
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src=".github/assets/demo.png" alt="Obelisk in action" width="540">
|
||||||
|
<br />
|
||||||
|
<p>Ask in plain language. The agent writes the query, runs it, answers.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What you can ask
|
||||||
|
|
||||||
|
```
|
||||||
|
/obelisk 上次那个 auth 的 bug 我怎么修的
|
||||||
|
/obelisk 哪些文件这周被反复修改
|
||||||
|
/obelisk 最近 workflow 跑出来什么结果
|
||||||
|
/obelisk 我让 subagent 做过哪些代码 review
|
||||||
|
```
|
||||||
|
|
||||||
|
Anything Claude Code has done before — sessions, tool calls, subagents, workflows — is indexed and searchable. Ask in your own words.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add tommy0103/obelisk
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually: copy `obelisk/` into your project's `.claude/skills/`.
|
||||||
|
|
||||||
|
Then in any Claude Code session:
|
||||||
|
|
||||||
|
```
|
||||||
|
/obelisk <your question>
|
||||||
|
```
|
||||||
|
|
||||||
|
First run builds the index (~5 seconds for 100 sessions). After that it rebuilds incrementally.
|
||||||
|
|
||||||
|
### Requires
|
||||||
|
|
||||||
|
- Node.js 22+ (uses built-in node:sqlite with FTS5)
|
||||||
|
- Claude Code with skills support.
|
||||||
|
|
||||||
|
## What gets indexed
|
||||||
|
|
||||||
|
| Layer | Source | What's captured |
|
||||||
|
|-------|--------|----------------|
|
||||||
|
| **Sessions** | `<project>/<sessionId>.jsonl` | Title, project, timestamps, git branch |
|
||||||
|
| **Messages** | user + assistant turns | Full text, model, token usage, parent chain |
|
||||||
|
| **Tool calls** | every tool invocation | Tool name, input, file paths touched |
|
||||||
|
| **Subagents** | `subagents/agent-<id>.jsonl` | Agent type, description, full conversation |
|
||||||
|
| **Workflows** | `workflows/wf_<runId>.json` | Script, structured result, agent count |
|
||||||
|
| **Workflow agents** | `subagents/workflows/wf_<runId>/` | Per-agent transcripts linked to workflow |
|
||||||
|
|
||||||
|
Full-text search via FTS5 covers all message text across every layer.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
```
|
||||||
|
You ask a question
|
||||||
|
↓
|
||||||
|
Agent writes a JS query against the SQLite index
|
||||||
|
↓
|
||||||
|
Runs it via node runtime.mjs --query <script>
|
||||||
|
↓
|
||||||
|
Reads the JSON result, answers you in natural language
|
||||||
|
```
|
||||||
|
|
||||||
|
The core idea: **don't design a query DSL** — let the agent write code. An agent that can write workflow scripts can also write query scripts. Same sandbox, same mental model.
|
||||||
|
|
||||||
|
The agent has a two-tier API. Most questions only need the simple layer:
|
||||||
|
|
||||||
|
**Simple API** — taught directly in the skill prompt:
|
||||||
|
|
||||||
|
- `search(text)` — FTS5 full-text search, returns matches with surrounding context
|
||||||
|
- `context(uuid)` — full story around a message (parent chain, subagent/workflow metadata)
|
||||||
|
- `sql(query, ...params)` — raw SQL for anything else
|
||||||
|
|
||||||
|
**Advanced API** — agent reads `references/schema.md` on demand:
|
||||||
|
|
||||||
|
- `trace()` · `thread()` · `subagents()` · `workflows()` · `workflowTree()` · `fileHistory()` · `failures()` · `recent()`
|
||||||
|
|
||||||
|
The design is progressive disclosure: the agent doesn't see the full schema until it needs it.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.claude/skills/obelisk/
|
||||||
|
├── SKILL.md # Skill definition + simple API + examples
|
||||||
|
├── scripts/
|
||||||
|
│ └── runtime.mjs # Indexer + query runtime (400 lines, zero deps)
|
||||||
|
└── references/
|
||||||
|
└── schema.md # Full table schema + advanced API + query patterns
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
The index rebuilds incrementally — only new or modified JSONL files are re-parsed.
|
||||||
|
|
||||||
|
Zero npm dependencies. Uses Node 22's built-in node:sqlite with FTS5. The entire runtime is ~400 lines.
|
||||||
|
|
||||||
|
20K lines of scattered JSONL → something the agent can search() and sql() against in milliseconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT @tommy0103
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
name: obelisk
|
||||||
|
description: >
|
||||||
|
Search and query past Claude Code session history.
|
||||||
|
Reactive: when the user asks "how did I fix X", "what did we do last time", "find the session where", "上次怎么修的", "之前的session", "历史记录".
|
||||||
|
Proactive: when the user references past work you lack context for, when you're about to modify a file with complex edit history, when the user says "继续之前的" or "continue where we left off", or when understanding prior decisions would improve your current response.
|
||||||
|
allowed-tools:
|
||||||
|
- Read
|
||||||
|
- Bash(node:*)
|
||||||
|
- Write
|
||||||
|
---
|
||||||
|
|
||||||
|
# obelisk
|
||||||
|
|
||||||
|
Searches and queries your Claude Code session history stored in `~/.claude/`.
|
||||||
|
A SQLite index with FTS5 full-text search covers all sessions, subagent conversations, and workflow agent runs.
|
||||||
|
You write JS query snippets that run in a sandboxed VM against the indexed data, then parse the JSON output.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
The base directory for this skill is provided as `$SKILL_DIR` at invocation time (shown as "Base directory for this skill: ...").
|
||||||
|
|
||||||
|
**Fast keyword search** (no script needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node $SKILL_DIR/scripts/runtime.mjs --search "keyword"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Custom query** (write a JS snippet, run it):
|
||||||
|
|
||||||
|
1. Write a query to a temp file (e.g. `/tmp/q.mjs`)
|
||||||
|
2. Run: `node $SKILL_DIR/scripts/runtime.mjs --query /tmp/q.mjs`
|
||||||
|
3. Parse the JSON stdout and answer the user
|
||||||
|
|
||||||
|
The query file body is executed inside `(async () => { ... })()` with the API below available as globals. The last expression is returned as JSON. Use `return` to emit results.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### search(text, opts?)
|
||||||
|
|
||||||
|
Full-text search across all messages (user, assistant, subagent, workflow agent).
|
||||||
|
|
||||||
|
Returns: `[{ message: {uuid, text, role, timestamp, model}, session: {id, title, project, started_at}, context: [...surrounding messages] }]`
|
||||||
|
|
||||||
|
opts: `{ limit, sessionId, project, after, before }`
|
||||||
|
|
||||||
|
### context(uuid)
|
||||||
|
|
||||||
|
Full story around a message: the message itself, parent chain, session info, subagent/workflow metadata.
|
||||||
|
|
||||||
|
Returns: `{ message, parentChain, session, subagent, workflow }`
|
||||||
|
|
||||||
|
### recent(n?)
|
||||||
|
|
||||||
|
Latest n sessions (default 10). Returns session rows with title, project, started_at, ended_at.
|
||||||
|
|
||||||
|
### sql(query, ...params)
|
||||||
|
|
||||||
|
Raw SQL. Use `?` placeholders. Returns array of row objects.
|
||||||
|
|
||||||
|
Tables: `sessions`, `messages`, `tool_calls`, `tool_results`, `subagents`, `workflows`, `workflow_agents`, `messages_fts`
|
||||||
|
|
||||||
|
### Other APIs
|
||||||
|
|
||||||
|
- `trace(uuid)` -- full parent chain from root to message
|
||||||
|
- `thread(sessionId)` -- all messages in a session, ordered by time
|
||||||
|
- `subagents(sessionId)` -- subagent metadata + message counts
|
||||||
|
- `workflows(sessionId?)` -- workflow runs (all if no sessionId)
|
||||||
|
- `workflowTree(runId)` -- workflow + its agents + all their messages
|
||||||
|
- `fileHistory(filePath)` -- every Edit/Write/Read on a file across sessions
|
||||||
|
- `failures(sessionId?)` -- tool calls that returned errors, with surrounding context
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### "上次怎么修 auth 的"
|
||||||
|
|
||||||
|
```js
|
||||||
|
const hits = search('auth fix')
|
||||||
|
return hits.slice(0, 5).map(h => ({
|
||||||
|
session: h.session.title,
|
||||||
|
date: h.session.started_at,
|
||||||
|
message: h.message.text?.slice(0, 200)
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### "最近在做什么"
|
||||||
|
|
||||||
|
```js
|
||||||
|
return recent(10).map(s => ({ title: s.title, project: s.project, date: s.started_at }))
|
||||||
|
```
|
||||||
|
|
||||||
|
### "哪些文件被反复修改"
|
||||||
|
|
||||||
|
```js
|
||||||
|
return sql(`
|
||||||
|
SELECT file_path, COUNT(*) as n FROM tool_calls
|
||||||
|
WHERE name IN ('Edit','Write') AND file_path IS NOT NULL
|
||||||
|
GROUP BY file_path HAVING n > 3 ORDER BY n DESC LIMIT 20
|
||||||
|
`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### "那个 review workflow 的结果是什么"
|
||||||
|
|
||||||
|
```js
|
||||||
|
const wfs = workflows()
|
||||||
|
const review = wfs.find(w =>
|
||||||
|
w.run_id.includes('review') ||
|
||||||
|
JSON.parse(w.result_json || '{}').synthesis
|
||||||
|
)
|
||||||
|
return review ? JSON.parse(review.result_json) : 'No review workflow found'
|
||||||
|
```
|
||||||
|
|
||||||
|
### "上次跑 experiment 用了多少 token"
|
||||||
|
|
||||||
|
```js
|
||||||
|
const hits = search('experiment')
|
||||||
|
if (!hits.length) return 'No experiment sessions found'
|
||||||
|
const sid = hits[0].session.id
|
||||||
|
return sql('SELECT SUM(input_tokens) as input, SUM(output_tokens) as output FROM messages WHERE session_id = ?', sid)
|
||||||
|
```
|
||||||
|
|
||||||
|
### "追踪一下那个决策是怎么做的"
|
||||||
|
|
||||||
|
```js
|
||||||
|
const hits = search('the decision query here')
|
||||||
|
if (!hits.length) return 'Nothing found'
|
||||||
|
return context(hits[0].message.uuid)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- First run builds the index (~5s for ~100 sessions). Subsequent runs are incremental.
|
||||||
|
- DB location: `~/.claude/obelisk.sqlite`
|
||||||
|
- Subagent and workflow agent conversations are fully indexed and searchable.
|
||||||
|
- Query scripts run in a sandboxed VM context -- no file system or network access from inside scripts.
|
||||||
|
- Text is truncated to 10k chars per message during indexing.
|
||||||
|
- FTS5 search supports standard SQLite FTS syntax: `"exact phrase"`, `term1 AND term2`, `term1 OR term2`, `term1 NOT term2`.
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
# Obelisk -- Schema and API Reference
|
||||||
|
|
||||||
|
Advanced reference for the obelisk database.
|
||||||
|
Read this when `search()`, `context()`, or `sql()` are not enough.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Database Schema
|
||||||
|
|
||||||
|
Database location: `~/.claude/obelisk.sqlite`
|
||||||
|
|
||||||
|
### sessions
|
||||||
|
|
||||||
|
One row per Claude Code session.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id TEXT PRIMARY KEY, -- session UUID (matches JSONL filename)
|
||||||
|
title TEXT, -- AI-generated session title (may be NULL)
|
||||||
|
project TEXT, -- project slug (hyphenated path, e.g. "Users-tomiya-Code-quiet-zero")
|
||||||
|
project_path TEXT, -- reconstructed filesystem path (e.g. "/Users/tomiya/Code/quiet-zero")
|
||||||
|
started_at TEXT, -- ISO 8601 timestamp of first message
|
||||||
|
ended_at TEXT, -- ISO 8601 timestamp of last message
|
||||||
|
git_branch TEXT, -- git branch active during session (if any)
|
||||||
|
version TEXT, -- Claude Code version string
|
||||||
|
message_count INTEGER DEFAULT 0, -- total user + assistant messages
|
||||||
|
jsonl_path TEXT -- absolute path to source JSONL file
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### messages
|
||||||
|
|
||||||
|
Every user and assistant message. Core table for all queries.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE messages (
|
||||||
|
uuid TEXT PRIMARY KEY, -- message UUID
|
||||||
|
session_id TEXT, -- FK -> sessions.id
|
||||||
|
type TEXT, -- "user" or "assistant"
|
||||||
|
parent_uuid TEXT, -- UUID of parent message (conversation tree)
|
||||||
|
timestamp TEXT, -- ISO 8601
|
||||||
|
role TEXT, -- "user" or "assistant" (from message payload)
|
||||||
|
text TEXT, -- extracted text content (thinking + text blocks, truncated to 10k chars)
|
||||||
|
model TEXT, -- model name (e.g. "claude-opus-4-6-20250529"), NULL for user messages
|
||||||
|
is_sidechain INTEGER DEFAULT 0, -- 1 if this message is on a sidechain (retry/branch)
|
||||||
|
agent_id TEXT, -- subagent or workflow agent UUID (NULL for main conversation)
|
||||||
|
input_tokens INTEGER, -- token usage (assistant messages only)
|
||||||
|
output_tokens INTEGER -- token usage (assistant messages only)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Indexes: `idx_messages_session(session_id)`, `idx_messages_agent(agent_id)`, `idx_messages_ts(session_id, timestamp)`.
|
||||||
|
|
||||||
|
### messages_fts
|
||||||
|
|
||||||
|
FTS5 virtual table for full-text search over message text.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
||||||
|
uuid UNINDEXED, -- not searchable, carried for JOINs
|
||||||
|
session_id UNINDEXED, -- not searchable, carried for filtering
|
||||||
|
text, -- the searchable column
|
||||||
|
content=messages, -- content-sync with messages table
|
||||||
|
content_rowid=rowid
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Queried via `MATCH` syntax. Rebuilt on each index pass.
|
||||||
|
|
||||||
|
### tool_calls
|
||||||
|
|
||||||
|
Every tool invocation by the assistant. One row per `tool_use` content block.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tool_calls (
|
||||||
|
id TEXT PRIMARY KEY, -- tool_use ID (from API response)
|
||||||
|
message_uuid TEXT, -- FK -> messages.uuid (the assistant message containing this call)
|
||||||
|
session_id TEXT, -- FK -> sessions.id (denormalized for fast queries)
|
||||||
|
name TEXT, -- tool name: "Read", "Edit", "Write", "Bash", "WebSearch", etc.
|
||||||
|
input_json TEXT, -- JSON-serialized tool input (truncated to 10k chars)
|
||||||
|
file_path TEXT -- extracted file_path for Read/Edit/Write/NotebookEdit (NULL otherwise)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Indexes: `idx_tc_session_name(session_id, name)`, `idx_tc_file(file_path)`.
|
||||||
|
|
||||||
|
### tool_results
|
||||||
|
|
||||||
|
The result returned for each tool call. Appears in the next user message.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tool_results (
|
||||||
|
tool_use_id TEXT PRIMARY KEY, -- FK -> tool_calls.id
|
||||||
|
message_uuid TEXT, -- FK -> messages.uuid (the user message carrying this result)
|
||||||
|
session_id TEXT, -- FK -> sessions.id (denormalized)
|
||||||
|
content TEXT, -- result text (truncated to 10k chars)
|
||||||
|
file_path TEXT -- file path from toolUseResult metadata (if any)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### subagents
|
||||||
|
|
||||||
|
Metadata for subagent spawns (non-workflow agents).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE subagents (
|
||||||
|
agent_id TEXT PRIMARY KEY, -- subagent UUID
|
||||||
|
session_id TEXT, -- FK -> sessions.id (parent session)
|
||||||
|
parent_tool_use_id TEXT, -- tool_use ID that spawned this agent
|
||||||
|
agent_type TEXT, -- e.g. "code-review", "research"
|
||||||
|
description TEXT, -- task description given to the subagent
|
||||||
|
duration_ms INTEGER, -- wall-clock duration (computed from message timestamps)
|
||||||
|
total_tokens INTEGER -- sum of input_tokens + output_tokens across all agent messages
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Index: `idx_sa_session(session_id)`.
|
||||||
|
|
||||||
|
### workflows
|
||||||
|
|
||||||
|
Workflow execution records. A workflow orchestrates multiple agents.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflows (
|
||||||
|
run_id TEXT PRIMARY KEY, -- workflow run UUID
|
||||||
|
session_id TEXT, -- FK -> sessions.id (parent session)
|
||||||
|
task_id TEXT, -- task identifier (if any)
|
||||||
|
script TEXT, -- workflow script content (truncated)
|
||||||
|
result_json TEXT, -- JSON-serialized workflow result
|
||||||
|
timestamp TEXT, -- ISO 8601 execution time
|
||||||
|
agent_count INTEGER DEFAULT 0 -- number of agents in this workflow
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Index: `idx_wf_session(session_id)`.
|
||||||
|
|
||||||
|
### workflow_agents
|
||||||
|
|
||||||
|
Individual agents within a workflow run.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflow_agents (
|
||||||
|
agent_id TEXT PRIMARY KEY, -- agent UUID
|
||||||
|
run_id TEXT, -- FK -> workflows.run_id
|
||||||
|
session_id TEXT, -- FK -> sessions.id
|
||||||
|
agent_type TEXT, -- agent type label
|
||||||
|
description TEXT -- task description
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Index: `idx_wa_run(run_id)`.
|
||||||
|
|
||||||
|
### index_state
|
||||||
|
|
||||||
|
Tracks incremental indexing progress per JSONL file.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE index_state (
|
||||||
|
jsonl_path TEXT PRIMARY KEY, -- absolute path to JSONL file
|
||||||
|
mtime REAL, -- file mtime at last index (milliseconds)
|
||||||
|
lines_processed INTEGER -- number of lines already processed
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Relationships
|
||||||
|
|
||||||
|
```
|
||||||
|
sessions.id <-- messages.session_id
|
||||||
|
sessions.id <-- tool_calls.session_id
|
||||||
|
sessions.id <-- tool_results.session_id
|
||||||
|
sessions.id <-- subagents.session_id
|
||||||
|
sessions.id <-- workflows.session_id
|
||||||
|
messages.uuid <-- tool_calls.message_uuid
|
||||||
|
messages.uuid <-- tool_results.message_uuid
|
||||||
|
messages.agent_id --> subagents.agent_id (for subagent messages)
|
||||||
|
messages.agent_id --> workflow_agents.agent_id (for workflow agent messages)
|
||||||
|
tool_calls.id <-- tool_results.tool_use_id
|
||||||
|
workflows.run_id <-- workflow_agents.run_id
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Query API Reference
|
||||||
|
|
||||||
|
All functions are available as globals inside `--query` scripts.
|
||||||
|
Scripts run in an async IIFE with a 30-second timeout.
|
||||||
|
|
||||||
|
### Simple Layer
|
||||||
|
|
||||||
|
#### `search(text, opts?)`
|
||||||
|
|
||||||
|
Full-text search across all message text using FTS5.
|
||||||
|
|
||||||
|
| Param | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `text` | `string` | FTS5 query (terms, phrases, prefix) |
|
||||||
|
| `opts.limit` | `number` | Max results (default 20) |
|
||||||
|
| `opts.sessionId` | `string` | Restrict to one session |
|
||||||
|
| `opts.project` | `string` | Restrict to a project slug |
|
||||||
|
| `opts.after` | `string` | ISO 8601 lower bound on timestamp |
|
||||||
|
| `opts.before` | `string` | ISO 8601 upper bound on timestamp |
|
||||||
|
|
||||||
|
**Returns:** `Array<{ message, session, context }>` where `context` is the 6 nearest messages by timestamp.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const hits = search('MCTS exploration');
|
||||||
|
return hits.map(h => ({ title: h.session.title, text: h.message.text?.slice(0, 200) }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `context(uuid)`
|
||||||
|
|
||||||
|
Full context around a single message: parent chain, session metadata, subagent/workflow info.
|
||||||
|
|
||||||
|
| Param | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `uuid` | `string` | Message UUID |
|
||||||
|
|
||||||
|
**Returns:** `{ message, parentChain, session, subagent, workflow }` or `null`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const c = context('abc-123-def');
|
||||||
|
return { chain_length: c.parentChain.length, session_title: c.session?.title };
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `sql(query, ...params)`
|
||||||
|
|
||||||
|
Raw SQL with parameterized bindings. Returns an array of row objects.
|
||||||
|
|
||||||
|
| Param | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `query` | `string` | SQL SELECT statement |
|
||||||
|
| `...params` | `any` | Bind parameters (positional `?`) |
|
||||||
|
|
||||||
|
**Returns:** `Array<Object>` -- each row as `{ column: value }`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const rows = sql('SELECT id, title FROM sessions WHERE project = ? ORDER BY ended_at DESC LIMIT 5', 'Users-tomiya-Code-quiet-zero');
|
||||||
|
return rows;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Layer
|
||||||
|
|
||||||
|
#### `trace(uuid)`
|
||||||
|
|
||||||
|
Walk the `parent_uuid` chain from a message up to the conversation root.
|
||||||
|
|
||||||
|
**Returns:** `Array<message>` ordered root-first.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const chain = trace('some-uuid');
|
||||||
|
return chain.map(m => ({ role: m.role, text: m.text?.slice(0, 100) }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `thread(sessionId)`
|
||||||
|
|
||||||
|
All messages in a session, ordered by timestamp.
|
||||||
|
|
||||||
|
**Returns:** `Array<message>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const msgs = thread('session-uuid');
|
||||||
|
return { count: msgs.length, first: msgs[0]?.text?.slice(0, 100) };
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `subagents(sessionId)`
|
||||||
|
|
||||||
|
All subagent spawns for a session, with message counts.
|
||||||
|
|
||||||
|
**Returns:** `Array<{ ...subagent_row, messageCount }>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const subs = subagents('session-uuid');
|
||||||
|
return subs.map(s => ({ type: s.agent_type, desc: s.description, msgs: s.messageCount, tokens: s.total_tokens }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `workflows(sessionId?)`
|
||||||
|
|
||||||
|
Workflow executions. Pass a session ID to filter, or omit for all workflows (newest first).
|
||||||
|
|
||||||
|
**Returns:** `Array<workflow_row>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const wfs = workflows();
|
||||||
|
return wfs.slice(0, 5).map(w => ({ run: w.run_id, agents: w.agent_count, time: w.timestamp }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `workflowTree(runId)`
|
||||||
|
|
||||||
|
Full execution tree for a workflow: the workflow record plus all its agents and their messages.
|
||||||
|
|
||||||
|
**Returns:** `{ ...workflow_row, agents: Array<{ ...agent_row, messages: Array<message> }> }` or `null`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const tree = workflowTree('run-uuid');
|
||||||
|
return tree?.agents.map(a => ({ type: a.agent_type, msgs: a.messages.length }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `fileHistory(filePath)`
|
||||||
|
|
||||||
|
All tool calls that touched a specific file, across every session.
|
||||||
|
|
||||||
|
**Returns:** `Array<{ toolCall, session, timestamp }>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const edits = fileHistory('/Users/tomiya/Code/quiet-zero/src/mcts.ts');
|
||||||
|
return edits.map(e => ({ tool: e.toolCall.name, session: e.session.title, time: e.timestamp }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `failures(sessionId?)`
|
||||||
|
|
||||||
|
Tool calls whose results contain error patterns (`Error`, `ENOENT`, `failed`, `permission denied`, etc.). Includes the 3 messages immediately after each failure for retry context.
|
||||||
|
|
||||||
|
**Returns:** `Array<{ toolCall, result, session, nextMessages }>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fails = failures('session-uuid');
|
||||||
|
return fails.map(f => ({ tool: f.toolCall?.name, error: f.result.content?.slice(0, 200) }));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `recent(n?)`
|
||||||
|
|
||||||
|
Last `n` sessions (default 10), ordered by `ended_at` descending.
|
||||||
|
|
||||||
|
**Returns:** `Array<session_row>`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const last5 = recent(5);
|
||||||
|
return last5.map(s => ({ title: s.title, project: s.project_path, ended: s.ended_at }));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Common Query Patterns
|
||||||
|
|
||||||
|
### Find sessions about a topic
|
||||||
|
|
||||||
|
```js
|
||||||
|
const hits = search('reinforcement learning');
|
||||||
|
const sessions = [...new Set(hits.map(h => h.session.id))];
|
||||||
|
return hits.slice(0, 10).map(h => ({
|
||||||
|
session: h.session.title,
|
||||||
|
snippet: h.message.text?.slice(0, 150),
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trace a decision chain
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Find a message, then trace its full parent chain to understand how we got there
|
||||||
|
const hits = search('"switched to PPO"');
|
||||||
|
if (!hits.length) return 'not found';
|
||||||
|
const chain = trace(hits[0].message.uuid);
|
||||||
|
return chain.map(m => ({ role: m.role, text: m.text?.slice(0, 120), ts: m.timestamp }));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find all edits to a file across sessions
|
||||||
|
|
||||||
|
```js
|
||||||
|
const edits = fileHistory('/Users/tomiya/Code/quiet-zero/src/mcts.ts');
|
||||||
|
return edits.map(e => ({
|
||||||
|
action: e.toolCall.name,
|
||||||
|
session: e.session.title,
|
||||||
|
time: e.timestamp,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find churned files (most-edited across all sessions)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const rows = sql(`
|
||||||
|
SELECT file_path, COUNT(*) as edit_count, COUNT(DISTINCT session_id) as session_count
|
||||||
|
FROM tool_calls
|
||||||
|
WHERE file_path IS NOT NULL AND name IN ('Edit','Write')
|
||||||
|
GROUP BY file_path
|
||||||
|
ORDER BY edit_count DESC
|
||||||
|
LIMIT 20
|
||||||
|
`);
|
||||||
|
return rows;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token usage analysis
|
||||||
|
|
||||||
|
```js
|
||||||
|
const rows = sql(`
|
||||||
|
SELECT s.id, s.title,
|
||||||
|
SUM(m.input_tokens) as total_in,
|
||||||
|
SUM(m.output_tokens) as total_out,
|
||||||
|
SUM(m.input_tokens) + SUM(m.output_tokens) as total
|
||||||
|
FROM messages m JOIN sessions s ON s.id = m.session_id
|
||||||
|
WHERE m.input_tokens IS NOT NULL
|
||||||
|
GROUP BY s.id
|
||||||
|
ORDER BY total DESC
|
||||||
|
LIMIT 10
|
||||||
|
`);
|
||||||
|
return rows;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find workflow results
|
||||||
|
|
||||||
|
```js
|
||||||
|
const wfs = workflows();
|
||||||
|
for (const wf of wfs.slice(0, 3)) {
|
||||||
|
const tree = workflowTree(wf.run_id);
|
||||||
|
wf.agent_details = tree?.agents.map(a => ({
|
||||||
|
type: a.agent_type, desc: a.description, msgs: a.messages.length,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return wfs.slice(0, 3);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find error patterns
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fails = failures();
|
||||||
|
// Group by tool name
|
||||||
|
const byTool = {};
|
||||||
|
for (const f of fails) {
|
||||||
|
const name = f.toolCall?.name || 'unknown';
|
||||||
|
byTool[name] = (byTool[name] || 0) + 1;
|
||||||
|
}
|
||||||
|
return { total: fails.length, byTool };
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find what tools were used most
|
||||||
|
|
||||||
|
```js
|
||||||
|
const rows = sql(`
|
||||||
|
SELECT name, COUNT(*) as call_count, COUNT(DISTINCT session_id) as session_count
|
||||||
|
FROM tool_calls
|
||||||
|
GROUP BY name
|
||||||
|
ORDER BY call_count DESC
|
||||||
|
`);
|
||||||
|
return rows;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find sessions by time range
|
||||||
|
|
||||||
|
```js
|
||||||
|
const rows = sql(`
|
||||||
|
SELECT id, title, project_path, started_at, ended_at, message_count
|
||||||
|
FROM sessions
|
||||||
|
WHERE started_at >= ? AND started_at < ?
|
||||||
|
ORDER BY started_at DESC
|
||||||
|
`, '2026-05-28T00:00:00Z', '2026-05-30T00:00:00Z');
|
||||||
|
return rows;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-reference subagent findings
|
||||||
|
|
||||||
|
```js
|
||||||
|
// See what all subagents did in a session
|
||||||
|
const subs = subagents('session-uuid');
|
||||||
|
const details = subs.map(s => {
|
||||||
|
const msgs = sql('SELECT text, role FROM messages WHERE agent_id = ? ORDER BY timestamp', s.agent_id);
|
||||||
|
return { type: s.agent_type, desc: s.description, summary: msgs.slice(-1)[0]?.text?.slice(0, 300) };
|
||||||
|
});
|
||||||
|
return details;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find all sessions for a project
|
||||||
|
|
||||||
|
```js
|
||||||
|
const rows = sql(`
|
||||||
|
SELECT id, title, started_at, ended_at, message_count, git_branch
|
||||||
|
FROM sessions
|
||||||
|
WHERE project_path = ?
|
||||||
|
ORDER BY started_at DESC
|
||||||
|
`, '/Users/tomiya/Code/quiet-zero');
|
||||||
|
return rows;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reconstruct what happened in a session
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Full timeline: messages + tool calls interleaved
|
||||||
|
const msgs = thread('session-uuid');
|
||||||
|
return msgs.map(m => {
|
||||||
|
const tools = sql('SELECT name, file_path FROM tool_calls WHERE message_uuid = ?', m.uuid);
|
||||||
|
return {
|
||||||
|
role: m.role, text: m.text?.slice(0, 100), ts: m.timestamp,
|
||||||
|
tools: tools.length ? tools.map(t => `${t.name}(${t.file_path || ''})`) : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tips
|
||||||
|
|
||||||
|
### When to use `search()` vs `sql()`
|
||||||
|
|
||||||
|
- **`search()`** -- when you are looking for messages containing specific words or phrases.
|
||||||
|
Uses FTS5 under the hood, returns ranked results with surrounding context.
|
||||||
|
Best for: "find where we discussed X", "when did I mention Y".
|
||||||
|
- **`sql()`** -- when you need structured queries: aggregations, JOINs, GROUP BY, date ranges,
|
||||||
|
or anything involving tables other than `messages`.
|
||||||
|
Best for: "how many edits to this file", "total tokens this week", "most-used tools".
|
||||||
|
|
||||||
|
### FTS5 Match Syntax
|
||||||
|
|
||||||
|
The `text` argument to `search()` uses SQLite FTS5 query syntax:
|
||||||
|
|
||||||
|
| Pattern | Meaning | Example |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `word` | Match token | `search('MCTS')` |
|
||||||
|
| `word1 word2` | Implicit AND | `search('MCTS exploration')` |
|
||||||
|
| `"exact phrase"` | Phrase match | `search('"Monte Carlo tree"')` |
|
||||||
|
| `word*` | Prefix match | `search('optim*')` matches optimize, optimizer, optimization |
|
||||||
|
| `word1 OR word2` | Either term | `search('PPO OR TRPO')` |
|
||||||
|
| `word1 NOT word2` | Exclude | `search('MCTS NOT debug')` |
|
||||||
|
|
||||||
|
Terms are case-insensitive. FTS5 tokenizes on whitespace and punctuation,
|
||||||
|
so `camelCase` is indexed as two tokens (`camel`, `case`).
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **FTS5 searches** are fast (milliseconds) regardless of database size.
|
||||||
|
- **`sql()` with indexes** is fast. The indexed columns cover the common patterns:
|
||||||
|
`messages(session_id)`, `messages(agent_id)`, `messages(session_id, timestamp)`,
|
||||||
|
`tool_calls(session_id, name)`, `tool_calls(file_path)`.
|
||||||
|
- **JOINs across large sessions** (1000+ messages) can be slow if you join
|
||||||
|
`messages` with `tool_calls` and `tool_results` without filtering by `session_id` first.
|
||||||
|
Always add a `session_id` filter when working within a session.
|
||||||
|
- **Full table scans on `tool_results`** (used by `failures()` with no session ID)
|
||||||
|
can be slow on large databases because it pattern-matches every result row.
|
||||||
|
Pass a `sessionId` when possible.
|
||||||
|
- **Text fields are truncated** to 10,000 characters at index time. If you need
|
||||||
|
the full content of a long message or tool result, read the source JSONL directly
|
||||||
|
(path available in `sessions.jsonl_path`).
|
||||||
|
- The database uses **WAL mode** and **NORMAL synchronous**, so reads never block
|
||||||
|
writes during re-indexing.
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
|
||||||
|
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
||||||
|
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
||||||
|
const HISTORY_PATH = path.join(CLAUDE_DIR, 'history.jsonl');
|
||||||
|
const DB_PATH = path.join(CLAUDE_DIR, 'obelisk.sqlite');
|
||||||
|
const TEXT_LIMIT = 10000;
|
||||||
|
|
||||||
|
const SCHEMA = `
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id TEXT PRIMARY KEY, title TEXT, project TEXT, project_path TEXT,
|
||||||
|
started_at TEXT, ended_at TEXT, git_branch TEXT, version TEXT,
|
||||||
|
message_count INTEGER DEFAULT 0, jsonl_path TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
uuid TEXT PRIMARY KEY, session_id TEXT, type TEXT, parent_uuid TEXT,
|
||||||
|
timestamp TEXT, role TEXT, text TEXT, model TEXT,
|
||||||
|
is_sidechain INTEGER DEFAULT 0, agent_id TEXT,
|
||||||
|
input_tokens INTEGER, output_tokens INTEGER);
|
||||||
|
CREATE TABLE IF NOT EXISTS tool_calls (
|
||||||
|
id TEXT PRIMARY KEY, message_uuid TEXT, session_id TEXT,
|
||||||
|
name TEXT, input_json TEXT, file_path TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS tool_results (
|
||||||
|
tool_use_id TEXT PRIMARY KEY, message_uuid TEXT, session_id TEXT,
|
||||||
|
content TEXT, file_path TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS subagents (
|
||||||
|
agent_id TEXT PRIMARY KEY, session_id TEXT, parent_tool_use_id TEXT,
|
||||||
|
agent_type TEXT, description TEXT, duration_ms INTEGER, total_tokens INTEGER);
|
||||||
|
CREATE TABLE IF NOT EXISTS workflows (
|
||||||
|
run_id TEXT PRIMARY KEY, session_id TEXT, task_id TEXT,
|
||||||
|
script TEXT, result_json TEXT, timestamp TEXT, agent_count INTEGER DEFAULT 0);
|
||||||
|
CREATE TABLE IF NOT EXISTS workflow_agents (
|
||||||
|
agent_id TEXT PRIMARY KEY, run_id TEXT, session_id TEXT,
|
||||||
|
agent_type TEXT, description TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS index_state (
|
||||||
|
jsonl_path TEXT PRIMARY KEY, mtime REAL, lines_processed INTEGER);
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||||
|
uuid UNINDEXED, session_id UNINDEXED, text, content=messages, content_rowid=rowid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_agent ON messages(agent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(session_id, timestamp);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tc_session_name ON tool_calls(session_id, name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tc_file ON tool_calls(file_path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sa_session ON subagents(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wf_session ON workflows(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wa_run ON workflow_agents(run_id);
|
||||||
|
`;
|
||||||
|
|
||||||
|
function openDb() {
|
||||||
|
const db = new DatabaseSync(DB_PATH);
|
||||||
|
db.exec('PRAGMA journal_mode=WAL');
|
||||||
|
db.exec('PRAGMA synchronous=NORMAL');
|
||||||
|
db.exec(SCHEMA);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trunc(s) {
|
||||||
|
return typeof s === 'string' && s.length > TEXT_LIMIT ? s.slice(0, TEXT_LIMIT) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(content) {
|
||||||
|
if (typeof content === 'string') return trunc(content);
|
||||||
|
if (!Array.isArray(content)) return null;
|
||||||
|
const parts = [];
|
||||||
|
for (const b of content) {
|
||||||
|
if (b.type === 'text' && b.text) parts.push(b.text);
|
||||||
|
else if (b.type === 'thinking' && b.thinking) parts.push(b.thinking);
|
||||||
|
}
|
||||||
|
return parts.length ? trunc(parts.join('\n')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filePath(name, input) {
|
||||||
|
if (!input) return null;
|
||||||
|
return ['Read', 'Edit', 'Write', 'NotebookEdit'].includes(name) ? (input.file_path || null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
|
||||||
|
|
||||||
|
function discoverJsonlFiles() {
|
||||||
|
const files = [];
|
||||||
|
if (!fs.existsSync(PROJECTS_DIR)) return files;
|
||||||
|
for (const proj of fs.readdirSync(PROJECTS_DIR)) {
|
||||||
|
const projPath = path.join(PROJECTS_DIR, proj);
|
||||||
|
if (!isDir(projPath)) continue;
|
||||||
|
for (const f of fs.readdirSync(projPath)) {
|
||||||
|
if (f.endsWith('.jsonl'))
|
||||||
|
files.push({ path: path.join(projPath, f), sessionId: f.slice(0, -6), project: proj, isSubagent: false });
|
||||||
|
}
|
||||||
|
for (const sd of fs.readdirSync(projPath)) {
|
||||||
|
const saDir = path.join(projPath, sd, 'subagents');
|
||||||
|
if (!isDir(saDir)) continue;
|
||||||
|
for (const sf of fs.readdirSync(saDir)) {
|
||||||
|
if (sf.endsWith('.jsonl'))
|
||||||
|
files.push({ path: path.join(saDir, sf), sessionId: sd, project: proj, isSubagent: true, agentId: sf.slice(0, -6) });
|
||||||
|
}
|
||||||
|
const wfRoot = path.join(saDir, 'workflows');
|
||||||
|
if (!isDir(wfRoot)) continue;
|
||||||
|
for (const wfDir of fs.readdirSync(wfRoot)) {
|
||||||
|
const wfPath = path.join(wfRoot, wfDir);
|
||||||
|
if (!isDir(wfPath)) continue;
|
||||||
|
for (const wf of fs.readdirSync(wfPath)) {
|
||||||
|
if (wf.endsWith('.jsonl'))
|
||||||
|
files.push({ path: path.join(wfPath, wf), sessionId: sd, project: proj, isSubagent: true, agentId: wf.slice(0, -6), workflowRunId: wfDir });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsReindex(db, fp) {
|
||||||
|
const mt = fs.statSync(fp).mtimeMs;
|
||||||
|
const row = db.prepare('SELECT mtime, lines_processed FROM index_state WHERE jsonl_path = ?').get(fp);
|
||||||
|
if (!row) return { needed: true, skip: 0 };
|
||||||
|
return mt > row.mtime ? { needed: true, skip: row.lines_processed } : { needed: false, skip: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexJsonl(db, fi) {
|
||||||
|
const { needed, skip } = needsReindex(db, fi.path);
|
||||||
|
if (!needed) return;
|
||||||
|
const lines = fs.readFileSync(fi.path, 'utf8').split('\n').filter(Boolean);
|
||||||
|
const mt = fs.statSync(fi.path).mtimeMs;
|
||||||
|
|
||||||
|
const ins = {
|
||||||
|
ses: db.prepare('INSERT OR REPLACE INTO sessions (id,title,project,project_path,started_at,ended_at,git_branch,version,message_count,jsonl_path) VALUES (?,?,?,?,?,?,?,?,?,?)'),
|
||||||
|
msg: db.prepare('INSERT OR REPLACE INTO messages (uuid,session_id,type,parent_uuid,timestamp,role,text,model,is_sidechain,agent_id,input_tokens,output_tokens) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'),
|
||||||
|
tc: db.prepare('INSERT OR REPLACE INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path) VALUES (?,?,?,?,?,?)'),
|
||||||
|
tr: db.prepare('INSERT OR REPLACE INTO tool_results (tool_use_id,message_uuid,session_id,content,file_path) VALUES (?,?,?,?,?)'),
|
||||||
|
idx: db.prepare('INSERT OR REPLACE INTO index_state (jsonl_path,mtime,lines_processed) VALUES (?,?,?)'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sm = { started_at: null, ended_at: null, git_branch: null, version: null, title: null, n: 0 };
|
||||||
|
|
||||||
|
for (let i = skip; i < lines.length; i++) {
|
||||||
|
let obj;
|
||||||
|
try { obj = JSON.parse(lines[i]); } catch { continue; }
|
||||||
|
const sid = fi.sessionId;
|
||||||
|
const ts = obj.timestamp || null;
|
||||||
|
|
||||||
|
if (obj.type === 'ai-title' && obj.aiTitle) { sm.title = obj.aiTitle; continue; }
|
||||||
|
if (obj.type !== 'user' && obj.type !== 'assistant') continue;
|
||||||
|
|
||||||
|
if (ts && (!sm.started_at || ts < sm.started_at)) sm.started_at = ts;
|
||||||
|
if (ts && (!sm.ended_at || ts > sm.ended_at)) sm.ended_at = ts;
|
||||||
|
if (obj.gitBranch) sm.git_branch = obj.gitBranch;
|
||||||
|
if (obj.version) sm.version = obj.version;
|
||||||
|
sm.n++;
|
||||||
|
|
||||||
|
const msg = obj.message || {};
|
||||||
|
const text = extractText(msg.content);
|
||||||
|
const usage = msg.usage || {};
|
||||||
|
const aid = fi.isSubagent ? fi.agentId : (obj.agentId || null);
|
||||||
|
|
||||||
|
if (obj.uuid) {
|
||||||
|
ins.msg.run(obj.uuid, sid, obj.type, obj.parentUuid || null, ts,
|
||||||
|
msg.role || obj.type, text, msg.model || null,
|
||||||
|
obj.isSidechain ? 1 : 0, aid, usage.input_tokens || null, usage.output_tokens || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'assistant' && Array.isArray(msg.content)) {
|
||||||
|
for (const b of msg.content) {
|
||||||
|
if (b.type === 'tool_use' && b.id)
|
||||||
|
ins.tc.run(b.id, obj.uuid, sid, b.name, trunc(JSON.stringify(b.input || {})), filePath(b.name, b.input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.type === 'user' && Array.isArray(msg.content)) {
|
||||||
|
for (const b of msg.content) {
|
||||||
|
if (b.type !== 'tool_result' || !b.tool_use_id) continue;
|
||||||
|
const rt = typeof b.content === 'string' ? b.content
|
||||||
|
: Array.isArray(b.content) ? b.content.map(c => c.text || '').join('\n') : '';
|
||||||
|
ins.tr.run(b.tool_use_id, obj.uuid, sid, trunc(rt), obj.toolUseResult?.filePath || null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fi.isSubagent) {
|
||||||
|
const pp = '/' + fi.project.replace(/-/g, '/').replace(/^\//, '');
|
||||||
|
ins.ses.run(fi.sessionId, sm.title, fi.project, pp, sm.started_at, sm.ended_at, sm.git_branch, sm.version, sm.n, fi.path);
|
||||||
|
}
|
||||||
|
ins.idx.run(fi.path, mt, lines.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexSubagentMeta(db, fi) {
|
||||||
|
if (!fi.isSubagent) return;
|
||||||
|
const mp = fi.path.replace('.jsonl', '.meta.json');
|
||||||
|
if (!fs.existsSync(mp)) return;
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(fs.readFileSync(mp, 'utf8'));
|
||||||
|
const tok = db.prepare('SELECT COALESCE(SUM(input_tokens),0)+COALESCE(SUM(output_tokens),0) as t FROM messages WHERE agent_id=?').get(fi.agentId);
|
||||||
|
const ts = db.prepare('SELECT MIN(timestamp) as t0, MAX(timestamp) as t1 FROM messages WHERE agent_id=?').get(fi.agentId);
|
||||||
|
const dur = ts?.t0 && ts?.t1 ? new Date(ts.t1).getTime() - new Date(ts.t0).getTime() : null;
|
||||||
|
if (fi.workflowRunId) {
|
||||||
|
db.prepare('INSERT OR REPLACE INTO workflow_agents VALUES(?,?,?,?,?)').run(fi.agentId, fi.workflowRunId, fi.sessionId, meta.agentType||null, meta.description||null);
|
||||||
|
} else {
|
||||||
|
db.prepare('INSERT OR REPLACE INTO subagents VALUES(?,?,?,?,?,?,?)').run(fi.agentId, fi.sessionId, meta.toolUseId||null, meta.agentType||null, meta.description||null, dur, tok?.t||0);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexWorkflows(db) {
|
||||||
|
if (!fs.existsSync(PROJECTS_DIR)) return;
|
||||||
|
for (const proj of fs.readdirSync(PROJECTS_DIR)) {
|
||||||
|
const pp = path.join(PROJECTS_DIR, proj);
|
||||||
|
if (!isDir(pp)) continue;
|
||||||
|
for (const sd of fs.readdirSync(pp)) {
|
||||||
|
const wd = path.join(pp, sd, 'workflows');
|
||||||
|
if (!isDir(wd)) continue;
|
||||||
|
for (const f of fs.readdirSync(wd)) {
|
||||||
|
if (!f.endsWith('.json')) continue;
|
||||||
|
try {
|
||||||
|
const wf = JSON.parse(fs.readFileSync(path.join(wd, f), 'utf8'));
|
||||||
|
if (!wf.runId) continue;
|
||||||
|
const ac = db.prepare('SELECT COUNT(*) as c FROM workflow_agents WHERE run_id=?').get(wf.runId);
|
||||||
|
db.prepare('INSERT OR REPLACE INTO workflows VALUES(?,?,?,?,?,?,?)').run(
|
||||||
|
wf.runId, sd, wf.taskId||null, trunc(wf.script||null),
|
||||||
|
wf.result ? trunc(JSON.stringify(wf.result)) : null, wf.timestamp||null, ac?.c||0);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexHistory(db) {
|
||||||
|
if (!fs.existsSync(HISTORY_PATH)) return;
|
||||||
|
for (const line of fs.readFileSync(HISTORY_PATH, 'utf8').split('\n').filter(Boolean)) {
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(line);
|
||||||
|
if (o.sessionId && o.title) db.prepare('UPDATE sessions SET title=? WHERE id=? AND title IS NULL').run(o.title, o.sessionId);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndex() {
|
||||||
|
const db = openDb();
|
||||||
|
const files = discoverJsonlFiles();
|
||||||
|
db.exec('BEGIN');
|
||||||
|
try {
|
||||||
|
for (const f of files) { indexJsonl(db, f); indexSubagentMeta(db, f); }
|
||||||
|
indexWorkflows(db);
|
||||||
|
indexHistory(db);
|
||||||
|
db.exec("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')");
|
||||||
|
db.exec('COMMIT');
|
||||||
|
} catch (e) { db.exec('ROLLBACK'); throw e; }
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Query API ---
|
||||||
|
|
||||||
|
const ERROR_PATS = ['error','Error','ENOENT','failed','Failed','FAILED','permission denied','Permission denied','EPERM','EACCES','command not found','No such file','Exit code'];
|
||||||
|
|
||||||
|
function createQueryApi(db) {
|
||||||
|
const q = (sql, ...p) => db.prepare(sql).all(...p);
|
||||||
|
|
||||||
|
const search = (text, opts = {}) => {
|
||||||
|
const { limit = 20, sessionId, project, after, before } = opts;
|
||||||
|
let where = 'WHERE mf.text MATCH ?';
|
||||||
|
const p = [text];
|
||||||
|
if (sessionId) { where += ' AND mf.session_id=?'; p.push(sessionId); }
|
||||||
|
if (project) { where += ' AND s.project=?'; p.push(project); }
|
||||||
|
if (after) { where += ' AND m.timestamp>?'; p.push(after); }
|
||||||
|
if (before) { where += ' AND m.timestamp<?'; p.push(before); }
|
||||||
|
p.push(limit);
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT m.uuid,m.session_id,m.text,m.role,m.timestamp,m.model,
|
||||||
|
s.id as s_id,s.title as s_title,s.project as s_project,s.started_at as s_started
|
||||||
|
FROM messages_fts mf JOIN messages m ON m.uuid=mf.uuid LEFT JOIN sessions s ON s.id=m.session_id
|
||||||
|
${where} ORDER BY rank LIMIT ?`).all(...p);
|
||||||
|
return rows.map(r => {
|
||||||
|
const ctx = db.prepare(
|
||||||
|
'SELECT uuid,text,role,timestamp,model FROM messages WHERE session_id=? AND uuid!=? ORDER BY ABS(JULIANDAY(timestamp)-JULIANDAY(?)) LIMIT 6'
|
||||||
|
).all(r.session_id, r.uuid, r.timestamp).sort((a,b) => a.timestamp < b.timestamp ? -1 : 1);
|
||||||
|
return {
|
||||||
|
message: { uuid: r.uuid, text: r.text, role: r.role, timestamp: r.timestamp, model: r.model },
|
||||||
|
session: { id: r.s_id, title: r.s_title, project: r.s_project, started_at: r.s_started },
|
||||||
|
context: ctx,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = (uuid) => {
|
||||||
|
const msg = db.prepare('SELECT * FROM messages WHERE uuid=?').get(uuid);
|
||||||
|
if (!msg) return null;
|
||||||
|
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(msg.session_id);
|
||||||
|
const chain = [];
|
||||||
|
let cur = msg;
|
||||||
|
while (cur?.parent_uuid) { cur = db.prepare('SELECT * FROM messages WHERE uuid=?').get(cur.parent_uuid); if (cur) chain.unshift(cur); }
|
||||||
|
let subagent = msg.agent_id ? db.prepare('SELECT * FROM subagents WHERE agent_id=?').get(msg.agent_id) : null;
|
||||||
|
let workflow = null;
|
||||||
|
if (msg.agent_id) {
|
||||||
|
const wa = db.prepare('SELECT * FROM workflow_agents WHERE agent_id=?').get(msg.agent_id);
|
||||||
|
if (wa) workflow = db.prepare('SELECT * FROM workflows WHERE run_id=?').get(wa.run_id);
|
||||||
|
}
|
||||||
|
return { message: msg, parentChain: chain, session, subagent, workflow };
|
||||||
|
};
|
||||||
|
|
||||||
|
const trace = (uuid) => {
|
||||||
|
const chain = [];
|
||||||
|
let cur = db.prepare('SELECT * FROM messages WHERE uuid=?').get(uuid);
|
||||||
|
while (cur) { chain.unshift(cur); cur = cur.parent_uuid ? db.prepare('SELECT * FROM messages WHERE uuid=?').get(cur.parent_uuid) : null; }
|
||||||
|
return chain;
|
||||||
|
};
|
||||||
|
|
||||||
|
const thread = (sid) => db.prepare('SELECT * FROM messages WHERE session_id=? ORDER BY timestamp').all(sid);
|
||||||
|
|
||||||
|
const subagents = (sid) => {
|
||||||
|
return db.prepare('SELECT * FROM subagents WHERE session_id=?').all(sid).map(r => {
|
||||||
|
const c = db.prepare('SELECT COUNT(*) as c FROM messages WHERE agent_id=?').get(r.agent_id);
|
||||||
|
return { ...r, messageCount: c?.c || 0 };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflows = (sid) => sid
|
||||||
|
? db.prepare('SELECT * FROM workflows WHERE session_id=? ORDER BY timestamp').all(sid)
|
||||||
|
: db.prepare('SELECT * FROM workflows ORDER BY timestamp DESC').all();
|
||||||
|
|
||||||
|
const workflowTree = (runId) => {
|
||||||
|
const wf = db.prepare('SELECT * FROM workflows WHERE run_id=?').get(runId);
|
||||||
|
if (!wf) return null;
|
||||||
|
const agents = db.prepare('SELECT * FROM workflow_agents WHERE run_id=?').all(runId).map(a => ({
|
||||||
|
...a, messages: db.prepare('SELECT * FROM messages WHERE agent_id=? ORDER BY timestamp').all(a.agent_id),
|
||||||
|
}));
|
||||||
|
return { ...wf, agents };
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileHistory = (fp) => {
|
||||||
|
return db.prepare(
|
||||||
|
'SELECT tc.*,s.title as s_title,s.project as s_project FROM tool_calls tc LEFT JOIN sessions s ON s.id=tc.session_id WHERE tc.file_path=? ORDER BY tc.id'
|
||||||
|
).all(fp).map(r => ({
|
||||||
|
toolCall: { id: r.id, message_uuid: r.message_uuid, name: r.name, input_json: r.input_json },
|
||||||
|
session: { id: r.session_id, title: r.s_title, project: r.s_project },
|
||||||
|
timestamp: db.prepare('SELECT timestamp FROM messages WHERE uuid=?').get(r.message_uuid)?.timestamp,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const failures = (sid) => {
|
||||||
|
const rows = sid
|
||||||
|
? db.prepare('SELECT * FROM tool_results WHERE session_id=?').all(sid)
|
||||||
|
: db.prepare('SELECT * FROM tool_results').all();
|
||||||
|
const out = [];
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!r.content || !ERROR_PATS.some(p => r.content.includes(p))) continue;
|
||||||
|
const tc = db.prepare('SELECT * FROM tool_calls WHERE id=?').get(r.tool_use_id);
|
||||||
|
const session = db.prepare('SELECT * FROM sessions WHERE id=?').get(r.session_id);
|
||||||
|
const rm = db.prepare('SELECT * FROM messages WHERE uuid=?').get(r.message_uuid);
|
||||||
|
const next = rm?.timestamp ? db.prepare('SELECT * FROM messages WHERE session_id=? AND timestamp>? ORDER BY timestamp LIMIT 3').all(r.session_id, rm.timestamp) : [];
|
||||||
|
out.push({ toolCall: tc, result: r, session, nextMessages: next });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const recent = (n = 10) => db.prepare('SELECT * FROM sessions ORDER BY ended_at DESC LIMIT ?').all(n);
|
||||||
|
|
||||||
|
return { sql: q, search, context, trace, thread, subagents, workflows, workflowTree, fileHistory, failures, recent };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Script executor ---
|
||||||
|
|
||||||
|
function executeQuery(db, scriptContent) {
|
||||||
|
const api = createQueryApi(db);
|
||||||
|
const sandbox = {
|
||||||
|
...api, JSON, Math, Array, Object, Set, Map, Date, RegExp,
|
||||||
|
parseInt, parseFloat, String, Number, Boolean, Error, Promise, console, setTimeout,
|
||||||
|
};
|
||||||
|
const ctx = vm.createContext(sandbox);
|
||||||
|
return vm.runInNewContext(`(async()=>{${scriptContent}})()`, ctx, { timeout: 30000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CLI ---
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args[0] === '--build') {
|
||||||
|
buildIndex();
|
||||||
|
process.stdout.write(JSON.stringify({ ok: true, db: DB_PATH }) + '\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[0] === '--search' && args[1]) {
|
||||||
|
buildIndex();
|
||||||
|
const db = openDb();
|
||||||
|
process.stdout.write(JSON.stringify(createQueryApi(db).search(args.slice(1).join(' ')), null, 2) + '\n');
|
||||||
|
db.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args[0] === '--query' && args[1]) {
|
||||||
|
buildIndex();
|
||||||
|
const db = openDb();
|
||||||
|
const script = fs.readFileSync(path.resolve(args[1]), 'utf8');
|
||||||
|
executeQuery(db, script)
|
||||||
|
.then(r => { process.stdout.write(JSON.stringify(r, null, 2) + '\n'); db.close(); })
|
||||||
|
.catch(e => { process.stdout.write(JSON.stringify({ error: e.message, stack: e.stack }) + '\n'); db.close(); process.exitCode = 1; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stderr.write('Usage:\n node runtime.mjs --build\n node runtime.mjs --search "text"\n node runtime.mjs --query <file.js>\n');
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user