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.
Files changed (67) hide show
  1. spooling-0.1.7/PKG-INFO +34 -0
  2. spooling-0.1.6/spooling.egg-info/PKG-INFO → spooling-0.1.7/README.md +106 -36
  3. {spooling-0.1.6 → spooling-0.1.7}/pyproject.toml +13 -14
  4. {spooling-0.1.6 → spooling-0.1.7}/spooling/cli.py +30 -0
  5. spooling-0.1.7/spooling/tunnel.py +185 -0
  6. spooling-0.1.7/spooling.egg-info/PKG-INFO +34 -0
  7. {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/SOURCES.txt +2 -4
  8. spooling-0.1.6/PKG-INFO +0 -529
  9. spooling-0.1.6/README.md +0 -493
  10. spooling-0.1.6/tests/test_parser.py +0 -273
  11. spooling-0.1.6/tests/test_pricing.py +0 -157
  12. spooling-0.1.6/tests/test_redact.py +0 -220
  13. {spooling-0.1.6 → spooling-0.1.7}/LICENSE +0 -0
  14. {spooling-0.1.6 → spooling-0.1.7}/setup.cfg +0 -0
  15. {spooling-0.1.6 → spooling-0.1.7}/spooling/__init__.py +0 -0
  16. {spooling-0.1.6 → spooling-0.1.7}/spooling/agent.py +0 -0
  17. {spooling-0.1.6 → spooling-0.1.7}/spooling/classifiers.py +0 -0
  18. {spooling-0.1.6 → spooling-0.1.7}/spooling/cloud.py +0 -0
  19. {spooling-0.1.6 → spooling-0.1.7}/spooling/config.py +0 -0
  20. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/__init__.py +0 -0
  21. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/base.py +0 -0
  22. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/bigquery.py +0 -0
  23. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/clickhouse.py +0 -0
  24. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/factory.py +0 -0
  25. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/mongodb.py +0 -0
  26. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/mysql.py +0 -0
  27. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/postgresql.py +0 -0
  28. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/rest/__init__.py +0 -0
  29. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/rest/base.py +0 -0
  30. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/rest/shopify_connectors.py +0 -0
  31. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/snowflake.py +0 -0
  32. {spooling-0.1.6 → spooling-0.1.7}/spooling/connectors/types.py +0 -0
  33. {spooling-0.1.6 → spooling-0.1.7}/spooling/db.py +0 -0
  34. {spooling-0.1.6 → spooling-0.1.7}/spooling/embeddings.py +0 -0
  35. {spooling-0.1.6 → spooling-0.1.7}/spooling/evals.py +0 -0
  36. {spooling-0.1.6 → spooling-0.1.7}/spooling/experiments.py +0 -0
  37. {spooling-0.1.6 → spooling-0.1.7}/spooling/ingest.py +0 -0
  38. {spooling-0.1.6 → spooling-0.1.7}/spooling/mcp_server.py +0 -0
  39. {spooling-0.1.6 → spooling-0.1.7}/spooling/parser.py +0 -0
  40. {spooling-0.1.6 → spooling-0.1.7}/spooling/pricing.py +0 -0
  41. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/__init__.py +0 -0
  42. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/antigravity.py +0 -0
  43. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/base.py +0 -0
  44. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/codex.py +0 -0
  45. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/copilot.py +0 -0
  46. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/cortex_code.py +0 -0
  47. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/cursor.py +0 -0
  48. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/gemini.py +0 -0
  49. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/github.py +0 -0
  50. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/gitlab.py +0 -0
  51. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/kiro.py +0 -0
  52. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/opencode.py +0 -0
  53. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/session_file.py +0 -0
  54. {spooling-0.1.6 → spooling-0.1.7}/spooling/providers/windsurf.py +0 -0
  55. {spooling-0.1.6 → spooling-0.1.7}/spooling/redact.py +0 -0
  56. {spooling-0.1.6 → spooling-0.1.7}/spooling/remote_otel.py +0 -0
  57. {spooling-0.1.6 → spooling-0.1.7}/spooling/sdk.py +0 -0
  58. {spooling-0.1.6 → spooling-0.1.7}/spooling/search.py +0 -0
  59. {spooling-0.1.6 → spooling-0.1.7}/spooling/server.py +0 -0
  60. {spooling-0.1.6 → spooling-0.1.7}/spooling/stats.py +0 -0
  61. {spooling-0.1.6 → spooling-0.1.7}/spooling/subscription_pricing.py +0 -0
  62. {spooling-0.1.6 → spooling-0.1.7}/spooling/tracing.py +0 -0
  63. {spooling-0.1.6 → spooling-0.1.7}/spooling/watcher.py +0 -0
  64. {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/dependency_links.txt +0 -0
  65. {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/entry_points.txt +0 -0
  66. {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/requires.txt +0 -0
  67. {spooling-0.1.6 → spooling-0.1.7}/spooling.egg-info/top_level.txt +0 -0
@@ -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.6"
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
- [tool.pytest.ini_options]
51
- testpaths = ["tests"]
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