spooling 0.1.6__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.6/spooling.egg-info/PKG-INFO → spooling-0.1.7/README.md +106 -36
- {spooling-0.1.6 → spooling-0.1.7}/pyproject.toml +13 -14
- {spooling-0.1.6 → spooling-0.1.7}/spooling/cli.py +30 -0
- spooling-0.1.7/spooling/tunnel.py +185 -0
- spooling-0.1.7/spooling.egg-info/PKG-INFO +34 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/SOURCES.txt +2 -4
- spooling-0.1.6/PKG-INFO +0 -529
- spooling-0.1.6/README.md +0 -493
- spooling-0.1.6/tests/test_parser.py +0 -273
- spooling-0.1.6/tests/test_pricing.py +0 -157
- spooling-0.1.6/tests/test_redact.py +0 -220
- {spooling-0.1.6 → spooling-0.1.7}/LICENSE +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/setup.cfg +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/__init__.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/agent.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/classifiers.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/cloud.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/config.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/__init__.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/base.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/bigquery.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/clickhouse.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/factory.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/mongodb.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/mysql.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/postgresql.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/rest/__init__.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/rest/base.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/rest/shopify_connectors.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/snowflake.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/types.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/db.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/embeddings.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/evals.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/experiments.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/ingest.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/mcp_server.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/parser.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/pricing.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/__init__.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/antigravity.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/base.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/codex.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/copilot.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/cursor.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/gemini.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/github.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/gitlab.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/kiro.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/opencode.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/session_file.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/windsurf.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/redact.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/remote_otel.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/sdk.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/search.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/server.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/stats.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/subscription_pricing.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/tracing.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling/watcher.py +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/requires.txt +0 -0
- {spooling-0.1.6 → 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,39 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: spooling
|
|
3
|
-
Version: 0.1.6
|
|
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
|
-
Provides-Extra: connectors
|
|
30
|
-
Requires-Dist: snowflake-connector-python>=3.0; extra == "connectors"
|
|
31
|
-
Requires-Dist: google-cloud-bigquery>=3.0; extra == "connectors"
|
|
32
|
-
Requires-Dist: mysql-connector-python>=8.0; extra == "connectors"
|
|
33
|
-
Requires-Dist: clickhouse-connect>=0.5; extra == "connectors"
|
|
34
|
-
Requires-Dist: motor>=3.0; extra == "connectors"
|
|
35
|
-
Dynamic: license-file
|
|
36
|
-
|
|
37
1
|
# Spooling
|
|
38
2
|
|
|
39
3
|
Local session tracker and semantic search for AI coding assistants.
|
|
@@ -227,6 +191,110 @@ spooling mcp # streamable-HTTP at http://127.0.0.1:3004/mcp (defaul
|
|
|
227
191
|
spooling mcp --stdio # stdio transport, for stdio-only clients
|
|
228
192
|
```
|
|
229
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
|
+
|
|
230
298
|
---
|
|
231
299
|
|
|
232
300
|
## Spooling Cloud (optional)
|
|
@@ -463,6 +531,7 @@ spooling/
|
|
|
463
531
|
│ ├── stats.py # Usage statistics
|
|
464
532
|
│ ├── watcher.py # File watcher (watchdog)
|
|
465
533
|
│ ├── agent.py # Chat agent (Ollama + Anthropic)
|
|
534
|
+
│ ├── tunnel.py # Cloudflare quick tunnel support
|
|
466
535
|
│ └── server.py # FastAPI API server
|
|
467
536
|
└── ui/ # Next.js frontend
|
|
468
537
|
├── next.config.js # API proxy to :3002
|
|
@@ -527,3 +596,4 @@ All optional - defaults work out of the box for local development.
|
|
|
527
596
|
Run `spooling init` to see which providers are detected on your system.
|
|
528
597
|
|
|
529
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,12 +26,6 @@ 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",
|
|
@@ -47,9 +39,16 @@ connectors = [
|
|
|
47
39
|
"motor>=3.0",
|
|
48
40
|
]
|
|
49
41
|
|
|
50
|
-
[
|
|
51
|
-
|
|
42
|
+
[project.scripts]
|
|
43
|
+
spooling = "spooling.cli:cli"
|
|
52
44
|
|
|
53
45
|
[build-system]
|
|
54
|
-
requires = ["setuptools>=68"]
|
|
46
|
+
requires = ["setuptools>=68", "wheel"]
|
|
55
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"]
|
|
@@ -678,6 +678,36 @@ def pricing_show(model):
|
|
|
678
678
|
console.print(table)
|
|
679
679
|
|
|
680
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
|
+
|
|
681
711
|
@cli.command()
|
|
682
712
|
@click.option("--stdio", is_flag=True, help="Use stdio transport (default is streamable-HTTP)")
|
|
683
713
|
@click.option("--local", "local_mode", flag_value="local", default=True, help="Local DB only, no cloud fallback")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Cloudflare tunnel support for exposing local MCP servers."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Pattern to match Cloudflare tunnel URL (strict: only lowercase alphanumeric + hyphens)
|
|
13
|
+
TUNNEL_URL_PATTERN = re.compile(r"^https://[a-z0-9]{2,63}-[a-z0-9]{2,63}-[a-z0-9]{2,63}\.trycloudflare\.com$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_cloudflared() -> bool:
|
|
17
|
+
"""Check if cloudflared is installed."""
|
|
18
|
+
try:
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
["cloudflared", "version"],
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
timeout=5,
|
|
24
|
+
)
|
|
25
|
+
return result.returncode == 0
|
|
26
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _validate_port(port: int) -> bool:
|
|
31
|
+
"""Validate port number is in valid range."""
|
|
32
|
+
return 1 <= port <= 65535
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def start_tunnel(
|
|
36
|
+
port: int,
|
|
37
|
+
name: Optional[str] = None,
|
|
38
|
+
no_autoupdate: bool = True,
|
|
39
|
+
) -> Optional[str]:
|
|
40
|
+
"""Start a Cloudflare quick tunnel and return the public URL.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
port: Local port to expose (1-65535)
|
|
44
|
+
name: Optional name for the tunnel (for display purposes)
|
|
45
|
+
no_autoupdate: Disable cloudflared auto-updates
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The tunnel URL if successful, None otherwise
|
|
49
|
+
"""
|
|
50
|
+
if not _validate_port(port):
|
|
51
|
+
console.print(f"[red]Invalid port: {port}. Must be 1-65535.[/red]")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if not check_cloudflared():
|
|
55
|
+
console.print("[red]cloudflared is not installed.[/red]")
|
|
56
|
+
console.print("Install it:")
|
|
57
|
+
console.print(" macOS: [bold]brew install cloudflared[/bold]")
|
|
58
|
+
console.print(" Linux: [bold]curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared[/bold]")
|
|
59
|
+
console.print(" Windows: [bold]winget install cloudflare.cloudflared[/bold]")
|
|
60
|
+
console.print(" Docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
cmd = ["cloudflared", "tunnel", "--url", f"http://localhost:{port}"]
|
|
64
|
+
if no_autoupdate:
|
|
65
|
+
cmd.append("--no-autoupdate")
|
|
66
|
+
|
|
67
|
+
console.print(f"[bold]Starting Cloudflare tunnel to localhost:{port}...[/bold]")
|
|
68
|
+
|
|
69
|
+
process = None
|
|
70
|
+
try:
|
|
71
|
+
process = subprocess.Popen(
|
|
72
|
+
cmd,
|
|
73
|
+
stdout=subprocess.PIPE,
|
|
74
|
+
stderr=subprocess.STDOUT,
|
|
75
|
+
text=True,
|
|
76
|
+
bufsize=1,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Read output until we find the URL or process exits
|
|
80
|
+
url = None
|
|
81
|
+
start_time = time.time()
|
|
82
|
+
timeout = 30 # seconds
|
|
83
|
+
|
|
84
|
+
while time.time() - start_time < timeout:
|
|
85
|
+
line = process.stdout.readline()
|
|
86
|
+
if not line:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
# Print cloudflared output for visibility
|
|
90
|
+
console.print(f"[dim]{line.rstrip()}[/dim]")
|
|
91
|
+
|
|
92
|
+
# Check for tunnel URL
|
|
93
|
+
match = TUNNEL_URL_PATTERN.search(line.strip())
|
|
94
|
+
if match:
|
|
95
|
+
url = match.group(0)
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
if url:
|
|
99
|
+
console.print()
|
|
100
|
+
console.print(f"[green]Tunnel started![/green]")
|
|
101
|
+
console.print(f" URL: [bold]{url}[/bold]")
|
|
102
|
+
console.print(f" Local: http://localhost:{port}")
|
|
103
|
+
console.print()
|
|
104
|
+
console.print("Press [bold]Ctrl+C[/bold] to stop the tunnel")
|
|
105
|
+
|
|
106
|
+
# Wait for Ctrl+C
|
|
107
|
+
try:
|
|
108
|
+
process.wait()
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
console.print("\n[yellow]Stopping tunnel...[/yellow]")
|
|
111
|
+
process.terminate()
|
|
112
|
+
try:
|
|
113
|
+
process.wait(timeout=5)
|
|
114
|
+
except subprocess.TimeoutExpired:
|
|
115
|
+
process.kill()
|
|
116
|
+
console.print("[green]Tunnel stopped.[/green]")
|
|
117
|
+
else:
|
|
118
|
+
console.print("[red]Failed to get tunnel URL within timeout.[/red]")
|
|
119
|
+
process.terminate()
|
|
120
|
+
process.wait(timeout=5)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
return url
|
|
124
|
+
|
|
125
|
+
except FileNotFoundError:
|
|
126
|
+
console.print("[red]cloudflared not found in PATH.[/red]")
|
|
127
|
+
return None
|
|
128
|
+
except Exception as e:
|
|
129
|
+
console.print(f"[red]Tunnel error:[/red] {e}")
|
|
130
|
+
if process and process.poll() is None:
|
|
131
|
+
process.terminate()
|
|
132
|
+
try:
|
|
133
|
+
process.wait(timeout=5)
|
|
134
|
+
except subprocess.TimeoutExpired:
|
|
135
|
+
process.kill()
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def start_tunnel_background(
|
|
140
|
+
port: int,
|
|
141
|
+
no_autoupdate: bool = True,
|
|
142
|
+
) -> Tuple[Optional[subprocess.Popen], Optional[str]]:
|
|
143
|
+
"""Start a tunnel in the background and return the process and URL.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
port: Local port to expose (1-65535)
|
|
147
|
+
no_autoupdate: Disable cloudflared auto-updates
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A tuple of (process, url) if successful, (None, None) otherwise
|
|
151
|
+
"""
|
|
152
|
+
if not _validate_port(port):
|
|
153
|
+
return None, None
|
|
154
|
+
|
|
155
|
+
if not check_cloudflared():
|
|
156
|
+
return None, None
|
|
157
|
+
|
|
158
|
+
cmd = ["cloudflared", "tunnel", "--url", f"http://localhost:{port}"]
|
|
159
|
+
if no_autoupdate:
|
|
160
|
+
cmd.append("--no-autoupdate")
|
|
161
|
+
|
|
162
|
+
process = subprocess.Popen(
|
|
163
|
+
cmd,
|
|
164
|
+
stdout=subprocess.PIPE,
|
|
165
|
+
stderr=subprocess.STDOUT,
|
|
166
|
+
text=True,
|
|
167
|
+
bufsize=1,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Read output until we find the URL
|
|
171
|
+
url = None
|
|
172
|
+
start_time = time.time()
|
|
173
|
+
timeout = 30
|
|
174
|
+
|
|
175
|
+
while time.time() - start_time < timeout:
|
|
176
|
+
line = process.stdout.readline()
|
|
177
|
+
if not line:
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
match = TUNNEL_URL_PATTERN.search(line.strip())
|
|
181
|
+
if match:
|
|
182
|
+
url = match.group(0)
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
return process, url
|
|
@@ -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
|
|
@@ -23,6 +23,7 @@ spooling/server.py
|
|
|
23
23
|
spooling/stats.py
|
|
24
24
|
spooling/subscription_pricing.py
|
|
25
25
|
spooling/tracing.py
|
|
26
|
+
spooling/tunnel.py
|
|
26
27
|
spooling/watcher.py
|
|
27
28
|
spooling.egg-info/PKG-INFO
|
|
28
29
|
spooling.egg-info/SOURCES.txt
|
|
@@ -56,7 +57,4 @@ spooling/providers/gitlab.py
|
|
|
56
57
|
spooling/providers/kiro.py
|
|
57
58
|
spooling/providers/opencode.py
|
|
58
59
|
spooling/providers/session_file.py
|
|
59
|
-
spooling/providers/windsurf.py
|
|
60
|
-
tests/test_parser.py
|
|
61
|
-
tests/test_pricing.py
|
|
62
|
-
tests/test_redact.py
|
|
60
|
+
spooling/providers/windsurf.py
|