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.
Files changed (66) hide show
  1. {spooling-0.2.0/spooling.egg-info → spooling-0.2.1}/PKG-INFO +34 -61
  2. {spooling-0.2.0 → spooling-0.2.1}/README.md +33 -60
  3. {spooling-0.2.0 → spooling-0.2.1}/pyproject.toml +1 -1
  4. {spooling-0.2.0 → spooling-0.2.1}/spooling/agent.py +2 -2
  5. {spooling-0.2.0 → spooling-0.2.1}/spooling/cloud.py +12 -3
  6. {spooling-0.2.0 → spooling-0.2.1}/spooling/db.py +4 -0
  7. {spooling-0.2.0 → spooling-0.2.1}/spooling/ingest.py +132 -89
  8. {spooling-0.2.0 → spooling-0.2.1}/spooling/parser.py +7 -2
  9. {spooling-0.2.0 → spooling-0.2.1/spooling.egg-info}/PKG-INFO +34 -61
  10. {spooling-0.2.0 → spooling-0.2.1}/LICENSE +0 -0
  11. {spooling-0.2.0 → spooling-0.2.1}/setup.cfg +0 -0
  12. {spooling-0.2.0 → spooling-0.2.1}/spooling/__init__.py +0 -0
  13. {spooling-0.2.0 → spooling-0.2.1}/spooling/classifiers.py +0 -0
  14. {spooling-0.2.0 → spooling-0.2.1}/spooling/cli.py +0 -0
  15. {spooling-0.2.0 → spooling-0.2.1}/spooling/config.py +0 -0
  16. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/__init__.py +0 -0
  17. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/base.py +0 -0
  18. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/bigquery.py +0 -0
  19. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/clickhouse.py +0 -0
  20. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/factory.py +0 -0
  21. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/mongodb.py +0 -0
  22. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/mysql.py +0 -0
  23. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/postgresql.py +0 -0
  24. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/rest/__init__.py +0 -0
  25. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/rest/base.py +0 -0
  26. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/rest/shopify_connectors.py +0 -0
  27. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/snowflake.py +0 -0
  28. {spooling-0.2.0 → spooling-0.2.1}/spooling/connectors/types.py +0 -0
  29. {spooling-0.2.0 → spooling-0.2.1}/spooling/embeddings.py +0 -0
  30. {spooling-0.2.0 → spooling-0.2.1}/spooling/evals.py +0 -0
  31. {spooling-0.2.0 → spooling-0.2.1}/spooling/experiments.py +0 -0
  32. {spooling-0.2.0 → spooling-0.2.1}/spooling/mcp_server.py +0 -0
  33. {spooling-0.2.0 → spooling-0.2.1}/spooling/pricing.py +0 -0
  34. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/__init__.py +0 -0
  35. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/antigravity.py +0 -0
  36. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/base.py +0 -0
  37. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/codex.py +0 -0
  38. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/copilot.py +0 -0
  39. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/cortex_code.py +0 -0
  40. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/cursor.py +0 -0
  41. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/gemini.py +0 -0
  42. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/github.py +0 -0
  43. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/gitlab.py +0 -0
  44. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/kiro.py +0 -0
  45. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/opencode.py +0 -0
  46. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/session_file.py +0 -0
  47. {spooling-0.2.0 → spooling-0.2.1}/spooling/providers/windsurf.py +0 -0
  48. {spooling-0.2.0 → spooling-0.2.1}/spooling/redact.py +0 -0
  49. {spooling-0.2.0 → spooling-0.2.1}/spooling/remote_otel.py +0 -0
  50. {spooling-0.2.0 → spooling-0.2.1}/spooling/schema.py +0 -0
  51. {spooling-0.2.0 → spooling-0.2.1}/spooling/sdk.py +0 -0
  52. {spooling-0.2.0 → spooling-0.2.1}/spooling/search.py +0 -0
  53. {spooling-0.2.0 → spooling-0.2.1}/spooling/server.py +0 -0
  54. {spooling-0.2.0 → spooling-0.2.1}/spooling/stats.py +0 -0
  55. {spooling-0.2.0 → spooling-0.2.1}/spooling/subscription_pricing.py +0 -0
  56. {spooling-0.2.0 → spooling-0.2.1}/spooling/tracing.py +0 -0
  57. {spooling-0.2.0 → spooling-0.2.1}/spooling/tunnel.py +0 -0
  58. {spooling-0.2.0 → spooling-0.2.1}/spooling/tunnel_auth.py +0 -0
  59. {spooling-0.2.0 → spooling-0.2.1}/spooling/watcher.py +0 -0
  60. {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/SOURCES.txt +0 -0
  61. {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/dependency_links.txt +0 -0
  62. {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/entry_points.txt +0 -0
  63. {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/requires.txt +0 -0
  64. {spooling-0.2.0 → spooling-0.2.1}/spooling.egg-info/top_level.txt +0 -0
  65. {spooling-0.2.0 → spooling-0.2.1}/tests/test_mcp_auth_middleware.py +0 -0
  66. {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.0
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 via pgvector, and a built-in AI chat agent to explore your history.
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   Clone & start the database
61
+ ### 01   Install
62
62
 
63
63
  ```bash
64
- git clone https://github.com/sashimiboi/spooling && cd spooling
65
- docker compose up -d # postgres + pgvector :5434
66
- ```
67
-
68
- ### 02   Install backend + UI
64
+ # pip
65
+ pip install spooling
69
66
 
70
- Choose one:
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 sync
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
- ### 03   Detect providers & sync
74
+ ### 02   Detect providers & sync
91
75
 
92
76
  ```bash
93
77
  spooling init # scan for available providers
94
- spooling sync # embed every session into pgvector
78
+ spooling sync # embed every session for semantic search
95
79
  ```
96
80
 
97
- ### 04   Search & explore
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 ui` automatically 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`.
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 into pgvector for semantic search.
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 only (for when you want to run the GUI separately).
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 - retrieves relevant context from pgvector before answering.
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/ # Python backend
533
+ ├── spooling/ # Python backend
559
534
  │ ├── cli.py # Click CLI
560
535
  │ ├── config.py # Configuration
561
- │ ├── db.py # Database connection
562
- │ ├── providers/ # Provider plugins (codex, copilot, cursor, windsurf)
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 # pgvector semantic search
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 | PostgreSQL 16 + pgvector (Docker) |
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 - defaults work out of the box for local development.
579
+ All optional — defaults work out of the box.
604
580
 
605
581
  | Variable | Default | Description |
606
582
  |----------|---------|-------------|
607
- | `SPOOLING_DB_HOST` | `localhost` | Database host |
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
- | `ANTHROPIC_API_KEY` | - | Anthropic API key (alternative to setting in UI) |
615
- | `API_URL` | `http://127.0.0.1:3002` | Backend API URL for Next.js rewrites (`/api/*`→`<API_URL>/api/*`). Set to `https://api.spooling.ai` in production. |
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 via pgvector, and a built-in AI chat agent to explore your history.
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 &nbsp; Clone & start the database
25
+ ### 01 &nbsp; Install
26
26
 
27
27
  ```bash
28
- git clone https://github.com/sashimiboi/spooling && cd spooling
29
- docker compose up -d # postgres + pgvector :5434
30
- ```
31
-
32
- ### 02 &nbsp; Install backend + UI
28
+ # pip
29
+ pip install spooling
33
30
 
34
- Choose one:
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 sync
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
- ### 03 &nbsp; Detect providers & sync
38
+ ### 02 &nbsp; Detect providers & sync
55
39
 
56
40
  ```bash
57
41
  spooling init # scan for available providers
58
- spooling sync # embed every session into pgvector
42
+ spooling sync # embed every session for semantic search
59
43
  ```
60
44
 
61
- ### 04 &nbsp; Search & explore
45
+ ### 03 &nbsp; 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 ui` automatically 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`.
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 into pgvector for semantic search.
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 only (for when you want to run the GUI separately).
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 - retrieves relevant context from pgvector before answering.
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/ # Python backend
497
+ ├── spooling/ # Python backend
523
498
  │ ├── cli.py # Click CLI
524
499
  │ ├── config.py # Configuration
525
- │ ├── db.py # Database connection
526
- │ ├── providers/ # Provider plugins (codex, copilot, cursor, windsurf)
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 # pgvector semantic search
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 | PostgreSQL 16 + pgvector (Docker) |
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 - defaults work out of the box for local development.
543
+ All optional — defaults work out of the box.
568
544
 
569
545
  | Variable | Default | Description |
570
546
  |----------|---------|-------------|
571
- | `SPOOLING_DB_HOST` | `localhost` | Database host |
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
- | `ANTHROPIC_API_KEY` | - | Anthropic API key (alternative to setting in UI) |
579
- | `API_URL` | `http://127.0.0.1:3002` | Backend API URL for Next.js rewrites (`/api/*`→`<API_URL>/api/*`). Set to `https://api.spooling.ai` in production. |
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spooling"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Local session tracker and semantic search for AI coding assistants"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Parsed Analytics, Inc." }]
@@ -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"].isoformat() if r["started_at"] else None,
183
- "ended_at": r["ended_at"].isoformat() if r["ended_at"] else None,
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"].isoformat() if m["timestamp"] else None,
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
- # Upsert messages
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
- conn.execute(
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
- msg.uuid, session.session_id, msg.role, _scrub(msg.content),
106
- msg.timestamp, json.dumps(msg.tools_used), _scrub(msg.cwd),
107
- _scrub(msg.git_branch), msg.estimated_tokens,
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
- # Insert spans in sequence order so parent rows always exist first.
199
- for span in sorted(trace.spans, key=lambda s: s.sequence):
200
- conn.execute(
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
- span.id, span.trace_id, span.parent_id, span.kind.value, _scrub(span.name), span.status.value,
212
- span.started_at, span.ended_at, span.duration_ms, span.depth, span.sequence,
213
- span.input_tokens, span.output_tokens, span.cache_read_tokens, span.cache_write_tokens,
214
- span.cost_usd, span.model, _scrub(span.tool_name),
215
- json.dumps(span.tool_input, default=str) if span.tool_input is not None else None,
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 _embed_session(conn, session: ParsedSession):
276
- """Chunk and embed session messages, storing vectors as BLOBs."""
277
- # Delete existing chunks for this session (re-embed on update)
278
- conn.execute("DELETE FROM chunks WHERE session_id = %s", (session.session_id,))
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
- chunks = chunk_text(msg.content)
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
- for chunk, vec, meta in zip(all_chunks, vectors, chunk_meta):
304
- conn.execute(
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 f in to_process:
467
- sessions = provider.parse_session_file(f)
468
- for session in sessions:
469
- _store_session(conn, session)
470
- if session.trace is not None:
471
- _store_trace(conn, session.trace)
472
- total_messages += session.message_count
473
- total_sessions += 1
474
-
475
- if embed:
476
- chunks = _embed_session(conn, session)
477
- total_chunks += chunks
478
-
479
- _mark_synced(conn, str(f), f.stat().st_size, prov_info["type"])
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
- return f"{path}:{offset}-{offset + limit}"
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.0
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 via pgvector, and a built-in AI chat agent to explore your history.
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 &nbsp; Clone & start the database
61
+ ### 01 &nbsp; Install
62
62
 
63
63
  ```bash
64
- git clone https://github.com/sashimiboi/spooling && cd spooling
65
- docker compose up -d # postgres + pgvector :5434
66
- ```
67
-
68
- ### 02 &nbsp; Install backend + UI
64
+ # pip
65
+ pip install spooling
69
66
 
70
- Choose one:
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 sync
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
- ### 03 &nbsp; Detect providers & sync
74
+ ### 02 &nbsp; Detect providers & sync
91
75
 
92
76
  ```bash
93
77
  spooling init # scan for available providers
94
- spooling sync # embed every session into pgvector
78
+ spooling sync # embed every session for semantic search
95
79
  ```
96
80
 
97
- ### 04 &nbsp; Search & explore
81
+ ### 03 &nbsp; 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 ui` automatically 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`.
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 into pgvector for semantic search.
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 only (for when you want to run the GUI separately).
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 - retrieves relevant context from pgvector before answering.
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/ # Python backend
533
+ ├── spooling/ # Python backend
559
534
  │ ├── cli.py # Click CLI
560
535
  │ ├── config.py # Configuration
561
- │ ├── db.py # Database connection
562
- │ ├── providers/ # Provider plugins (codex, copilot, cursor, windsurf)
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 # pgvector semantic search
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 | PostgreSQL 16 + pgvector (Docker) |
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 - defaults work out of the box for local development.
579
+ All optional — defaults work out of the box.
604
580
 
605
581
  | Variable | Default | Description |
606
582
  |----------|---------|-------------|
607
- | `SPOOLING_DB_HOST` | `localhost` | Database host |
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
- | `ANTHROPIC_API_KEY` | - | Anthropic API key (alternative to setting in UI) |
615
- | `API_URL` | `http://127.0.0.1:3002` | Backend API URL for Next.js rewrites (`/api/*`→`<API_URL>/api/*`). Set to `https://api.spooling.ai` in production. |
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