spooling 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {spooling-0.2.0/spooling.egg-info → spooling-0.2.1}/PKG-INFO +34 -61
- {spooling-0.2.0 → spooling-0.2.1}/README.md +33 -60
- {spooling-0.2.0 → spooling-0.2.1}/pyproject.toml +1 -1
- {spooling-0.2.0 → spooling-0.2.1}/spooling/agent.py +2 -2
- {spooling-0.2.0 → spooling-0.2.1}/spooling/cloud.py +12 -3
- {spooling-0.2.0 → spooling-0.2.1}/spooling/db.py +4 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/ingest.py +132 -89
- {spooling-0.2.0 → spooling-0.2.1}/spooling/parser.py +7 -2
- {spooling-0.2.0 → spooling-0.2.1/spooling.egg-info}/PKG-INFO +34 -61
- {spooling-0.2.0 → spooling-0.2.1}/LICENSE +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/setup.cfg +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/__init__.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/classifiers.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/cli.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/config.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/__init__.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/base.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/bigquery.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/clickhouse.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/factory.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/mongodb.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/mysql.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/postgresql.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/rest/__init__.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/rest/base.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/rest/shopify_connectors.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/snowflake.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/types.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/embeddings.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/evals.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/experiments.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/mcp_server.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/pricing.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/__init__.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/antigravity.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/base.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/codex.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/copilot.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/cursor.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/gemini.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/github.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/gitlab.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/kiro.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/opencode.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/session_file.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/windsurf.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/redact.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/remote_otel.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/schema.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/sdk.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/search.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/server.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/stats.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/subscription_pricing.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/tracing.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/tunnel.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/tunnel_auth.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling/watcher.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/SOURCES.txt +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/requires.txt +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/top_level.txt +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/tests/test_mcp_auth_middleware.py +0 -0
- {spooling-0.2.0 → spooling-0.2.1}/tests/test_tunnel_auth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spooling
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Local session tracker and semantic search for AI coding assistants
|
|
5
5
|
Author: Parsed Analytics, Inc.
|
|
6
6
|
License: MIT
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
Local session tracker and semantic search for AI coding assistants.
|
|
40
40
|
|
|
41
|
-
Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, **Cursor**, **Windsurf**, **Kiro**, **Google Antigravity**, and **opencode**, all in one place. Get usage stats, cost estimates, per-provider breakdowns, semantic search
|
|
41
|
+
Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, **Cursor**, **Windsurf**, **Kiro**, **Google Antigravity**, and **opencode**, all in one place. Get usage stats, cost estimates, per-provider breakdowns, semantic search, and a built-in AI chat agent to explore your history.
|
|
42
42
|
|
|
43
43
|
**Website:** [spooling.ai](https://spooling.ai)
|
|
44
44
|
|
|
@@ -47,67 +47,50 @@ Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, *
|
|
|
47
47
|
## Prerequisites
|
|
48
48
|
|
|
49
49
|
- **Python 3.11+**
|
|
50
|
-
- **Node.js 18+**
|
|
51
|
-
- **Docker** (for PostgreSQL + pgvector)
|
|
52
50
|
- **pipx** or **uv** (optional, alternative install methods)
|
|
53
51
|
- **Ollama** (optional, for free local AI chat) or an **Anthropic API key**
|
|
54
52
|
|
|
53
|
+
No Docker, no PostgreSQL, no Node.js required.
|
|
54
|
+
|
|
55
55
|
---
|
|
56
56
|
|
|
57
57
|
## How it works
|
|
58
58
|
|
|
59
59
|
**Four commands. Zero cloud.**
|
|
60
60
|
|
|
61
|
-
### 01
|
|
61
|
+
### 01 Install
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 02 Install backend + UI
|
|
64
|
+
# pip
|
|
65
|
+
pip install spooling
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
# pip (recommended)
|
|
74
|
-
python3 -m venv .venv && source .venv/bin/activate
|
|
75
|
-
pip install -e .
|
|
76
|
-
|
|
77
|
-
# pipx
|
|
78
|
-
pipx install .
|
|
67
|
+
# pipx (recommended — keeps it isolated)
|
|
68
|
+
pipx install spooling
|
|
79
69
|
|
|
80
70
|
# uv
|
|
81
|
-
uv
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Then install the UI:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
cd ui && npm install && cd ..
|
|
71
|
+
uv tool install spooling
|
|
88
72
|
```
|
|
89
73
|
|
|
90
|
-
###
|
|
74
|
+
### 02 Detect providers & sync
|
|
91
75
|
|
|
92
76
|
```bash
|
|
93
77
|
spooling init # scan for available providers
|
|
94
|
-
spooling sync # embed every session
|
|
78
|
+
spooling sync # embed every session for semantic search
|
|
95
79
|
```
|
|
96
80
|
|
|
97
|
-
###
|
|
81
|
+
### 03 Search & explore
|
|
98
82
|
|
|
99
83
|
```bash
|
|
100
|
-
spooling ui # API :3002 · MCP :3004 · GUI :3003
|
|
101
84
|
spooling search "that redis race condition"
|
|
85
|
+
spooling stats
|
|
86
|
+
spooling mcp # MCP server at http://127.0.0.1:3004/mcp
|
|
102
87
|
```
|
|
103
88
|
|
|
104
|
-
Open **http://localhost:3003** and you're in.
|
|
105
|
-
|
|
106
89
|
---
|
|
107
90
|
|
|
108
91
|
## Connect Spooling to your AI coding agent
|
|
109
92
|
|
|
110
|
-
`spooling
|
|
93
|
+
`spooling mcp` exposes an MCP server at `http://127.0.0.1:3004/mcp` (HTTP streamable transport). Any MCP-speaking agent can connect to it and pull context from your local KB mid-conversation. The agent gets tools like `spooling_search`, `spooling_recent_sessions`, `spooling_get_session`, `spooling_workspace_stats`, and `spooling_top_projects`.
|
|
111
94
|
|
|
112
95
|
### Cursor / Windsurf / Codex / Antigravity
|
|
113
96
|
|
|
@@ -140,12 +123,6 @@ Should return the five `spool_*` tools. If you get "connection refused", `spooli
|
|
|
140
123
|
|
|
141
124
|
## CLI Usage
|
|
142
125
|
|
|
143
|
-
All CLI commands require the venv to be active and the database running.
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
source .venv/bin/activate
|
|
147
|
-
```
|
|
148
|
-
|
|
149
126
|
### `spooling init`
|
|
150
127
|
|
|
151
128
|
Check database connection and show which AI coding tool providers are detected on your system. This scans default paths for Codex CLI, GitHub Copilot, Cursor, and Windsurf session data.
|
|
@@ -156,7 +133,7 @@ spooling init
|
|
|
156
133
|
|
|
157
134
|
### `spooling sync`
|
|
158
135
|
|
|
159
|
-
Parse and ingest sessions from all connected providers into the database. Chunks and embeds message content
|
|
136
|
+
Parse and ingest sessions from all connected providers into the local database. Chunks and embeds message content for semantic search.
|
|
160
137
|
|
|
161
138
|
```bash
|
|
162
139
|
spooling sync # Full sync with embeddings
|
|
@@ -199,7 +176,7 @@ spooling watch
|
|
|
199
176
|
|
|
200
177
|
### `spooling serve`
|
|
201
178
|
|
|
202
|
-
Start the API server
|
|
179
|
+
Start the API server.
|
|
203
180
|
|
|
204
181
|
```bash
|
|
205
182
|
spooling serve # Default: http://127.0.0.1:3002
|
|
@@ -209,10 +186,10 @@ spooling serve --host 0.0.0.0 # Bind to all interfaces
|
|
|
209
186
|
|
|
210
187
|
### `spooling ui`
|
|
211
188
|
|
|
212
|
-
Start the API server, the MCP HTTP server, and the Next.js UI together.
|
|
189
|
+
Start the API server, the MCP HTTP server, and the Next.js UI together. Requires the `ui/` directory from the [source repo](https://github.com/sashimiboi/spooling) to be present. Set `SPOOLING_UI_DIR` to point at it if running from a non-standard location.
|
|
213
190
|
|
|
214
191
|
```bash
|
|
215
|
-
spooling ui
|
|
192
|
+
spooling ui # API :3002 · MCP :3004 · GUI :3003
|
|
216
193
|
```
|
|
217
194
|
|
|
218
195
|
### `spooling mcp`
|
|
@@ -523,7 +500,7 @@ Or use `spooling ui` to start both at once.
|
|
|
523
500
|
|
|
524
501
|
## Chat Agent Setup
|
|
525
502
|
|
|
526
|
-
The chat page lets you ask questions about your coding sessions in natural language. It uses RAG
|
|
503
|
+
The chat page lets you ask questions about your coding sessions in natural language. It uses RAG — retrieves relevant context from the local database before answering.
|
|
527
504
|
|
|
528
505
|
### Option A: Ollama (free, local)
|
|
529
506
|
|
|
@@ -552,24 +529,23 @@ Available models: Sonnet, Haiku, Opus.
|
|
|
552
529
|
|
|
553
530
|
```
|
|
554
531
|
spooling/
|
|
555
|
-
├── docker-compose.yml # PostgreSQL + pgvector
|
|
556
|
-
├── init.sql # Database schema
|
|
557
532
|
├── pyproject.toml # Python package config
|
|
558
|
-
├── spooling/
|
|
533
|
+
├── spooling/ # Python backend
|
|
559
534
|
│ ├── cli.py # Click CLI
|
|
560
535
|
│ ├── config.py # Configuration
|
|
561
|
-
│ ├── db.py #
|
|
562
|
-
│ ├──
|
|
536
|
+
│ ├── db.py # SQLite connection (PostgreSQL shim)
|
|
537
|
+
│ ├── schema.py # SQLite schema DDL
|
|
538
|
+
│ ├── providers/ # Provider plugins (codex, copilot, cursor, windsurf, ...)
|
|
563
539
|
│ ├── parser.py # Session JSONL parser
|
|
564
540
|
│ ├── embeddings.py # sentence-transformers (all-MiniLM-L6-v2)
|
|
565
541
|
│ ├── ingest.py # Sync pipeline
|
|
566
|
-
│ ├── search.py #
|
|
542
|
+
│ ├── search.py # Numpy cosine similarity search
|
|
567
543
|
│ ├── stats.py # Usage statistics
|
|
568
544
|
│ ├── watcher.py # File watcher (watchdog)
|
|
569
545
|
│ ├── agent.py # Chat agent (Ollama + Anthropic)
|
|
570
546
|
│ ├── tunnel.py # Cloudflare quick tunnel support
|
|
571
547
|
│ └── server.py # FastAPI API server
|
|
572
|
-
└── ui/ # Next.js frontend
|
|
548
|
+
└── ui/ # Next.js frontend (source repo only)
|
|
573
549
|
├── next.config.js # API proxy to :3002
|
|
574
550
|
└── src/
|
|
575
551
|
├── components/ # shadcn/ui components
|
|
@@ -581,8 +557,9 @@ spooling/
|
|
|
581
557
|
|
|
582
558
|
| Layer | Technology |
|
|
583
559
|
|-------|-----------|
|
|
584
|
-
| Database |
|
|
560
|
+
| Database | SQLite (no external dependencies) |
|
|
585
561
|
| Embeddings | sentence-transformers / all-MiniLM-L6-v2 (local) |
|
|
562
|
+
| Search | numpy cosine similarity |
|
|
586
563
|
| Backend | Python, FastAPI, Click |
|
|
587
564
|
| Frontend | Next.js 14, shadcn/ui, Tailwind CSS, AG Charts |
|
|
588
565
|
| Chat AI | Ollama (local) or Anthropic API |
|
|
@@ -591,7 +568,6 @@ spooling/
|
|
|
591
568
|
|
|
592
569
|
| Service | Port |
|
|
593
570
|
|---------|------|
|
|
594
|
-
| PostgreSQL | 5434 |
|
|
595
571
|
| API Server | 3002 |
|
|
596
572
|
| GUI | 3003 |
|
|
597
573
|
| MCP Server (streamable-HTTP) | 3004 |
|
|
@@ -600,19 +576,16 @@ spooling/
|
|
|
600
576
|
|
|
601
577
|
## Environment Variables
|
|
602
578
|
|
|
603
|
-
All optional
|
|
579
|
+
All optional — defaults work out of the box.
|
|
604
580
|
|
|
605
581
|
| Variable | Default | Description |
|
|
606
582
|
|----------|---------|-------------|
|
|
607
|
-
| `
|
|
608
|
-
| `SPOOLING_DB_PORT` | `5434` | Database port |
|
|
609
|
-
| `SPOOLING_DB_NAME` | `spooling` | Database name |
|
|
610
|
-
| `SPOOLING_DB_USER` | `spooling` | Database user |
|
|
611
|
-
| `SPOOLING_DB_PASSWORD` | `spooling` | Database password |
|
|
583
|
+
| `SPOOLING_DB` | `~/.local/share/spooling/spooling.db` | Path to the SQLite database file (`%APPDATA%\spooling\spooling.db` on Windows) |
|
|
612
584
|
| `SPOOLING_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence transformer model |
|
|
613
585
|
| `SPOOLING_UI_HOST` | `127.0.0.1` | API server host |
|
|
614
|
-
| `
|
|
615
|
-
| `
|
|
586
|
+
| `SPOOLING_UI_PORT` | `3001` | API server port (used by `spooling serve`) |
|
|
587
|
+
| `SPOOLING_UI_DIR` | *(auto-detected)* | Path to the Next.js `ui/` directory (required for `spooling ui`) |
|
|
588
|
+
| `ANTHROPIC_API_KEY` | - | Anthropic API key (alternative to setting in the GUI) |
|
|
616
589
|
|
|
617
590
|
---
|
|
618
591
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local session tracker and semantic search for AI coding assistants.
|
|
4
4
|
|
|
5
|
-
Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, **Cursor**, **Windsurf**, **Kiro**, **Google Antigravity**, and **opencode**, all in one place. Get usage stats, cost estimates, per-provider breakdowns, semantic search
|
|
5
|
+
Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, **Cursor**, **Windsurf**, **Kiro**, **Google Antigravity**, and **opencode**, all in one place. Get usage stats, cost estimates, per-provider breakdowns, semantic search, and a built-in AI chat agent to explore your history.
|
|
6
6
|
|
|
7
7
|
**Website:** [spooling.ai](https://spooling.ai)
|
|
8
8
|
|
|
@@ -11,67 +11,50 @@ Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, *
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
13
|
- **Python 3.11+**
|
|
14
|
-
- **Node.js 18+**
|
|
15
|
-
- **Docker** (for PostgreSQL + pgvector)
|
|
16
14
|
- **pipx** or **uv** (optional, alternative install methods)
|
|
17
15
|
- **Ollama** (optional, for free local AI chat) or an **Anthropic API key**
|
|
18
16
|
|
|
17
|
+
No Docker, no PostgreSQL, no Node.js required.
|
|
18
|
+
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
## How it works
|
|
22
22
|
|
|
23
23
|
**Four commands. Zero cloud.**
|
|
24
24
|
|
|
25
|
-
### 01
|
|
25
|
+
### 01 Install
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 02 Install backend + UI
|
|
28
|
+
# pip
|
|
29
|
+
pip install spooling
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
# pip (recommended)
|
|
38
|
-
python3 -m venv .venv && source .venv/bin/activate
|
|
39
|
-
pip install -e .
|
|
40
|
-
|
|
41
|
-
# pipx
|
|
42
|
-
pipx install .
|
|
31
|
+
# pipx (recommended — keeps it isolated)
|
|
32
|
+
pipx install spooling
|
|
43
33
|
|
|
44
34
|
# uv
|
|
45
|
-
uv
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Then install the UI:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
cd ui && npm install && cd ..
|
|
35
|
+
uv tool install spooling
|
|
52
36
|
```
|
|
53
37
|
|
|
54
|
-
###
|
|
38
|
+
### 02 Detect providers & sync
|
|
55
39
|
|
|
56
40
|
```bash
|
|
57
41
|
spooling init # scan for available providers
|
|
58
|
-
spooling sync # embed every session
|
|
42
|
+
spooling sync # embed every session for semantic search
|
|
59
43
|
```
|
|
60
44
|
|
|
61
|
-
###
|
|
45
|
+
### 03 Search & explore
|
|
62
46
|
|
|
63
47
|
```bash
|
|
64
|
-
spooling ui # API :3002 · MCP :3004 · GUI :3003
|
|
65
48
|
spooling search "that redis race condition"
|
|
49
|
+
spooling stats
|
|
50
|
+
spooling mcp # MCP server at http://127.0.0.1:3004/mcp
|
|
66
51
|
```
|
|
67
52
|
|
|
68
|
-
Open **http://localhost:3003** and you're in.
|
|
69
|
-
|
|
70
53
|
---
|
|
71
54
|
|
|
72
55
|
## Connect Spooling to your AI coding agent
|
|
73
56
|
|
|
74
|
-
`spooling
|
|
57
|
+
`spooling mcp` exposes an MCP server at `http://127.0.0.1:3004/mcp` (HTTP streamable transport). Any MCP-speaking agent can connect to it and pull context from your local KB mid-conversation. The agent gets tools like `spooling_search`, `spooling_recent_sessions`, `spooling_get_session`, `spooling_workspace_stats`, and `spooling_top_projects`.
|
|
75
58
|
|
|
76
59
|
### Cursor / Windsurf / Codex / Antigravity
|
|
77
60
|
|
|
@@ -104,12 +87,6 @@ Should return the five `spool_*` tools. If you get "connection refused", `spooli
|
|
|
104
87
|
|
|
105
88
|
## CLI Usage
|
|
106
89
|
|
|
107
|
-
All CLI commands require the venv to be active and the database running.
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
source .venv/bin/activate
|
|
111
|
-
```
|
|
112
|
-
|
|
113
90
|
### `spooling init`
|
|
114
91
|
|
|
115
92
|
Check database connection and show which AI coding tool providers are detected on your system. This scans default paths for Codex CLI, GitHub Copilot, Cursor, and Windsurf session data.
|
|
@@ -120,7 +97,7 @@ spooling init
|
|
|
120
97
|
|
|
121
98
|
### `spooling sync`
|
|
122
99
|
|
|
123
|
-
Parse and ingest sessions from all connected providers into the database. Chunks and embeds message content
|
|
100
|
+
Parse and ingest sessions from all connected providers into the local database. Chunks and embeds message content for semantic search.
|
|
124
101
|
|
|
125
102
|
```bash
|
|
126
103
|
spooling sync # Full sync with embeddings
|
|
@@ -163,7 +140,7 @@ spooling watch
|
|
|
163
140
|
|
|
164
141
|
### `spooling serve`
|
|
165
142
|
|
|
166
|
-
Start the API server
|
|
143
|
+
Start the API server.
|
|
167
144
|
|
|
168
145
|
```bash
|
|
169
146
|
spooling serve # Default: http://127.0.0.1:3002
|
|
@@ -173,10 +150,10 @@ spooling serve --host 0.0.0.0 # Bind to all interfaces
|
|
|
173
150
|
|
|
174
151
|
### `spooling ui`
|
|
175
152
|
|
|
176
|
-
Start the API server, the MCP HTTP server, and the Next.js UI together.
|
|
153
|
+
Start the API server, the MCP HTTP server, and the Next.js UI together. Requires the `ui/` directory from the [source repo](https://github.com/sashimiboi/spooling) to be present. Set `SPOOLING_UI_DIR` to point at it if running from a non-standard location.
|
|
177
154
|
|
|
178
155
|
```bash
|
|
179
|
-
spooling ui
|
|
156
|
+
spooling ui # API :3002 · MCP :3004 · GUI :3003
|
|
180
157
|
```
|
|
181
158
|
|
|
182
159
|
### `spooling mcp`
|
|
@@ -487,7 +464,7 @@ Or use `spooling ui` to start both at once.
|
|
|
487
464
|
|
|
488
465
|
## Chat Agent Setup
|
|
489
466
|
|
|
490
|
-
The chat page lets you ask questions about your coding sessions in natural language. It uses RAG
|
|
467
|
+
The chat page lets you ask questions about your coding sessions in natural language. It uses RAG — retrieves relevant context from the local database before answering.
|
|
491
468
|
|
|
492
469
|
### Option A: Ollama (free, local)
|
|
493
470
|
|
|
@@ -516,24 +493,23 @@ Available models: Sonnet, Haiku, Opus.
|
|
|
516
493
|
|
|
517
494
|
```
|
|
518
495
|
spooling/
|
|
519
|
-
├── docker-compose.yml # PostgreSQL + pgvector
|
|
520
|
-
├── init.sql # Database schema
|
|
521
496
|
├── pyproject.toml # Python package config
|
|
522
|
-
├── spooling/
|
|
497
|
+
├── spooling/ # Python backend
|
|
523
498
|
│ ├── cli.py # Click CLI
|
|
524
499
|
│ ├── config.py # Configuration
|
|
525
|
-
│ ├── db.py #
|
|
526
|
-
│ ├──
|
|
500
|
+
│ ├── db.py # SQLite connection (PostgreSQL shim)
|
|
501
|
+
│ ├── schema.py # SQLite schema DDL
|
|
502
|
+
│ ├── providers/ # Provider plugins (codex, copilot, cursor, windsurf, ...)
|
|
527
503
|
│ ├── parser.py # Session JSONL parser
|
|
528
504
|
│ ├── embeddings.py # sentence-transformers (all-MiniLM-L6-v2)
|
|
529
505
|
│ ├── ingest.py # Sync pipeline
|
|
530
|
-
│ ├── search.py #
|
|
506
|
+
│ ├── search.py # Numpy cosine similarity search
|
|
531
507
|
│ ├── stats.py # Usage statistics
|
|
532
508
|
│ ├── watcher.py # File watcher (watchdog)
|
|
533
509
|
│ ├── agent.py # Chat agent (Ollama + Anthropic)
|
|
534
510
|
│ ├── tunnel.py # Cloudflare quick tunnel support
|
|
535
511
|
│ └── server.py # FastAPI API server
|
|
536
|
-
└── ui/ # Next.js frontend
|
|
512
|
+
└── ui/ # Next.js frontend (source repo only)
|
|
537
513
|
├── next.config.js # API proxy to :3002
|
|
538
514
|
└── src/
|
|
539
515
|
├── components/ # shadcn/ui components
|
|
@@ -545,8 +521,9 @@ spooling/
|
|
|
545
521
|
|
|
546
522
|
| Layer | Technology |
|
|
547
523
|
|-------|-----------|
|
|
548
|
-
| Database |
|
|
524
|
+
| Database | SQLite (no external dependencies) |
|
|
549
525
|
| Embeddings | sentence-transformers / all-MiniLM-L6-v2 (local) |
|
|
526
|
+
| Search | numpy cosine similarity |
|
|
550
527
|
| Backend | Python, FastAPI, Click |
|
|
551
528
|
| Frontend | Next.js 14, shadcn/ui, Tailwind CSS, AG Charts |
|
|
552
529
|
| Chat AI | Ollama (local) or Anthropic API |
|
|
@@ -555,7 +532,6 @@ spooling/
|
|
|
555
532
|
|
|
556
533
|
| Service | Port |
|
|
557
534
|
|---------|------|
|
|
558
|
-
| PostgreSQL | 5434 |
|
|
559
535
|
| API Server | 3002 |
|
|
560
536
|
| GUI | 3003 |
|
|
561
537
|
| MCP Server (streamable-HTTP) | 3004 |
|
|
@@ -564,19 +540,16 @@ spooling/
|
|
|
564
540
|
|
|
565
541
|
## Environment Variables
|
|
566
542
|
|
|
567
|
-
All optional
|
|
543
|
+
All optional — defaults work out of the box.
|
|
568
544
|
|
|
569
545
|
| Variable | Default | Description |
|
|
570
546
|
|----------|---------|-------------|
|
|
571
|
-
| `
|
|
572
|
-
| `SPOOLING_DB_PORT` | `5434` | Database port |
|
|
573
|
-
| `SPOOLING_DB_NAME` | `spooling` | Database name |
|
|
574
|
-
| `SPOOLING_DB_USER` | `spooling` | Database user |
|
|
575
|
-
| `SPOOLING_DB_PASSWORD` | `spooling` | Database password |
|
|
547
|
+
| `SPOOLING_DB` | `~/.local/share/spooling/spooling.db` | Path to the SQLite database file (`%APPDATA%\spooling\spooling.db` on Windows) |
|
|
576
548
|
| `SPOOLING_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence transformer model |
|
|
577
549
|
| `SPOOLING_UI_HOST` | `127.0.0.1` | API server host |
|
|
578
|
-
| `
|
|
579
|
-
| `
|
|
550
|
+
| `SPOOLING_UI_PORT` | `3001` | API server port (used by `spooling serve`) |
|
|
551
|
+
| `SPOOLING_UI_DIR` | *(auto-detected)* | Path to the Next.js `ui/` directory (required for `spooling ui`) |
|
|
552
|
+
| `ANTHROPIC_API_KEY` | - | Anthropic API key (alternative to setting in the GUI) |
|
|
580
553
|
|
|
581
554
|
---
|
|
582
555
|
|
|
@@ -156,7 +156,7 @@ def _build_context(query: str) -> dict:
|
|
|
156
156
|
context_parts.append("## Recent sessions")
|
|
157
157
|
for r in recent:
|
|
158
158
|
title = (r["title"] or "")[:60]
|
|
159
|
-
started = r["started_at"].isoformat()[:10] if r["started_at"] else ""
|
|
159
|
+
started = (r["started_at"] if isinstance(r["started_at"], str) else r["started_at"].isoformat())[:10] if r["started_at"] else ""
|
|
160
160
|
context_parts.append(
|
|
161
161
|
f"- [{r['id']}] {r['provider_id']} {r['project'] or ''} {started}: {title}"
|
|
162
162
|
)
|
|
@@ -508,7 +508,7 @@ async def chat_stream(
|
|
|
508
508
|
context_parts.append("## Recent sessions")
|
|
509
509
|
for r in recent:
|
|
510
510
|
title = (r["title"] or "")[:60]
|
|
511
|
-
started = r["started_at"].isoformat()[:10] if r["started_at"] else ""
|
|
511
|
+
started = (r["started_at"] if isinstance(r["started_at"], str) else r["started_at"].isoformat())[:10] if r["started_at"] else ""
|
|
512
512
|
context_parts.append(
|
|
513
513
|
f"- [{r['id']}] {r['provider_id']} {r['project'] or ''} {started}: {title}"
|
|
514
514
|
)
|
|
@@ -23,6 +23,15 @@ _MAX_TITLE = 4096
|
|
|
23
23
|
_MAX_SPAN_NAME = 512
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _to_iso(val) -> str | None:
|
|
27
|
+
"""Return an ISO-format string for *val*, whether it's already a str or a datetime."""
|
|
28
|
+
if val is None:
|
|
29
|
+
return None
|
|
30
|
+
if isinstance(val, str):
|
|
31
|
+
return val
|
|
32
|
+
return val.isoformat()
|
|
33
|
+
|
|
34
|
+
|
|
26
35
|
def _clean_str(raw: str | None, max_len: int) -> str | None:
|
|
27
36
|
if not raw:
|
|
28
37
|
return raw
|
|
@@ -179,8 +188,8 @@ the agent was launched from the home dir so the cwd
|
|
|
179
188
|
"project": r["project"],
|
|
180
189
|
"title": _clean_str(r["title"], _MAX_TITLE),
|
|
181
190
|
"cwd": r["cwd"],
|
|
182
|
-
"started_at": r["started_at"]
|
|
183
|
-
"ended_at": r["ended_at"]
|
|
191
|
+
"started_at": _to_iso(r["started_at"]),
|
|
192
|
+
"ended_at": _to_iso(r["ended_at"]),
|
|
184
193
|
"message_count": r["message_count"] or 0,
|
|
185
194
|
"tool_call_count": r["tool_call_count"] or 0,
|
|
186
195
|
"input_tokens": r["estimated_input_tokens"] or 0,
|
|
@@ -192,7 +201,7 @@ the agent was launched from the home dir so the cwd
|
|
|
192
201
|
"role": m["role"],
|
|
193
202
|
"content": (m["content"] or "")[:20000],
|
|
194
203
|
"sequence": int(m["seq"]),
|
|
195
|
-
"timestamp": m["timestamp"]
|
|
204
|
+
"timestamp": _to_iso(m["timestamp"]),
|
|
196
205
|
}
|
|
197
206
|
for m in msgs
|
|
198
207
|
],
|
|
@@ -14,6 +14,7 @@ codebase to their SQLite equivalents at query time:
|
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
16
|
import sqlite3
|
|
17
|
+
from functools import lru_cache
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
from spooling.config import DB_PATH
|
|
@@ -29,6 +30,7 @@ _CAST_RE = re.compile(r"::\w+")
|
|
|
29
30
|
_ILIKE_RE = re.compile(r"\bILIKE\b", re.IGNORECASE)
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@lru_cache(maxsize=512)
|
|
32
34
|
def _adapt_sql(sql: str) -> str:
|
|
33
35
|
"""Translate PostgreSQL SQL idioms → SQLite equivalents."""
|
|
34
36
|
sql = _PLACEHOLDER_RE.sub("?", sql)
|
|
@@ -148,6 +150,8 @@ def get_connection() -> SpoolingConnection:
|
|
|
148
150
|
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
149
151
|
raw = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
|
150
152
|
raw.execute("PRAGMA journal_mode=WAL")
|
|
153
|
+
raw.execute("PRAGMA synchronous=NORMAL") # safe with WAL; much faster than FULL
|
|
154
|
+
raw.execute("PRAGMA cache_size=-32000") # 32 MB page cache
|
|
151
155
|
raw.execute("PRAGMA foreign_keys=ON")
|
|
152
156
|
_ensure_schema(raw)
|
|
153
157
|
return SpoolingConnection(raw)
|
|
@@ -92,42 +92,52 @@ def _store_session(conn, session: ParsedSession):
|
|
|
92
92
|
),
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
-
#
|
|
95
|
+
# Batch-upsert messages and tool calls
|
|
96
|
+
msg_rows = []
|
|
97
|
+
tool_rows_full = []
|
|
98
|
+
tool_rows_simple = []
|
|
96
99
|
for msg in session.messages:
|
|
97
100
|
if not msg.uuid:
|
|
98
101
|
continue
|
|
99
|
-
|
|
102
|
+
msg_rows.append((
|
|
103
|
+
msg.uuid, session.session_id, msg.role, _scrub(msg.content),
|
|
104
|
+
msg.timestamp, json.dumps(msg.tools_used), _scrub(msg.cwd),
|
|
105
|
+
_scrub(msg.git_branch), msg.estimated_tokens,
|
|
106
|
+
))
|
|
107
|
+
if getattr(msg, "tool_details", None):
|
|
108
|
+
for td in msg.tool_details:
|
|
109
|
+
tool_rows_full.append((
|
|
110
|
+
session.session_id, msg.uuid, _scrub(td.name),
|
|
111
|
+
_scrub(td.input_summary),
|
|
112
|
+
_scrub(td.result_preview) or None,
|
|
113
|
+
msg.timestamp,
|
|
114
|
+
))
|
|
115
|
+
else:
|
|
116
|
+
for tool_name in msg.tools_used:
|
|
117
|
+
tool_rows_simple.append((
|
|
118
|
+
session.session_id, msg.uuid, _scrub(tool_name), msg.timestamp,
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
if msg_rows:
|
|
122
|
+
conn.executemany(
|
|
100
123
|
"""INSERT INTO messages (id, session_id, role, content, timestamp, tools_used,
|
|
101
124
|
cwd, git_branch, estimated_tokens)
|
|
102
125
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
103
126
|
ON CONFLICT (id) DO NOTHING""",
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
127
|
+
msg_rows,
|
|
128
|
+
)
|
|
129
|
+
if tool_rows_full:
|
|
130
|
+
conn.executemany(
|
|
131
|
+
"""INSERT INTO tool_calls (session_id, message_id, tool_name, tool_input, tool_result_preview, timestamp)
|
|
132
|
+
VALUES (%s, %s, %s, %s, %s, %s)""",
|
|
133
|
+
tool_rows_full,
|
|
134
|
+
)
|
|
135
|
+
if tool_rows_simple:
|
|
136
|
+
conn.executemany(
|
|
137
|
+
"""INSERT INTO tool_calls (session_id, message_id, tool_name, timestamp)
|
|
138
|
+
VALUES (%s, %s, %s, %s)""",
|
|
139
|
+
tool_rows_simple,
|
|
109
140
|
)
|
|
110
|
-
|
|
111
|
-
# Store tool calls (with rich details when available).
|
|
112
|
-
if getattr(msg, "tool_details", None):
|
|
113
|
-
for td in msg.tool_details:
|
|
114
|
-
conn.execute(
|
|
115
|
-
"""INSERT INTO tool_calls (session_id, message_id, tool_name, tool_input, tool_result_preview, timestamp)
|
|
116
|
-
VALUES (%s, %s, %s, %s, %s, %s)""",
|
|
117
|
-
(
|
|
118
|
-
session.session_id, msg.uuid, _scrub(td.name),
|
|
119
|
-
_scrub(td.input_summary),
|
|
120
|
-
_scrub(td.result_preview) or None,
|
|
121
|
-
msg.timestamp,
|
|
122
|
-
),
|
|
123
|
-
)
|
|
124
|
-
else:
|
|
125
|
-
for tool_name in msg.tools_used:
|
|
126
|
-
conn.execute(
|
|
127
|
-
"""INSERT INTO tool_calls (session_id, message_id, tool_name, timestamp)
|
|
128
|
-
VALUES (%s, %s, %s, %s)""",
|
|
129
|
-
(session.session_id, msg.uuid, _scrub(tool_name), msg.timestamp),
|
|
130
|
-
)
|
|
131
141
|
|
|
132
142
|
|
|
133
143
|
def _store_trace(conn, trace: Trace):
|
|
@@ -195,9 +205,26 @@ def _store_trace(conn, trace: Trace):
|
|
|
195
205
|
),
|
|
196
206
|
)
|
|
197
207
|
|
|
198
|
-
#
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
# Batch-insert spans (already in sequence order by construction) and events.
|
|
209
|
+
span_rows = []
|
|
210
|
+
event_rows = []
|
|
211
|
+
for span in trace.spans:
|
|
212
|
+
span_rows.append((
|
|
213
|
+
span.id, span.trace_id, span.parent_id, span.kind.value, _scrub(span.name), span.status.value,
|
|
214
|
+
span.started_at, span.ended_at, span.duration_ms, span.depth, span.sequence,
|
|
215
|
+
span.input_tokens, span.output_tokens, span.cache_read_tokens, span.cache_write_tokens,
|
|
216
|
+
span.cost_usd, span.model, _scrub(span.tool_name),
|
|
217
|
+
json.dumps(span.tool_input, default=str) if span.tool_input is not None else None,
|
|
218
|
+
_scrub(span.tool_output), span.tool_is_error,
|
|
219
|
+
_scrub(span.agent_type), _scrub(span.agent_prompt),
|
|
220
|
+
_scrub(span.vendor), _scrub(span.category),
|
|
221
|
+
json.dumps(span.attrs or {}, default=str),
|
|
222
|
+
))
|
|
223
|
+
for ev in span.events:
|
|
224
|
+
event_rows.append((span.id, trace.id, ev.name, ev.timestamp, json.dumps(ev.attrs or {})))
|
|
225
|
+
|
|
226
|
+
if span_rows:
|
|
227
|
+
conn.executemany(
|
|
201
228
|
"""INSERT INTO spans (
|
|
202
229
|
id, trace_id, parent_id, kind, name, status,
|
|
203
230
|
started_at, ended_at, duration_ms, depth, sequence,
|
|
@@ -205,27 +232,15 @@ def _store_trace(conn, trace: Trace):
|
|
|
205
232
|
cost_usd, model, tool_name, tool_input, tool_output, tool_is_error,
|
|
206
233
|
agent_type, agent_prompt, vendor, category, attrs
|
|
207
234
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
|
208
|
-
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
_scrub(span.tool_output), span.tool_is_error,
|
|
217
|
-
_scrub(span.agent_type), _scrub(span.agent_prompt),
|
|
218
|
-
_scrub(span.vendor), _scrub(span.category),
|
|
219
|
-
json.dumps(span.attrs or {}, default=str),
|
|
220
|
-
),
|
|
235
|
+
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
|
236
|
+
span_rows,
|
|
237
|
+
)
|
|
238
|
+
if event_rows:
|
|
239
|
+
conn.executemany(
|
|
240
|
+
"""INSERT INTO span_events (span_id, trace_id, name, timestamp, attrs)
|
|
241
|
+
VALUES (%s, %s, %s, %s, %s)""",
|
|
242
|
+
event_rows,
|
|
221
243
|
)
|
|
222
|
-
|
|
223
|
-
for ev in span.events:
|
|
224
|
-
conn.execute(
|
|
225
|
-
"""INSERT INTO span_events (span_id, trace_id, name, timestamp, attrs)
|
|
226
|
-
VALUES (%s, %s, %s, %s, %s)""",
|
|
227
|
-
(span.id, trace.id, ev.name, ev.timestamp, json.dumps(ev.attrs or {})),
|
|
228
|
-
)
|
|
229
244
|
|
|
230
245
|
# After spans land, sync the session's headline cost to the sum of its
|
|
231
246
|
# llm_call span costs. Span cost is computed from real per-turn usage
|
|
@@ -272,19 +287,14 @@ def _serialize_embedding(vec: list[float]) -> bytes:
|
|
|
272
287
|
return struct.pack(f"{len(vec)}f", *vec)
|
|
273
288
|
|
|
274
289
|
|
|
275
|
-
def
|
|
276
|
-
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
all_chunks = []
|
|
281
|
-
chunk_meta = []
|
|
282
|
-
|
|
290
|
+
def _collect_session_chunks(session: ParsedSession) -> tuple[list[str], list[dict]]:
|
|
291
|
+
"""Collect all text chunks and their metadata for a session (no embedding)."""
|
|
292
|
+
all_chunks: list[str] = []
|
|
293
|
+
chunk_meta: list[dict] = []
|
|
283
294
|
for msg in session.messages:
|
|
284
295
|
if not msg.content.strip():
|
|
285
296
|
continue
|
|
286
|
-
|
|
287
|
-
for chunk in chunks:
|
|
297
|
+
for chunk in chunk_text(msg.content):
|
|
288
298
|
all_chunks.append(chunk)
|
|
289
299
|
chunk_meta.append({
|
|
290
300
|
"session_id": session.session_id,
|
|
@@ -293,25 +303,36 @@ def _embed_session(conn, session: ParsedSession):
|
|
|
293
303
|
"project": session.project,
|
|
294
304
|
"timestamp": msg.timestamp,
|
|
295
305
|
})
|
|
306
|
+
return all_chunks, chunk_meta
|
|
296
307
|
|
|
297
|
-
if not all_chunks:
|
|
298
|
-
return 0
|
|
299
|
-
|
|
300
|
-
# Batch embed
|
|
301
|
-
vectors = embed_texts(all_chunks)
|
|
302
308
|
|
|
303
|
-
|
|
304
|
-
|
|
309
|
+
def _write_chunk_rows(conn, all_chunks: list[str], vectors, chunk_meta: list[dict]) -> None:
|
|
310
|
+
"""Write pre-embedded chunks to the DB using executemany."""
|
|
311
|
+
rows = [
|
|
312
|
+
(
|
|
313
|
+
meta["session_id"], meta["message_id"], chunk,
|
|
314
|
+
meta["role"], meta["project"],
|
|
315
|
+
meta["timestamp"].isoformat() if hasattr(meta["timestamp"], "isoformat") else meta["timestamp"],
|
|
316
|
+
_serialize_embedding(vec),
|
|
317
|
+
)
|
|
318
|
+
for chunk, vec, meta in zip(all_chunks, vectors, chunk_meta)
|
|
319
|
+
]
|
|
320
|
+
if rows:
|
|
321
|
+
conn.executemany(
|
|
305
322
|
"""INSERT INTO chunks (session_id, message_id, content, role, project, timestamp, embedding)
|
|
306
323
|
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
|
307
|
-
|
|
308
|
-
meta["session_id"], meta["message_id"], chunk,
|
|
309
|
-
meta["role"], meta["project"],
|
|
310
|
-
meta["timestamp"].isoformat() if hasattr(meta["timestamp"], "isoformat") else meta["timestamp"],
|
|
311
|
-
_serialize_embedding(vec),
|
|
312
|
-
),
|
|
324
|
+
rows,
|
|
313
325
|
)
|
|
314
326
|
|
|
327
|
+
|
|
328
|
+
def _embed_session(conn, session: ParsedSession):
|
|
329
|
+
"""Chunk and embed session messages, storing vectors as BLOBs."""
|
|
330
|
+
conn.execute("DELETE FROM chunks WHERE session_id = %s", (session.session_id,))
|
|
331
|
+
all_chunks, chunk_meta = _collect_session_chunks(session)
|
|
332
|
+
if not all_chunks:
|
|
333
|
+
return 0
|
|
334
|
+
vectors = embed_texts(all_chunks)
|
|
335
|
+
_write_chunk_rows(conn, all_chunks, vectors, chunk_meta)
|
|
315
336
|
return len(all_chunks)
|
|
316
337
|
|
|
317
338
|
|
|
@@ -454,6 +475,10 @@ def sync(embed: bool = True, provider_filter: str | None = None):
|
|
|
454
475
|
total_chunks = 0
|
|
455
476
|
total_sessions = 0
|
|
456
477
|
|
|
478
|
+
# Process files in batches: parse+store all, then embed all at once.
|
|
479
|
+
# One model.encode() call per batch instead of one per session.
|
|
480
|
+
_EMBED_BATCH = 20
|
|
481
|
+
|
|
457
482
|
with Progress(
|
|
458
483
|
SpinnerColumn(),
|
|
459
484
|
TextColumn("[progress.description]{task.description}"),
|
|
@@ -463,22 +488,40 @@ def sync(embed: bool = True, provider_filter: str | None = None):
|
|
|
463
488
|
) as progress:
|
|
464
489
|
task = progress.add_task(f"Syncing {provider.name}...", total=len(to_process))
|
|
465
490
|
|
|
466
|
-
for
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
491
|
+
for batch_start in range(0, len(to_process), _EMBED_BATCH):
|
|
492
|
+
batch_files = to_process[batch_start : batch_start + _EMBED_BATCH]
|
|
493
|
+
batch_sessions: list[tuple] = [] # (file, [sessions])
|
|
494
|
+
|
|
495
|
+
for f in batch_files:
|
|
496
|
+
sessions = provider.parse_session_file(f)
|
|
497
|
+
for session in sessions:
|
|
498
|
+
_store_session(conn, session)
|
|
499
|
+
if session.trace is not None:
|
|
500
|
+
_store_trace(conn, session.trace)
|
|
501
|
+
total_messages += session.message_count
|
|
502
|
+
total_sessions += 1
|
|
503
|
+
batch_sessions.append((f, sessions))
|
|
504
|
+
progress.advance(task)
|
|
505
|
+
|
|
506
|
+
if embed:
|
|
507
|
+
# Collect chunks from every session in the batch, then embed once.
|
|
508
|
+
all_chunks: list[str] = []
|
|
509
|
+
all_metas: list[dict] = []
|
|
510
|
+
for _f, sessions in batch_sessions:
|
|
511
|
+
for session in sessions:
|
|
512
|
+
conn.execute("DELETE FROM chunks WHERE session_id = %s", (session.session_id,))
|
|
513
|
+
c, m = _collect_session_chunks(session)
|
|
514
|
+
all_chunks.extend(c)
|
|
515
|
+
all_metas.extend(m)
|
|
516
|
+
|
|
517
|
+
if all_chunks:
|
|
518
|
+
vectors = embed_texts(all_chunks)
|
|
519
|
+
_write_chunk_rows(conn, all_chunks, vectors, all_metas)
|
|
520
|
+
total_chunks += len(all_chunks)
|
|
521
|
+
|
|
522
|
+
for f, _ in batch_sessions:
|
|
523
|
+
_mark_synced(conn, str(f), f.stat().st_size, prov_info["type"])
|
|
480
524
|
conn.commit()
|
|
481
|
-
progress.advance(task)
|
|
482
525
|
|
|
483
526
|
# Update provider stats
|
|
484
527
|
conn.execute(
|
|
@@ -95,8 +95,13 @@ def _summarize_tool_input(name: str, inp: dict) -> str:
|
|
|
95
95
|
path = inp.get("file_path", "")
|
|
96
96
|
offset = inp.get("offset")
|
|
97
97
|
limit = inp.get("limit")
|
|
98
|
-
if offset and limit:
|
|
99
|
-
|
|
98
|
+
if offset is not None and limit is not None:
|
|
99
|
+
try:
|
|
100
|
+
offset = int(offset[0]) if isinstance(offset, list) else int(offset)
|
|
101
|
+
limit = int(limit[0]) if isinstance(limit, list) else int(limit)
|
|
102
|
+
return f"{path}:{offset}-{offset + limit}"
|
|
103
|
+
except (TypeError, ValueError, IndexError):
|
|
104
|
+
pass
|
|
100
105
|
return path
|
|
101
106
|
if name in ("Edit", "Write"):
|
|
102
107
|
return inp.get("file_path", "")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spooling
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Local session tracker and semantic search for AI coding assistants
|
|
5
5
|
Author: Parsed Analytics, Inc.
|
|
6
6
|
License: MIT
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
Local session tracker and semantic search for AI coding assistants.
|
|
40
40
|
|
|
41
|
-
Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, **Cursor**, **Windsurf**, **Kiro**, **Google Antigravity**, and **opencode**, all in one place. Get usage stats, cost estimates, per-provider breakdowns, semantic search
|
|
41
|
+
Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, **Cursor**, **Windsurf**, **Kiro**, **Google Antigravity**, and **opencode**, all in one place. Get usage stats, cost estimates, per-provider breakdowns, semantic search, and a built-in AI chat agent to explore your history.
|
|
42
42
|
|
|
43
43
|
**Website:** [spooling.ai](https://spooling.ai)
|
|
44
44
|
|
|
@@ -47,67 +47,50 @@ Track your AI coding sessions across **OpenAI Codex CLI**, **GitHub Copilot**, *
|
|
|
47
47
|
## Prerequisites
|
|
48
48
|
|
|
49
49
|
- **Python 3.11+**
|
|
50
|
-
- **Node.js 18+**
|
|
51
|
-
- **Docker** (for PostgreSQL + pgvector)
|
|
52
50
|
- **pipx** or **uv** (optional, alternative install methods)
|
|
53
51
|
- **Ollama** (optional, for free local AI chat) or an **Anthropic API key**
|
|
54
52
|
|
|
53
|
+
No Docker, no PostgreSQL, no Node.js required.
|
|
54
|
+
|
|
55
55
|
---
|
|
56
56
|
|
|
57
57
|
## How it works
|
|
58
58
|
|
|
59
59
|
**Four commands. Zero cloud.**
|
|
60
60
|
|
|
61
|
-
### 01
|
|
61
|
+
### 01 Install
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 02 Install backend + UI
|
|
64
|
+
# pip
|
|
65
|
+
pip install spooling
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
# pip (recommended)
|
|
74
|
-
python3 -m venv .venv && source .venv/bin/activate
|
|
75
|
-
pip install -e .
|
|
76
|
-
|
|
77
|
-
# pipx
|
|
78
|
-
pipx install .
|
|
67
|
+
# pipx (recommended — keeps it isolated)
|
|
68
|
+
pipx install spooling
|
|
79
69
|
|
|
80
70
|
# uv
|
|
81
|
-
uv
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Then install the UI:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
cd ui && npm install && cd ..
|
|
71
|
+
uv tool install spooling
|
|
88
72
|
```
|
|
89
73
|
|
|
90
|
-
###
|
|
74
|
+
### 02 Detect providers & sync
|
|
91
75
|
|
|
92
76
|
```bash
|
|
93
77
|
spooling init # scan for available providers
|
|
94
|
-
spooling sync # embed every session
|
|
78
|
+
spooling sync # embed every session for semantic search
|
|
95
79
|
```
|
|
96
80
|
|
|
97
|
-
###
|
|
81
|
+
### 03 Search & explore
|
|
98
82
|
|
|
99
83
|
```bash
|
|
100
|
-
spooling ui # API :3002 · MCP :3004 · GUI :3003
|
|
101
84
|
spooling search "that redis race condition"
|
|
85
|
+
spooling stats
|
|
86
|
+
spooling mcp # MCP server at http://127.0.0.1:3004/mcp
|
|
102
87
|
```
|
|
103
88
|
|
|
104
|
-
Open **http://localhost:3003** and you're in.
|
|
105
|
-
|
|
106
89
|
---
|
|
107
90
|
|
|
108
91
|
## Connect Spooling to your AI coding agent
|
|
109
92
|
|
|
110
|
-
`spooling
|
|
93
|
+
`spooling mcp` exposes an MCP server at `http://127.0.0.1:3004/mcp` (HTTP streamable transport). Any MCP-speaking agent can connect to it and pull context from your local KB mid-conversation. The agent gets tools like `spooling_search`, `spooling_recent_sessions`, `spooling_get_session`, `spooling_workspace_stats`, and `spooling_top_projects`.
|
|
111
94
|
|
|
112
95
|
### Cursor / Windsurf / Codex / Antigravity
|
|
113
96
|
|
|
@@ -140,12 +123,6 @@ Should return the five `spool_*` tools. If you get "connection refused", `spooli
|
|
|
140
123
|
|
|
141
124
|
## CLI Usage
|
|
142
125
|
|
|
143
|
-
All CLI commands require the venv to be active and the database running.
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
source .venv/bin/activate
|
|
147
|
-
```
|
|
148
|
-
|
|
149
126
|
### `spooling init`
|
|
150
127
|
|
|
151
128
|
Check database connection and show which AI coding tool providers are detected on your system. This scans default paths for Codex CLI, GitHub Copilot, Cursor, and Windsurf session data.
|
|
@@ -156,7 +133,7 @@ spooling init
|
|
|
156
133
|
|
|
157
134
|
### `spooling sync`
|
|
158
135
|
|
|
159
|
-
Parse and ingest sessions from all connected providers into the database. Chunks and embeds message content
|
|
136
|
+
Parse and ingest sessions from all connected providers into the local database. Chunks and embeds message content for semantic search.
|
|
160
137
|
|
|
161
138
|
```bash
|
|
162
139
|
spooling sync # Full sync with embeddings
|
|
@@ -199,7 +176,7 @@ spooling watch
|
|
|
199
176
|
|
|
200
177
|
### `spooling serve`
|
|
201
178
|
|
|
202
|
-
Start the API server
|
|
179
|
+
Start the API server.
|
|
203
180
|
|
|
204
181
|
```bash
|
|
205
182
|
spooling serve # Default: http://127.0.0.1:3002
|
|
@@ -209,10 +186,10 @@ spooling serve --host 0.0.0.0 # Bind to all interfaces
|
|
|
209
186
|
|
|
210
187
|
### `spooling ui`
|
|
211
188
|
|
|
212
|
-
Start the API server, the MCP HTTP server, and the Next.js UI together.
|
|
189
|
+
Start the API server, the MCP HTTP server, and the Next.js UI together. Requires the `ui/` directory from the [source repo](https://github.com/sashimiboi/spooling) to be present. Set `SPOOLING_UI_DIR` to point at it if running from a non-standard location.
|
|
213
190
|
|
|
214
191
|
```bash
|
|
215
|
-
spooling ui
|
|
192
|
+
spooling ui # API :3002 · MCP :3004 · GUI :3003
|
|
216
193
|
```
|
|
217
194
|
|
|
218
195
|
### `spooling mcp`
|
|
@@ -523,7 +500,7 @@ Or use `spooling ui` to start both at once.
|
|
|
523
500
|
|
|
524
501
|
## Chat Agent Setup
|
|
525
502
|
|
|
526
|
-
The chat page lets you ask questions about your coding sessions in natural language. It uses RAG
|
|
503
|
+
The chat page lets you ask questions about your coding sessions in natural language. It uses RAG — retrieves relevant context from the local database before answering.
|
|
527
504
|
|
|
528
505
|
### Option A: Ollama (free, local)
|
|
529
506
|
|
|
@@ -552,24 +529,23 @@ Available models: Sonnet, Haiku, Opus.
|
|
|
552
529
|
|
|
553
530
|
```
|
|
554
531
|
spooling/
|
|
555
|
-
├── docker-compose.yml # PostgreSQL + pgvector
|
|
556
|
-
├── init.sql # Database schema
|
|
557
532
|
├── pyproject.toml # Python package config
|
|
558
|
-
├── spooling/
|
|
533
|
+
├── spooling/ # Python backend
|
|
559
534
|
│ ├── cli.py # Click CLI
|
|
560
535
|
│ ├── config.py # Configuration
|
|
561
|
-
│ ├── db.py #
|
|
562
|
-
│ ├──
|
|
536
|
+
│ ├── db.py # SQLite connection (PostgreSQL shim)
|
|
537
|
+
│ ├── schema.py # SQLite schema DDL
|
|
538
|
+
│ ├── providers/ # Provider plugins (codex, copilot, cursor, windsurf, ...)
|
|
563
539
|
│ ├── parser.py # Session JSONL parser
|
|
564
540
|
│ ├── embeddings.py # sentence-transformers (all-MiniLM-L6-v2)
|
|
565
541
|
│ ├── ingest.py # Sync pipeline
|
|
566
|
-
│ ├── search.py #
|
|
542
|
+
│ ├── search.py # Numpy cosine similarity search
|
|
567
543
|
│ ├── stats.py # Usage statistics
|
|
568
544
|
│ ├── watcher.py # File watcher (watchdog)
|
|
569
545
|
│ ├── agent.py # Chat agent (Ollama + Anthropic)
|
|
570
546
|
│ ├── tunnel.py # Cloudflare quick tunnel support
|
|
571
547
|
│ └── server.py # FastAPI API server
|
|
572
|
-
└── ui/ # Next.js frontend
|
|
548
|
+
└── ui/ # Next.js frontend (source repo only)
|
|
573
549
|
├── next.config.js # API proxy to :3002
|
|
574
550
|
└── src/
|
|
575
551
|
├── components/ # shadcn/ui components
|
|
@@ -581,8 +557,9 @@ spooling/
|
|
|
581
557
|
|
|
582
558
|
| Layer | Technology |
|
|
583
559
|
|-------|-----------|
|
|
584
|
-
| Database |
|
|
560
|
+
| Database | SQLite (no external dependencies) |
|
|
585
561
|
| Embeddings | sentence-transformers / all-MiniLM-L6-v2 (local) |
|
|
562
|
+
| Search | numpy cosine similarity |
|
|
586
563
|
| Backend | Python, FastAPI, Click |
|
|
587
564
|
| Frontend | Next.js 14, shadcn/ui, Tailwind CSS, AG Charts |
|
|
588
565
|
| Chat AI | Ollama (local) or Anthropic API |
|
|
@@ -591,7 +568,6 @@ spooling/
|
|
|
591
568
|
|
|
592
569
|
| Service | Port |
|
|
593
570
|
|---------|------|
|
|
594
|
-
| PostgreSQL | 5434 |
|
|
595
571
|
| API Server | 3002 |
|
|
596
572
|
| GUI | 3003 |
|
|
597
573
|
| MCP Server (streamable-HTTP) | 3004 |
|
|
@@ -600,19 +576,16 @@ spooling/
|
|
|
600
576
|
|
|
601
577
|
## Environment Variables
|
|
602
578
|
|
|
603
|
-
All optional
|
|
579
|
+
All optional — defaults work out of the box.
|
|
604
580
|
|
|
605
581
|
| Variable | Default | Description |
|
|
606
582
|
|----------|---------|-------------|
|
|
607
|
-
| `
|
|
608
|
-
| `SPOOLING_DB_PORT` | `5434` | Database port |
|
|
609
|
-
| `SPOOLING_DB_NAME` | `spooling` | Database name |
|
|
610
|
-
| `SPOOLING_DB_USER` | `spooling` | Database user |
|
|
611
|
-
| `SPOOLING_DB_PASSWORD` | `spooling` | Database password |
|
|
583
|
+
| `SPOOLING_DB` | `~/.local/share/spooling/spooling.db` | Path to the SQLite database file (`%APPDATA%\spooling\spooling.db` on Windows) |
|
|
612
584
|
| `SPOOLING_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Sentence transformer model |
|
|
613
585
|
| `SPOOLING_UI_HOST` | `127.0.0.1` | API server host |
|
|
614
|
-
| `
|
|
615
|
-
| `
|
|
586
|
+
| `SPOOLING_UI_PORT` | `3001` | API server port (used by `spooling serve`) |
|
|
587
|
+
| `SPOOLING_UI_DIR` | *(auto-detected)* | Path to the Next.js `ui/` directory (required for `spooling ui`) |
|
|
588
|
+
| `ANTHROPIC_API_KEY` | - | Anthropic API key (alternative to setting in the GUI) |
|
|
616
589
|
|
|
617
590
|
---
|
|
618
591
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|