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.
Files changed (67) hide show
  1. spooling-0.1.7/PKG-INFO +34 -0
  2. spooling-0.1.5/spooling.egg-info/PKG-INFO → spooling-0.1.7/README.md +106 -30
  3. {spooling-0.1.5 → spooling-0.1.7}/pyproject.toml +20 -14
  4. {spooling-0.1.5 → spooling-0.1.7}/spooling/cli.py +70 -10
  5. {spooling-0.1.5 → spooling-0.1.7}/spooling/cloud.py +359 -0
  6. spooling-0.1.7/spooling/connectors/__init__.py +0 -0
  7. spooling-0.1.7/spooling/connectors/base.py +37 -0
  8. spooling-0.1.7/spooling/connectors/bigquery.py +140 -0
  9. spooling-0.1.7/spooling/connectors/clickhouse.py +111 -0
  10. spooling-0.1.7/spooling/connectors/factory.py +68 -0
  11. spooling-0.1.7/spooling/connectors/mongodb.py +103 -0
  12. spooling-0.1.7/spooling/connectors/mysql.py +129 -0
  13. spooling-0.1.7/spooling/connectors/postgresql.py +120 -0
  14. spooling-0.1.7/spooling/connectors/rest/__init__.py +0 -0
  15. spooling-0.1.7/spooling/connectors/rest/base.py +206 -0
  16. spooling-0.1.7/spooling/connectors/rest/shopify_connectors.py +172 -0
  17. spooling-0.1.7/spooling/connectors/snowflake.py +155 -0
  18. spooling-0.1.7/spooling/connectors/types.py +87 -0
  19. {spooling-0.1.5 → spooling-0.1.7}/spooling/mcp_server.py +266 -8
  20. {spooling-0.1.5 → spooling-0.1.7}/spooling/server.py +326 -0
  21. spooling-0.1.7/spooling/tunnel.py +185 -0
  22. spooling-0.1.7/spooling.egg-info/PKG-INFO +34 -0
  23. {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/SOURCES.txt +15 -4
  24. {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/requires.txt +7 -0
  25. spooling-0.1.5/PKG-INFO +0 -523
  26. spooling-0.1.5/README.md +0 -493
  27. spooling-0.1.5/tests/test_parser.py +0 -273
  28. spooling-0.1.5/tests/test_pricing.py +0 -157
  29. spooling-0.1.5/tests/test_redact.py +0 -220
  30. {spooling-0.1.5 → spooling-0.1.7}/LICENSE +0 -0
  31. {spooling-0.1.5 → spooling-0.1.7}/setup.cfg +0 -0
  32. {spooling-0.1.5 → spooling-0.1.7}/spooling/__init__.py +0 -0
  33. {spooling-0.1.5 → spooling-0.1.7}/spooling/agent.py +0 -0
  34. {spooling-0.1.5 → spooling-0.1.7}/spooling/classifiers.py +0 -0
  35. {spooling-0.1.5 → spooling-0.1.7}/spooling/config.py +0 -0
  36. {spooling-0.1.5 → spooling-0.1.7}/spooling/db.py +0 -0
  37. {spooling-0.1.5 → spooling-0.1.7}/spooling/embeddings.py +0 -0
  38. {spooling-0.1.5 → spooling-0.1.7}/spooling/evals.py +0 -0
  39. {spooling-0.1.5 → spooling-0.1.7}/spooling/experiments.py +0 -0
  40. {spooling-0.1.5 → spooling-0.1.7}/spooling/ingest.py +0 -0
  41. {spooling-0.1.5 → spooling-0.1.7}/spooling/parser.py +0 -0
  42. {spooling-0.1.5 → spooling-0.1.7}/spooling/pricing.py +0 -0
  43. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/__init__.py +0 -0
  44. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/antigravity.py +0 -0
  45. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/base.py +0 -0
  46. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/codex.py +0 -0
  47. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/copilot.py +0 -0
  48. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/cortex_code.py +0 -0
  49. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/cursor.py +0 -0
  50. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/gemini.py +0 -0
  51. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/github.py +0 -0
  52. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/gitlab.py +0 -0
  53. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/kiro.py +0 -0
  54. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/opencode.py +0 -0
  55. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/session_file.py +0 -0
  56. {spooling-0.1.5 → spooling-0.1.7}/spooling/providers/windsurf.py +0 -0
  57. {spooling-0.1.5 → spooling-0.1.7}/spooling/redact.py +0 -0
  58. {spooling-0.1.5 → spooling-0.1.7}/spooling/remote_otel.py +0 -0
  59. {spooling-0.1.5 → spooling-0.1.7}/spooling/sdk.py +0 -0
  60. {spooling-0.1.5 → spooling-0.1.7}/spooling/search.py +0 -0
  61. {spooling-0.1.5 → spooling-0.1.7}/spooling/stats.py +0 -0
  62. {spooling-0.1.5 → spooling-0.1.7}/spooling/subscription_pricing.py +0 -0
  63. {spooling-0.1.5 → spooling-0.1.7}/spooling/tracing.py +0 -0
  64. {spooling-0.1.5 → spooling-0.1.7}/spooling/watcher.py +0 -0
  65. {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/dependency_links.txt +0 -0
  66. {spooling-0.1.5 → spooling-0.1.7}/spooling.egg-info/entry_points.txt +0 -0
  67. {spooling-0.1.5 → 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,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.5"
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
- [tool.pytest.ini_options]
44
- testpaths = ["tests"]
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
- def search(query, limit, project):
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
- from spooling.search import search as do_search
89
-
90
- results = do_search(query, limit=limit, project=project)
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
- def stats(week, days):
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
- def mcp(stdio):
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
- serve_stdio()
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
- serve_http()
736
+ _serve_http()
677
737
 
678
738
 
679
739
  def _check_ollama_preflight() -> None: