spooling 0.1.5__tar.gz → 0.1.7__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.1.7/PKG-INFO +34 -0
- spooling-0.1.5/spooling.egg-info/PKG-INFO → spooling-0.1.7/README.md +106 -30
- {spooling-0.1.5 → spooling-0.1.7}/pyproject.toml +20 -14
- {spooling-0.1.5 → spooling-0.1.7}/spooling/cli.py +70 -10
- {spooling-0.1.5 → spooling-0.1.7}/spooling/cloud.py +359 -0
- spooling-0.1.7/spooling/connectors/__init__.py +0 -0
- spooling-0.1.7/spooling/connectors/base.py +37 -0
- spooling-0.1.7/spooling/connectors/bigquery.py +140 -0
- spooling-0.1.7/spooling/connectors/clickhouse.py +111 -0
- spooling-0.1.7/spooling/connectors/factory.py +68 -0
- spooling-0.1.7/spooling/connectors/mongodb.py +103 -0
- spooling-0.1.7/spooling/connectors/mysql.py +129 -0
- spooling-0.1.7/spooling/connectors/postgresql.py +120 -0
- spooling-0.1.7/spooling/connectors/rest/__init__.py +0 -0
- spooling-0.1.7/spooling/connectors/rest/base.py +206 -0
- spooling-0.1.7/spooling/connectors/rest/shopify_connectors.py +172 -0
- spooling-0.1.7/spooling/connectors/snowflake.py +155 -0
- spooling-0.1.7/spooling/connectors/types.py +87 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/mcp_server.py +266 -8
- {spooling-0.1.5 → spooling-0.1.7}/spooling/server.py +326 -0
- spooling-0.1.7/spooling/tunnel.py +185 -0
- spooling-0.1.7/spooling.egg-info/PKG-INFO +34 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/SOURCES.txt +15 -4
- {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/requires.txt +7 -0
- spooling-0.1.5/PKG-INFO +0 -523
- spooling-0.1.5/README.md +0 -493
- spooling-0.1.5/tests/test_parser.py +0 -273
- spooling-0.1.5/tests/test_pricing.py +0 -157
- spooling-0.1.5/tests/test_redact.py +0 -220
- {spooling-0.1.5 → spooling-0.1.7}/LICENSE +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/setup.cfg +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/__init__.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/agent.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/classifiers.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/config.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/db.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/embeddings.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/evals.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/experiments.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/ingest.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/parser.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/pricing.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/__init__.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/antigravity.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/base.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/codex.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/copilot.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/cursor.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/gemini.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/github.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/gitlab.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/kiro.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/opencode.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/session_file.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/windsurf.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/redact.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/remote_otel.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/sdk.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/search.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/stats.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/subscription_pricing.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/tracing.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling/watcher.py +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/top_level.txt +0 -0
spooling-0.1.7/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spooling
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: Local session tracker and semantic search for AI coding assistants
|
|
5
|
+
Author: Parsed Analytics, Inc.
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: click>=8.1
|
|
13
|
+
Requires-Dist: psycopg[binary]>=3.1
|
|
14
|
+
Requires-Dist: sentence-transformers>=3.0
|
|
15
|
+
Requires-Dist: fastapi>=0.111
|
|
16
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
17
|
+
Requires-Dist: jinja2>=3.1
|
|
18
|
+
Requires-Dist: watchdog>=4.0
|
|
19
|
+
Requires-Dist: rich>=13.0
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: anthropic>=0.40
|
|
22
|
+
Requires-Dist: strands-agents[ollama]>=1.35
|
|
23
|
+
Requires-Dist: strands-agents-evals>=0.1
|
|
24
|
+
Requires-Dist: mcp>=1.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-mock>=3.14; extra == "dev"
|
|
28
|
+
Provides-Extra: connectors
|
|
29
|
+
Requires-Dist: snowflake-connector-python>=3.0; extra == "connectors"
|
|
30
|
+
Requires-Dist: google-cloud-bigquery>=3.0; extra == "connectors"
|
|
31
|
+
Requires-Dist: mysql-connector-python>=8.0; extra == "connectors"
|
|
32
|
+
Requires-Dist: clickhouse-connect>=0.5; extra == "connectors"
|
|
33
|
+
Requires-Dist: motor>=3.0; extra == "connectors"
|
|
34
|
+
Dynamic: license-file
|
|
@@ -1,33 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: spooling
|
|
3
|
-
Version: 0.1.5
|
|
4
|
-
Summary: Local session tracker and semantic search for AI coding assistants
|
|
5
|
-
Author: Parsed Analytics, Inc.
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
-
Requires-Python: >=3.11
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
License-File: LICENSE
|
|
13
|
-
Requires-Dist: click>=8.1
|
|
14
|
-
Requires-Dist: psycopg[binary]>=3.1
|
|
15
|
-
Requires-Dist: sentence-transformers>=3.0
|
|
16
|
-
Requires-Dist: fastapi>=0.111
|
|
17
|
-
Requires-Dist: uvicorn[standard]>=0.30
|
|
18
|
-
Requires-Dist: jinja2>=3.1
|
|
19
|
-
Requires-Dist: watchdog>=4.0
|
|
20
|
-
Requires-Dist: rich>=13.0
|
|
21
|
-
Requires-Dist: httpx>=0.27
|
|
22
|
-
Requires-Dist: anthropic>=0.40
|
|
23
|
-
Requires-Dist: strands-agents[ollama]>=1.35
|
|
24
|
-
Requires-Dist: strands-agents-evals>=0.1
|
|
25
|
-
Requires-Dist: mcp>=1.27
|
|
26
|
-
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
-
Requires-Dist: pytest-mock>=3.14; extra == "dev"
|
|
29
|
-
Dynamic: license-file
|
|
30
|
-
|
|
31
1
|
# Spooling
|
|
32
2
|
|
|
33
3
|
Local session tracker and semantic search for AI coding assistants.
|
|
@@ -221,6 +191,110 @@ spooling mcp # streamable-HTTP at http://127.0.0.1:3004/mcp (defaul
|
|
|
221
191
|
spooling mcp --stdio # stdio transport, for stdio-only clients
|
|
222
192
|
```
|
|
223
193
|
|
|
194
|
+
### `spooling tunnel`
|
|
195
|
+
|
|
196
|
+
Create a Cloudflare quick tunnel (no account required) to expose your local MCP server to the internet. Once started, it prints a public HTTPS URL you can drop straight into any remote MCP client config. Requires [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) to be installed.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
spooling tunnel # Expose Spooling MCP (port 3004, default)
|
|
200
|
+
spooling tunnel --port 8090 # Expose a custom local port
|
|
201
|
+
spooling tunnel -p 3000 -n my-api # Named tunnel on port 3000
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Options:
|
|
205
|
+
- `-p, --port` - Local port to expose (default: 3004)
|
|
206
|
+
- `-n, --name` - Label for display purposes
|
|
207
|
+
|
|
208
|
+
After the tunnel starts, it prints the public URL and an MCP config snippet ready to paste:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{"mcpServers": {"spooling": {"type": "http", "url": "https://<random>.trycloudflare.com/mcp"}}}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Install `cloudflared` if not already present:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# macOS
|
|
218
|
+
brew install cloudflared
|
|
219
|
+
|
|
220
|
+
# Linux
|
|
221
|
+
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
|
|
222
|
+
-o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared
|
|
223
|
+
|
|
224
|
+
# Windows
|
|
225
|
+
winget install cloudflare.cloudflared
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `spooling cost`
|
|
229
|
+
|
|
230
|
+
Detailed cost tracking and spend analysis. All subcommands read from the local database — no network calls.
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
spooling cost overview # Total spend: input/output/cache breakdown
|
|
234
|
+
spooling cost overview --provider cursor # Filter to one provider
|
|
235
|
+
spooling cost overview --days 7 # Last 7 days only
|
|
236
|
+
|
|
237
|
+
spooling cost breakdown # Cost by provider (default)
|
|
238
|
+
spooling cost breakdown --by model # Cost by model
|
|
239
|
+
spooling cost breakdown --by project # Cost by project
|
|
240
|
+
spooling cost breakdown --by model --days 30 # Model breakdown, last 30 days
|
|
241
|
+
|
|
242
|
+
spooling cost daily # Daily trend (last 30 days)
|
|
243
|
+
spooling cost daily --days 14 # Last 14 days
|
|
244
|
+
spooling cost daily --provider codex # Filter by provider
|
|
245
|
+
|
|
246
|
+
spooling cost monthly # Monthly trend (last 12 months)
|
|
247
|
+
spooling cost monthly --months 6 # Last 6 months
|
|
248
|
+
|
|
249
|
+
spooling cost session <session-id> # Detailed cost for a single session
|
|
250
|
+
|
|
251
|
+
spooling cost recalc # Dry-run: re-price all sessions against current rates
|
|
252
|
+
spooling cost recalc --apply # Actually persist the repriced values
|
|
253
|
+
spooling cost recalc --session <id> --apply # Reprice one session
|
|
254
|
+
spooling cost recalc --provider cursor --apply # Reprice all sessions for a provider
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `spooling eval`
|
|
258
|
+
|
|
259
|
+
Run LLM-as-judge rubrics against your session traces.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
spooling eval list # List all configured rubrics
|
|
263
|
+
spooling eval run --rubric <id> --trace <id> # Score one trace
|
|
264
|
+
spooling eval run --rubric <id> --days 7 # Batch-score traces from the last 7 days
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `spooling experiment`
|
|
268
|
+
|
|
269
|
+
Create and run Strands-style experiments — a set of test cases evaluated by one or more rubrics.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
spooling experiment list # List all experiments
|
|
273
|
+
spooling experiment create --file spec.json # Register an experiment from a JSON spec
|
|
274
|
+
spooling experiment run --id <experiment-id> # Run an experiment and persist the report
|
|
275
|
+
spooling experiment show --run <run-id> # Show the results of a past run
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `spooling otel`
|
|
279
|
+
|
|
280
|
+
Ingest OpenTelemetry / Strands span exports from external sources into Spooling.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
spooling otel ingest --file spans.json # Ingest an OTLP JSON export
|
|
284
|
+
spooling otel ingest --file spans.json --provider my-agent # Tag with a custom provider id
|
|
285
|
+
spooling otel ingest --file spans.json --project myproject # Tag with a project name
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### `spooling pricing`
|
|
289
|
+
|
|
290
|
+
Manage the LiteLLM-backed model pricing table used for cost calculations.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
spooling pricing show # Show cache status and source info
|
|
294
|
+
spooling pricing show claude-sonnet-4-5 # Show per-component rates for a model ($/Mtok)
|
|
295
|
+
spooling pricing refresh # Force-fetch latest rates into ~/.spool/model_prices.json
|
|
296
|
+
```
|
|
297
|
+
|
|
224
298
|
---
|
|
225
299
|
|
|
226
300
|
## Spooling Cloud (optional)
|
|
@@ -457,6 +531,7 @@ spooling/
|
|
|
457
531
|
│ ├── stats.py # Usage statistics
|
|
458
532
|
│ ├── watcher.py # File watcher (watchdog)
|
|
459
533
|
│ ├── agent.py # Chat agent (Ollama + Anthropic)
|
|
534
|
+
│ ├── tunnel.py # Cloudflare quick tunnel support
|
|
460
535
|
│ └── server.py # FastAPI API server
|
|
461
536
|
└── ui/ # Next.js frontend
|
|
462
537
|
├── next.config.js # API proxy to :3002
|
|
@@ -521,3 +596,4 @@ All optional - defaults work out of the box for local development.
|
|
|
521
596
|
Run `spooling init` to see which providers are detected on your system.
|
|
522
597
|
|
|
523
598
|
No data is sent to external servers. Everything runs locally.
|
|
599
|
+
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spooling"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
4
4
|
description = "Local session tracker and semantic search for AI coding assistants"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
license = "MIT"
|
|
7
|
-
license-files = ["LICENSE"]
|
|
8
5
|
authors = [{ name = "Parsed Analytics, Inc." }]
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
9
8
|
classifiers = [
|
|
10
9
|
"Programming Language :: Python :: 3",
|
|
11
10
|
"Programming Language :: Python :: 3.11",
|
|
12
11
|
"Programming Language :: Python :: 3.12",
|
|
13
12
|
]
|
|
14
|
-
requires-python = ">=3.11"
|
|
15
13
|
dependencies = [
|
|
16
14
|
"click>=8.1",
|
|
17
15
|
"psycopg[binary]>=3.1",
|
|
@@ -28,21 +26,29 @@ dependencies = [
|
|
|
28
26
|
"mcp>=1.27",
|
|
29
27
|
]
|
|
30
28
|
|
|
31
|
-
[project.scripts]
|
|
32
|
-
spooling = "spooling.cli:cli"
|
|
33
|
-
|
|
34
|
-
[tool.setuptools.packages.find]
|
|
35
|
-
include = ["spooling*"]
|
|
36
|
-
|
|
37
29
|
[project.optional-dependencies]
|
|
38
30
|
dev = [
|
|
39
31
|
"pytest>=8.0",
|
|
40
32
|
"pytest-mock>=3.14",
|
|
41
33
|
]
|
|
34
|
+
connectors = [
|
|
35
|
+
"snowflake-connector-python>=3.0",
|
|
36
|
+
"google-cloud-bigquery>=3.0",
|
|
37
|
+
"mysql-connector-python>=8.0",
|
|
38
|
+
"clickhouse-connect>=0.5",
|
|
39
|
+
"motor>=3.0",
|
|
40
|
+
]
|
|
42
41
|
|
|
43
|
-
[
|
|
44
|
-
|
|
42
|
+
[project.scripts]
|
|
43
|
+
spooling = "spooling.cli:cli"
|
|
45
44
|
|
|
46
45
|
[build-system]
|
|
47
|
-
requires = ["setuptools>=68"]
|
|
46
|
+
requires = ["setuptools>=68", "wheel"]
|
|
48
47
|
build-backend = "setuptools.build_meta"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["."]
|
|
51
|
+
include = ["spooling*"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
@@ -83,17 +83,22 @@ def watch():
|
|
|
83
83
|
@click.argument("query")
|
|
84
84
|
@click.option("-n", "--limit", default=10, help="Number of results")
|
|
85
85
|
@click.option("-p", "--project", default=None, help="Filter by project")
|
|
86
|
-
|
|
86
|
+
@click.option("--cloud", "cloud_mode", is_flag=True, help="Search cloud workspace instead of local DB")
|
|
87
|
+
def search(query, limit, project, cloud_mode):
|
|
87
88
|
"""Semantic search across session history."""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
if cloud_mode:
|
|
90
|
+
from spooling.cloud import cloud_search
|
|
91
|
+
results = cloud_search(query, limit=limit, project=project)
|
|
92
|
+
else:
|
|
93
|
+
from spooling.search import search as do_search
|
|
94
|
+
results = do_search(query, limit=limit, project=project)
|
|
91
95
|
|
|
92
96
|
if not results:
|
|
93
97
|
console.print("[yellow]No results found.[/yellow]")
|
|
94
98
|
return
|
|
95
99
|
|
|
96
100
|
for i, r in enumerate(results, 1):
|
|
101
|
+
source = r.get("_source", "local")
|
|
97
102
|
similarity = f"{r['similarity']:.1%}"
|
|
98
103
|
project_name = r["project"] or "unknown"
|
|
99
104
|
role = r["role"]
|
|
@@ -104,6 +109,7 @@ def search(query, limit, project):
|
|
|
104
109
|
f"[dim]{project_name}[/dim] "
|
|
105
110
|
f"[{'green' if role == 'user' else 'blue'}]{role}[/{'green' if role == 'user' else 'blue'}] "
|
|
106
111
|
f"[dim]{ts[:19]}[/dim]"
|
|
112
|
+
+ (f" [cyan](cloud)[/cyan]" if source == "cloud" else "")
|
|
107
113
|
)
|
|
108
114
|
if r["title"]:
|
|
109
115
|
console.print(f" [dim]Session:[/dim] {r['title']}")
|
|
@@ -113,8 +119,23 @@ def search(query, limit, project):
|
|
|
113
119
|
@cli.command()
|
|
114
120
|
@click.option("--week", is_flag=True, help="Show weekly breakdown")
|
|
115
121
|
@click.option("--days", default=7, help="Number of days for daily stats")
|
|
116
|
-
|
|
122
|
+
@click.option("--cloud", "cloud_mode", is_flag=True, help="Show stats from cloud workspace")
|
|
123
|
+
def stats(week, days, cloud_mode):
|
|
117
124
|
"""Show usage statistics."""
|
|
125
|
+
if cloud_mode:
|
|
126
|
+
from spooling.cloud import cloud_stats
|
|
127
|
+
data = cloud_stats()
|
|
128
|
+
if not data:
|
|
129
|
+
console.print("[yellow]No cloud stats available. Run 'spooling cloud login' first.[/yellow]")
|
|
130
|
+
return
|
|
131
|
+
console.print(Panel(
|
|
132
|
+
f"Sessions: [bold]{data.get('sessions', 0)}[/bold] | "
|
|
133
|
+
f"Cost: [bold]${float(data.get('cost', 0)):.2f}[/bold] est.",
|
|
134
|
+
title="[bold]Spooling Cloud Overview[/bold]",
|
|
135
|
+
style="cyan",
|
|
136
|
+
))
|
|
137
|
+
return
|
|
138
|
+
|
|
118
139
|
from spooling.stats import get_overview, get_daily_stats
|
|
119
140
|
|
|
120
141
|
overview = get_overview()
|
|
@@ -657,23 +678,62 @@ def pricing_show(model):
|
|
|
657
678
|
console.print(table)
|
|
658
679
|
|
|
659
680
|
|
|
681
|
+
@cli.command()
|
|
682
|
+
@click.option("--port", "-p", default=3004, type=int, help="Local port to tunnel (default: 3004 for Spooling MCP)")
|
|
683
|
+
@click.option("--name", "-n", default=None, help="Name for this tunnel (for display)")
|
|
684
|
+
def tunnel(port, name):
|
|
685
|
+
"""Start a Cloudflare tunnel to expose a local MCP server.
|
|
686
|
+
|
|
687
|
+
Creates a quick tunnel (no account required) that exposes your local
|
|
688
|
+
MCP server to the internet. The tunnel URL can be used by any
|
|
689
|
+
MCP-compatible client.
|
|
690
|
+
|
|
691
|
+
Examples:
|
|
692
|
+
\b
|
|
693
|
+
spooling tunnel # Tunnel Spooling MCP (port 3004)
|
|
694
|
+
spooling tunnel --port 8090 # Tunnel a custom MCP server
|
|
695
|
+
spooling tunnel -p 3000 -n my-api # Named tunnel
|
|
696
|
+
"""
|
|
697
|
+
from spooling.tunnel import start_tunnel
|
|
698
|
+
|
|
699
|
+
display_name = name or f"port {port}"
|
|
700
|
+
console.print(f"[bold]Starting tunnel for {display_name}...[/bold]")
|
|
701
|
+
|
|
702
|
+
url = start_tunnel(port=port, name=name)
|
|
703
|
+
if url:
|
|
704
|
+
# Print MCP config snippet for easy copy-paste
|
|
705
|
+
console.print()
|
|
706
|
+
console.print("[dim]Add this to your MCP client config:[/dim]")
|
|
707
|
+
console.print(f'[bold]{{"mcpServers": {{"custom": {{"type": "http", "url": "{url}/mcp"}}}}}}[/bold]')
|
|
708
|
+
console.print()
|
|
709
|
+
|
|
710
|
+
|
|
660
711
|
@cli.command()
|
|
661
712
|
@click.option("--stdio", is_flag=True, help="Use stdio transport (default is streamable-HTTP)")
|
|
662
|
-
|
|
713
|
+
@click.option("--local", "local_mode", flag_value="local", default=True, help="Local DB only, no cloud fallback")
|
|
714
|
+
@click.option("--cloud", "local_mode", flag_value="cloud", help="Cloud only, skip local DB")
|
|
715
|
+
@click.option("--hybrid", "local_mode", flag_value="hybrid", help="Local DB with cloud fallback (default)")
|
|
716
|
+
def mcp(stdio, local_mode):
|
|
663
717
|
"""Launch the Spooling MCP server.
|
|
664
718
|
|
|
665
719
|
Defaults to streamable-HTTP at http://127.0.0.1:3004/mcp so any
|
|
666
720
|
MCP-compatible agent (Codex, Cursor, web agents) can
|
|
667
721
|
connect by URL. Pass --stdio for stdio-only clients.
|
|
722
|
+
|
|
723
|
+
Data source modes:
|
|
724
|
+
\b
|
|
725
|
+
--hybrid Local DB with cloud fallback (default)
|
|
726
|
+
--local Local DB only
|
|
727
|
+
--cloud Cloud only
|
|
668
728
|
"""
|
|
729
|
+
from spooling.mcp_server import set_mode, serve_http as _serve_http, serve_stdio as _serve_stdio, MCP_URL
|
|
730
|
+
set_mode(local_mode or "hybrid")
|
|
669
731
|
if stdio:
|
|
670
|
-
from spooling.mcp_server import serve_stdio
|
|
671
732
|
console.print("[bold]Spooling MCP[/bold] over stdio")
|
|
672
|
-
|
|
733
|
+
_serve_stdio()
|
|
673
734
|
else:
|
|
674
|
-
from spooling.mcp_server import serve_http, MCP_URL
|
|
675
735
|
console.print(f"[bold]Spooling MCP[/bold] at {MCP_URL}")
|
|
676
|
-
|
|
736
|
+
_serve_http()
|
|
677
737
|
|
|
678
738
|
|
|
679
739
|
def _check_ollama_preflight() -> None:
|