spooling 0.2.12__tar.gz → 0.2.14__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 (79) hide show
  1. {spooling-0.2.12/spooling.egg-info → spooling-0.2.14}/PKG-INFO +60 -10
  2. {spooling-0.2.12 → spooling-0.2.14}/README.md +59 -9
  3. {spooling-0.2.12 → spooling-0.2.14}/pyproject.toml +1 -1
  4. {spooling-0.2.12 → spooling-0.2.14}/spooling/cli.py +172 -28
  5. {spooling-0.2.12 → spooling-0.2.14}/spooling/cloud.py +1 -1
  6. spooling-0.2.14/spooling/insights.py +427 -0
  7. {spooling-0.2.12 → spooling-0.2.14/spooling.egg-info}/PKG-INFO +60 -10
  8. {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/SOURCES.txt +1 -0
  9. spooling-0.2.14/tests/test_docs.py +31 -0
  10. spooling-0.2.14/tests/test_insights.py +407 -0
  11. spooling-0.2.12/spooling/insights.py +0 -253
  12. spooling-0.2.12/tests/test_insights.py +0 -218
  13. {spooling-0.2.12 → spooling-0.2.14}/LICENSE +0 -0
  14. {spooling-0.2.12 → spooling-0.2.14}/setup.cfg +0 -0
  15. {spooling-0.2.12 → spooling-0.2.14}/spooling/__init__.py +0 -0
  16. {spooling-0.2.12 → spooling-0.2.14}/spooling/agent.py +0 -0
  17. {spooling-0.2.12 → spooling-0.2.14}/spooling/classifiers.py +0 -0
  18. {spooling-0.2.12 → spooling-0.2.14}/spooling/config.py +0 -0
  19. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/__init__.py +0 -0
  20. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/base.py +0 -0
  21. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/bigquery.py +0 -0
  22. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/clickhouse.py +0 -0
  23. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/factory.py +0 -0
  24. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/mongodb.py +0 -0
  25. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/mysql.py +0 -0
  26. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/postgresql.py +0 -0
  27. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/rest/__init__.py +0 -0
  28. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/rest/base.py +0 -0
  29. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/rest/shopify_connectors.py +0 -0
  30. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/snowflake.py +0 -0
  31. {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/types.py +0 -0
  32. {spooling-0.2.12 → spooling-0.2.14}/spooling/db.py +0 -0
  33. {spooling-0.2.12 → spooling-0.2.14}/spooling/embeddings.py +0 -0
  34. {spooling-0.2.12 → spooling-0.2.14}/spooling/evals.py +0 -0
  35. {spooling-0.2.12 → spooling-0.2.14}/spooling/experiments.py +0 -0
  36. {spooling-0.2.12 → spooling-0.2.14}/spooling/ingest.py +0 -0
  37. {spooling-0.2.12 → spooling-0.2.14}/spooling/mcp_server.py +0 -0
  38. {spooling-0.2.12 → spooling-0.2.14}/spooling/optimize.py +0 -0
  39. {spooling-0.2.12 → spooling-0.2.14}/spooling/parser.py +0 -0
  40. {spooling-0.2.12 → spooling-0.2.14}/spooling/pricing.py +0 -0
  41. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/__init__.py +0 -0
  42. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/antigravity.py +0 -0
  43. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/base.py +0 -0
  44. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/claude_code.py +0 -0
  45. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/codex.py +0 -0
  46. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/copilot.py +0 -0
  47. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/cortex_code.py +0 -0
  48. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/cursor.py +0 -0
  49. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/gemini.py +0 -0
  50. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/github.py +0 -0
  51. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/gitlab.py +0 -0
  52. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/kiro.py +0 -0
  53. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/opencode.py +0 -0
  54. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/session_file.py +0 -0
  55. {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/windsurf.py +0 -0
  56. {spooling-0.2.12 → spooling-0.2.14}/spooling/redact.py +0 -0
  57. {spooling-0.2.12 → spooling-0.2.14}/spooling/remote_otel.py +0 -0
  58. {spooling-0.2.12 → spooling-0.2.14}/spooling/schema.py +0 -0
  59. {spooling-0.2.12 → spooling-0.2.14}/spooling/sdk.py +0 -0
  60. {spooling-0.2.12 → spooling-0.2.14}/spooling/search.py +0 -0
  61. {spooling-0.2.12 → spooling-0.2.14}/spooling/server.py +0 -0
  62. {spooling-0.2.12 → spooling-0.2.14}/spooling/sessions.py +0 -0
  63. {spooling-0.2.12 → spooling-0.2.14}/spooling/stats.py +0 -0
  64. {spooling-0.2.12 → spooling-0.2.14}/spooling/subscription_pricing.py +0 -0
  65. {spooling-0.2.12 → spooling-0.2.14}/spooling/tracing.py +0 -0
  66. {spooling-0.2.12 → spooling-0.2.14}/spooling/tunnel.py +0 -0
  67. {spooling-0.2.12 → spooling-0.2.14}/spooling/tunnel_auth.py +0 -0
  68. {spooling-0.2.12 → spooling-0.2.14}/spooling/watcher.py +0 -0
  69. {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/dependency_links.txt +0 -0
  70. {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/entry_points.txt +0 -0
  71. {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/requires.txt +0 -0
  72. {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/top_level.txt +0 -0
  73. {spooling-0.2.12 → spooling-0.2.14}/tests/test_agent_spans.py +0 -0
  74. {spooling-0.2.12 → spooling-0.2.14}/tests/test_claude_code_provider.py +0 -0
  75. {spooling-0.2.12 → spooling-0.2.14}/tests/test_cloud_errors.py +0 -0
  76. {spooling-0.2.12 → spooling-0.2.14}/tests/test_mcp_auth_middleware.py +0 -0
  77. {spooling-0.2.12 → spooling-0.2.14}/tests/test_session_ids.py +0 -0
  78. {spooling-0.2.12 → spooling-0.2.14}/tests/test_storage_optimizations.py +0 -0
  79. {spooling-0.2.12 → spooling-0.2.14}/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.12
3
+ Version: 0.2.14
4
4
  Summary: Local session tracker and semantic search for AI coding assistants
5
5
  Author: Parsed Analytics, Inc.
6
6
  License: MIT
@@ -155,28 +155,38 @@ spooling stats --week # Weekly breakdown
155
155
  spooling stats --days 30 # Last 30 days
156
156
  ```
157
157
 
158
- ### `spooling tools`, `spooling agents`, `spooling traces`
158
+ ### `spooling tools`, `spooling agents`, `spooling mcps`, `spooling traces`
159
159
 
160
- See which tools and agents your sessions use. These read the spans stored by `spooling sync`, and use the same agent/tool names as the web app (an agent is its subagent type, falling back to its span name).
160
+ See which tools, MCP servers and agents your sessions use. These read the spans stored by `spooling sync`, and use the same names as the web app (an agent is its subagent type, falling back to its span name; an MCP tool is named `mcp__<server>__<tool>`).
161
161
 
162
162
  ```bash
163
163
  spooling tools # Top tools: uses, traces, errors, avg time
164
+ spooling tools -s read # Tools whose name contains "read"
164
165
  spooling tools --agent explore # Tools called by the "explore" agent
165
- spooling tools --days 7 --provider claude-code
166
+ spooling tools --mcp github # Tools of MCP servers matching "github"
167
+
168
+ spooling mcps # Every MCP server used: tools, calls, traces, errors
169
+ spooling mcps spool # The tools of servers matching "spool"
166
170
 
167
171
  spooling agents # Agents: runs, traces, tokens, cost
172
+ spooling agents -s expl # Agents whose name contains "expl"
168
173
  spooling agents --tool Bash # Agents whose runs called Bash
174
+ spooling agents --mcp github # Agents whose runs used a GitHub MCP tool
169
175
 
170
- spooling traces # Latest traces, with the agents each ran
171
- spooling traces ses_10dd # Search title, project, session id, trace id or agent name
172
- spooling traces --agent explore --tool Bash --days 30
176
+ spooling traces # Latest traces, with the agents and MCP servers each used
177
+ spooling traces ses_10dd # Search title, project, session id, trace id, agent or MCP server name
178
+ spooling traces --agent explore --tool Bash --mcp spool --days 30
173
179
  ```
174
180
 
175
- Common options: `--days N`, `--project TEXT`, `--provider/-p NAME`, `--limit/-n N`, and `--json` for scripting. `--agent` and `--tool` can be repeated (matches any).
181
+ Common options: `--days N`, `--project TEXT`, `--provider/-p NAME`, `--limit/-n N`, and `--json` for scripting.
182
+
183
+ Matching rules: `--agent` and `--tool` match the exact name, ignoring case, and can be repeated (matches any). `--mcp` and `--search/-s` match part of the name, ignoring case. When you combine different filters on `traces`, a trace must satisfy all of them.
184
+
185
+ `tools --agent` and `agents --tool/--mcp` look inside an agent's own runs. `traces --agent/--tool/--mcp` matches whole traces, like the Traces page.
176
186
 
177
- `tools --agent` and `agents --tool` look inside an agent's own runs. `traces --agent/--tool` matches whole traces, like the Traces page.
187
+ `--cloud` on `tools`, `agents` and `mcps` shows workspace-wide totals from Spooling Cloud. The cloud endpoints only return the 100 most-used tools, so `--search` and `--mcp` filter those client-side, and `--days/--project/--provider/--agent/--tool` can't be combined with `--cloud`.
178
188
 
179
- `spooling tools --cloud` and `spooling agents --cloud` show workspace-wide totals from Spooling Cloud. The cloud endpoints only support `--limit`, so combining `--cloud` with other filters is an error. Agent data is only recorded for providers that expose subagents (currently Claude Code, via its `Agent` tool, previously called `Task`), so `spooling agents` is empty for other providers.
189
+ Agent data is only recorded for providers that expose subagents (currently Claude Code, via its `Agent` tool, previously called `Task`), so `spooling agents` is empty for other providers. MCP calls are recognised by the `mcp__<server>__<tool>` name Claude Code uses; tools from other clients that name MCP tools differently (for example `server_tool`) appear in `spooling tools` but not in `spooling mcps`.
180
190
 
181
191
  After upgrading, `spooling sync` re-reads your Claude Code sessions once to pick up agents. To force a re-read of any provider's files at any time, run `spooling sync --reparse` (optionally with `-p claude-code`).
182
192
 
@@ -405,6 +415,46 @@ spooling push --dry-run # show what would be pushed
405
415
 
406
416
  `push` ingests new provider sessions into the local database before uploading, so you don't need to run `spooling sync` first. The server upserts by session id, so re-running is safe.
407
417
 
418
+ ### Push only some sessions, or to a specific team
419
+
420
+ **Which team gets the sessions** is decided by the API key you are logged in with. Each key belongs to one workspace (personal or a team), and `spooling push` sends to that workspace. Mint a key for the team in the app, then log in with it:
421
+
422
+ ```bash
423
+ spooling cloud login --key sk_<team-key> # replaces the stored key; later pushes go to this team
424
+ spooling cloud status # session/cost totals of the workspace this key belongs to
425
+ ```
426
+
427
+ To push to a different team, log in with that team's key. Only one key is stored at a time. `cloud status` doesn't print the workspace name, so compare its totals with what the team shows in the app.
428
+
429
+ **Which sessions go up.** With no filters, `spooling push` sends the most recent `--limit` sessions from every project on the machine. Use these to send only some of them. Filters combine with AND:
430
+
431
+ | Option | Matches |
432
+ |---|---|
433
+ | `--project NAME` | Sessions whose project name is exactly `NAME` (find names with `spooling cost breakdown --by project`) |
434
+ | `--cwd TEXT` | Sessions whose working directory contains `TEXT` (case-insensitive) |
435
+ | `--title TEXT` | Sessions whose title contains `TEXT` (case-insensitive). Useful when an agent was launched from your home directory, so every session shares a generic working directory, but the project name is in the title |
436
+
437
+ ```bash
438
+ spooling cloud login --key sk_<team-key>
439
+ spooling push --cwd toebox --dry-run # preview what would be sent; nothing is uploaded
440
+ spooling push --cwd toebox # send only sessions from the toebox directory
441
+ spooling push --project my-app --limit 200
442
+ spooling push --title islet # sessions with "islet" in the title
443
+ ```
444
+
445
+ **Share the same sessions with more than one team.** Add `--copy` to write the sessions into the workspace you are logged into as fresh rows, even if they were already pushed to another workspace. Re-running is safe:
446
+
447
+ ```bash
448
+ spooling cloud login --key sk_<other-team-key>
449
+ spooling push --cwd toebox --copy
450
+ ```
451
+
452
+ **Include trace data.** Add `--with-spans` to also push each session's trace and spans (per-LLM-call usage, cost, tool calls, MCP calls and agent runs). The Traces page in the app, its agent, tool and MCP filters, and `spooling agents --cloud` / `spooling mcps --cloud` all rely on it. `--with-spans` can't be combined with `--copy` yet.
453
+
454
+ **Secrets** (cloud and AWS keys, GitHub, OpenAI, Anthropic and Stripe tokens, PEM private keys, and `KEY=VALUE` lines whose key looks sensitive) are redacted on your machine before anything is uploaded. Pass `--no-redact` only if you are sure the content is clean.
455
+
456
+ `spooling cloud watch` has no filters: it keeps pushing every new or updated session to the workspace you are logged into. Use filtered `spooling push` runs when you need to keep some sessions out.
457
+
408
458
  To bring sessions from the cloud back down, use `spooling cloud pull`, or `spooling cloud sync` for both directions.
409
459
 
410
460
  ### Watch (continuous push)
@@ -119,28 +119,38 @@ spooling stats --week # Weekly breakdown
119
119
  spooling stats --days 30 # Last 30 days
120
120
  ```
121
121
 
122
- ### `spooling tools`, `spooling agents`, `spooling traces`
122
+ ### `spooling tools`, `spooling agents`, `spooling mcps`, `spooling traces`
123
123
 
124
- See which tools and agents your sessions use. These read the spans stored by `spooling sync`, and use the same agent/tool names as the web app (an agent is its subagent type, falling back to its span name).
124
+ See which tools, MCP servers and agents your sessions use. These read the spans stored by `spooling sync`, and use the same names as the web app (an agent is its subagent type, falling back to its span name; an MCP tool is named `mcp__<server>__<tool>`).
125
125
 
126
126
  ```bash
127
127
  spooling tools # Top tools: uses, traces, errors, avg time
128
+ spooling tools -s read # Tools whose name contains "read"
128
129
  spooling tools --agent explore # Tools called by the "explore" agent
129
- spooling tools --days 7 --provider claude-code
130
+ spooling tools --mcp github # Tools of MCP servers matching "github"
131
+
132
+ spooling mcps # Every MCP server used: tools, calls, traces, errors
133
+ spooling mcps spool # The tools of servers matching "spool"
130
134
 
131
135
  spooling agents # Agents: runs, traces, tokens, cost
136
+ spooling agents -s expl # Agents whose name contains "expl"
132
137
  spooling agents --tool Bash # Agents whose runs called Bash
138
+ spooling agents --mcp github # Agents whose runs used a GitHub MCP tool
133
139
 
134
- spooling traces # Latest traces, with the agents each ran
135
- spooling traces ses_10dd # Search title, project, session id, trace id or agent name
136
- spooling traces --agent explore --tool Bash --days 30
140
+ spooling traces # Latest traces, with the agents and MCP servers each used
141
+ spooling traces ses_10dd # Search title, project, session id, trace id, agent or MCP server name
142
+ spooling traces --agent explore --tool Bash --mcp spool --days 30
137
143
  ```
138
144
 
139
- Common options: `--days N`, `--project TEXT`, `--provider/-p NAME`, `--limit/-n N`, and `--json` for scripting. `--agent` and `--tool` can be repeated (matches any).
145
+ Common options: `--days N`, `--project TEXT`, `--provider/-p NAME`, `--limit/-n N`, and `--json` for scripting.
146
+
147
+ Matching rules: `--agent` and `--tool` match the exact name, ignoring case, and can be repeated (matches any). `--mcp` and `--search/-s` match part of the name, ignoring case. When you combine different filters on `traces`, a trace must satisfy all of them.
148
+
149
+ `tools --agent` and `agents --tool/--mcp` look inside an agent's own runs. `traces --agent/--tool/--mcp` matches whole traces, like the Traces page.
140
150
 
141
- `tools --agent` and `agents --tool` look inside an agent's own runs. `traces --agent/--tool` matches whole traces, like the Traces page.
151
+ `--cloud` on `tools`, `agents` and `mcps` shows workspace-wide totals from Spooling Cloud. The cloud endpoints only return the 100 most-used tools, so `--search` and `--mcp` filter those client-side, and `--days/--project/--provider/--agent/--tool` can't be combined with `--cloud`.
142
152
 
143
- `spooling tools --cloud` and `spooling agents --cloud` show workspace-wide totals from Spooling Cloud. The cloud endpoints only support `--limit`, so combining `--cloud` with other filters is an error. Agent data is only recorded for providers that expose subagents (currently Claude Code, via its `Agent` tool, previously called `Task`), so `spooling agents` is empty for other providers.
153
+ Agent data is only recorded for providers that expose subagents (currently Claude Code, via its `Agent` tool, previously called `Task`), so `spooling agents` is empty for other providers. MCP calls are recognised by the `mcp__<server>__<tool>` name Claude Code uses; tools from other clients that name MCP tools differently (for example `server_tool`) appear in `spooling tools` but not in `spooling mcps`.
144
154
 
145
155
  After upgrading, `spooling sync` re-reads your Claude Code sessions once to pick up agents. To force a re-read of any provider's files at any time, run `spooling sync --reparse` (optionally with `-p claude-code`).
146
156
 
@@ -369,6 +379,46 @@ spooling push --dry-run # show what would be pushed
369
379
 
370
380
  `push` ingests new provider sessions into the local database before uploading, so you don't need to run `spooling sync` first. The server upserts by session id, so re-running is safe.
371
381
 
382
+ ### Push only some sessions, or to a specific team
383
+
384
+ **Which team gets the sessions** is decided by the API key you are logged in with. Each key belongs to one workspace (personal or a team), and `spooling push` sends to that workspace. Mint a key for the team in the app, then log in with it:
385
+
386
+ ```bash
387
+ spooling cloud login --key sk_<team-key> # replaces the stored key; later pushes go to this team
388
+ spooling cloud status # session/cost totals of the workspace this key belongs to
389
+ ```
390
+
391
+ To push to a different team, log in with that team's key. Only one key is stored at a time. `cloud status` doesn't print the workspace name, so compare its totals with what the team shows in the app.
392
+
393
+ **Which sessions go up.** With no filters, `spooling push` sends the most recent `--limit` sessions from every project on the machine. Use these to send only some of them. Filters combine with AND:
394
+
395
+ | Option | Matches |
396
+ |---|---|
397
+ | `--project NAME` | Sessions whose project name is exactly `NAME` (find names with `spooling cost breakdown --by project`) |
398
+ | `--cwd TEXT` | Sessions whose working directory contains `TEXT` (case-insensitive) |
399
+ | `--title TEXT` | Sessions whose title contains `TEXT` (case-insensitive). Useful when an agent was launched from your home directory, so every session shares a generic working directory, but the project name is in the title |
400
+
401
+ ```bash
402
+ spooling cloud login --key sk_<team-key>
403
+ spooling push --cwd toebox --dry-run # preview what would be sent; nothing is uploaded
404
+ spooling push --cwd toebox # send only sessions from the toebox directory
405
+ spooling push --project my-app --limit 200
406
+ spooling push --title islet # sessions with "islet" in the title
407
+ ```
408
+
409
+ **Share the same sessions with more than one team.** Add `--copy` to write the sessions into the workspace you are logged into as fresh rows, even if they were already pushed to another workspace. Re-running is safe:
410
+
411
+ ```bash
412
+ spooling cloud login --key sk_<other-team-key>
413
+ spooling push --cwd toebox --copy
414
+ ```
415
+
416
+ **Include trace data.** Add `--with-spans` to also push each session's trace and spans (per-LLM-call usage, cost, tool calls, MCP calls and agent runs). The Traces page in the app, its agent, tool and MCP filters, and `spooling agents --cloud` / `spooling mcps --cloud` all rely on it. `--with-spans` can't be combined with `--copy` yet.
417
+
418
+ **Secrets** (cloud and AWS keys, GitHub, OpenAI, Anthropic and Stripe tokens, PEM private keys, and `KEY=VALUE` lines whose key looks sensitive) are redacted on your machine before anything is uploaded. Pass `--no-redact` only if you are sure the content is clean.
419
+
420
+ `spooling cloud watch` has no filters: it keeps pushing every new or updated session to the workspace you are logged into. Use filtered `spooling push` runs when you need to keep some sessions out.
421
+
372
422
  To bring sessions from the cloud back down, use `spooling cloud pull`, or `spooling cloud sync` for both directions.
373
423
 
374
424
  ### Watch (continuous push)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spooling"
3
- version = "0.2.12"
3
+ version = "0.2.14"
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." }]
@@ -1373,7 +1373,8 @@ def _reject_cloud_filters(**filters) -> None:
1373
1373
  if used:
1374
1374
  raise click.UsageError(
1375
1375
  f"{', '.join(used)} can't be combined with --cloud "
1376
- "(the cloud endpoint only supports --limit). Drop --cloud to filter local data."
1376
+ "(the cloud endpoint only supports --limit, --search and --mcp). "
1377
+ "Drop --cloud to filter local data."
1377
1378
  )
1378
1379
 
1379
1380
 
@@ -1392,23 +1393,44 @@ def _filter_opts(fn):
1392
1393
  return fn
1393
1394
 
1394
1395
 
1396
+ def _error_cell(n) -> str:
1397
+ return f"[red]{n}[/red]" if n else "0"
1398
+
1399
+
1400
+ def _cloud_tool_rows(limit: int, search: str | None, mcps) -> list[dict]:
1401
+ """Cloud per-tool rows, filtered client-side by name/MCP server (server caps at 100)."""
1402
+ from spooling.cloud import cloud_tools
1403
+ from spooling.insights import mcp_server_of
1404
+ rows = cloud_tools(100)
1405
+ if search:
1406
+ rows = [r for r in rows if search.lower() in r["tool_name"].lower()]
1407
+ if mcps:
1408
+ rows = [r for r in rows
1409
+ if any(m.lower() in (mcp_server_of(r["tool_name"]) or "").lower() for m in mcps)]
1410
+ return rows[:limit]
1411
+
1412
+
1395
1413
  @cli.command("tools")
1396
1414
  @click.option("--agent", "agents", multiple=True, help="Only tools called by this agent (repeatable)")
1415
+ @click.option("--mcp", "mcps", multiple=True, help="Only tools of MCP servers whose name contains this (repeatable)")
1416
+ @click.option("--search", "-s", default=None, help="Only tools whose name contains this text")
1397
1417
  @click.option("--cloud", "cloud_mode", is_flag=True, help="Use the cloud workspace instead of local data")
1398
1418
  @_filter_opts
1399
- def tools_cmd(agents, cloud_mode, days, project, provider, limit, as_json):
1419
+ def tools_cmd(agents, mcps, search, cloud_mode, days, project, provider, limit, as_json):
1400
1420
  """Show which tools are used, how often, and how often they fail.
1401
1421
 
1422
+ MCP tools are named mcp__<server>__<tool>; use --mcp to see one server's tools.
1423
+
1402
1424
  \b
1403
1425
  spooling tools top tools across all traces
1426
+ spooling tools -s read tools whose name contains "read"
1427
+ spooling tools --mcp spool tools from MCP servers matching "spool"
1404
1428
  spooling tools --agent explore tools called by the 'explore' agent
1405
- spooling tools --days 7 --json
1406
- spooling tools --cloud workspace-wide, from Spooling Cloud
1429
+ spooling tools --cloud --mcp github workspace-wide, from Spooling Cloud
1407
1430
  """
1408
1431
  if cloud_mode:
1409
1432
  _reject_cloud_filters(agent=agents, days=days, project=project, provider=provider)
1410
- from spooling.cloud import cloud_tools
1411
- rows = cloud_tools(limit)
1433
+ rows = _cloud_tool_rows(limit, search, mcps)
1412
1434
  if as_json:
1413
1435
  return _emit_json(rows)
1414
1436
  table = Table(title="Tools (cloud)", show_lines=False)
@@ -1417,20 +1439,25 @@ def tools_cmd(agents, cloud_mode, days, project, provider, limit, as_json):
1417
1439
  table.add_column("Sessions", justify="right")
1418
1440
  table.add_column("Errors", justify="right")
1419
1441
  for r in rows:
1420
- table.add_row(r["tool_name"], _fmt_num(r["uses"]), _fmt_num(r["sessions"]),
1421
- f"[red]{r['errors']}[/red]" if r["errors"] else "0")
1422
- console.print(table if rows else "[yellow]No tool data in the cloud workspace yet.[/yellow]")
1442
+ table.add_row(r["tool_name"], _fmt_num(r["uses"]), _fmt_num(r["sessions"]), _error_cell(r["errors"]))
1443
+ console.print(table if rows else "[yellow]No matching tools in the cloud workspace.[/yellow]")
1423
1444
  return
1424
1445
 
1425
1446
  from spooling.insights import tool_stats
1426
- rows = tool_stats(days=days, project=project, provider=provider, agents=list(agents) or None, limit=limit)
1447
+ rows = tool_stats(days=days, project=project, provider=provider, agents=list(agents) or None,
1448
+ mcps=list(mcps) or None, search=search, limit=limit)
1427
1449
  if as_json:
1428
1450
  return _emit_json(rows)
1429
1451
  if not rows:
1430
- console.print("[yellow]No tool data found. Run 'spooling sync' first"
1452
+ console.print("[yellow]No matching tool data found. Run 'spooling sync' first"
1431
1453
  + (" (agent filters need traces that contain agent spans)." if agents else ".") + "[/yellow]")
1432
1454
  return
1433
- table = Table(title="Tools" + (f" called by {', '.join(agents)}" if agents else ""), show_lines=False)
1455
+ title = "Tools"
1456
+ if agents:
1457
+ title += f" called by {', '.join(agents)}"
1458
+ if mcps:
1459
+ title += f" from MCP {', '.join(mcps)}"
1460
+ table = Table(title=title, show_lines=False)
1434
1461
  table.add_column("Tool", style="cyan")
1435
1462
  table.add_column("Uses", justify="right")
1436
1463
  table.add_column("Traces", justify="right")
@@ -1438,27 +1465,130 @@ def tools_cmd(agents, cloud_mode, days, project, provider, limit, as_json):
1438
1465
  table.add_column("Avg time", justify="right", style="dim")
1439
1466
  for r in rows:
1440
1467
  table.add_row(r["name"], _fmt_num(r["uses"]), _fmt_num(r["traces"]),
1441
- f"[red]{r['errors']}[/red]" if r["errors"] else "0", _fmt_ms(r["avg_ms"]))
1468
+ _error_cell(r["errors"]), _fmt_ms(r["avg_ms"]))
1442
1469
  console.print(table)
1443
1470
 
1444
1471
 
1472
+ @cli.command("mcps")
1473
+ @click.argument("server", required=False)
1474
+ @click.option("--search", "-s", default=None, help="Only servers/tools whose name contains this text")
1475
+ @click.option("--cloud", "cloud_mode", is_flag=True, help="Use the cloud workspace instead of local data")
1476
+ @_filter_opts
1477
+ def mcps_cmd(server, search, cloud_mode, days, project, provider, limit, as_json):
1478
+ """Show which MCP servers were used, and which of their tools.
1479
+
1480
+ With no argument, one row per MCP server. Pass a server name (or part of
1481
+ it) to list that server's tools instead.
1482
+
1483
+ \b
1484
+ spooling mcps every MCP server used: tools, calls, traces, errors
1485
+ spooling mcps spool the tools of servers matching "spool"
1486
+ spooling mcps -s search servers/tools with "search" in the name
1487
+ spooling mcps --days 7 --json
1488
+ spooling mcps --cloud workspace-wide, from Spooling Cloud
1489
+ """
1490
+ if cloud_mode:
1491
+ _reject_cloud_filters(days=days, project=project, provider=provider)
1492
+ from spooling.insights import group_mcp_tools
1493
+ rows = _cloud_tool_rows(100, search, [server] if server else None)
1494
+ mcp_rows = [r for r in rows if r["tool_name"].lower().startswith("mcp__")]
1495
+ if server:
1496
+ mcp_rows = mcp_rows[:limit]
1497
+ if as_json:
1498
+ return _emit_json(mcp_rows)
1499
+ table = Table(title=f"MCP tools matching '{server}' (cloud)", show_lines=False)
1500
+ table.add_column("Tool", style="cyan")
1501
+ table.add_column("Uses", justify="right")
1502
+ table.add_column("Sessions", justify="right")
1503
+ table.add_column("Errors", justify="right")
1504
+ for r in mcp_rows:
1505
+ table.add_row(r["tool_name"], _fmt_num(r["uses"]), _fmt_num(r["sessions"]), _error_cell(r["errors"]))
1506
+ console.print(table if mcp_rows else "[yellow]No matching MCP tools in the cloud workspace.[/yellow]")
1507
+ return
1508
+ grouped = group_mcp_tools(mcp_rows)[:limit]
1509
+ if as_json:
1510
+ return _emit_json(grouped)
1511
+ table = Table(title="MCP servers (cloud)", show_lines=False)
1512
+ table.add_column("MCP server", style="cyan")
1513
+ table.add_column("Tools", justify="right")
1514
+ table.add_column("Calls", justify="right")
1515
+ table.add_column("Errors", justify="right")
1516
+ for g in grouped:
1517
+ table.add_row(g["name"], _fmt_num(g["tools"]), _fmt_num(g["calls"]), _error_cell(g["errors"]))
1518
+ console.print(table if grouped else "[yellow]No MCP tool calls in the cloud workspace.[/yellow]")
1519
+ console.print("[dim]Based on the 100 most-used tools in the workspace.[/dim]")
1520
+ return
1521
+
1522
+ from spooling.insights import mcp_stats, tool_stats
1523
+ if server:
1524
+ rows = tool_stats(days=days, project=project, provider=provider, mcps=[server],
1525
+ search=search, limit=limit)
1526
+ if as_json:
1527
+ return _emit_json(rows)
1528
+ if not rows:
1529
+ console.print(f"[yellow]No MCP tools found for a server matching '{server}'. "
1530
+ "Run 'spooling mcps' to see the servers.[/yellow]")
1531
+ return
1532
+ table = Table(title=f"MCP tools matching '{server}'", show_lines=False)
1533
+ table.add_column("Tool", style="cyan")
1534
+ table.add_column("Server", style="dim")
1535
+ table.add_column("Uses", justify="right")
1536
+ table.add_column("Traces", justify="right")
1537
+ table.add_column("Errors", justify="right")
1538
+ table.add_column("Avg time", justify="right", style="dim")
1539
+ for r in rows:
1540
+ table.add_row(r["name"], r["mcp_server"] or "", _fmt_num(r["uses"]), _fmt_num(r["traces"]),
1541
+ _error_cell(r["errors"]), _fmt_ms(r["avg_ms"]))
1542
+ console.print(table)
1543
+ return
1544
+
1545
+ rows = mcp_stats(days=days, project=project, provider=provider, search=search, limit=limit)
1546
+ if as_json:
1547
+ return _emit_json(rows)
1548
+ if not rows:
1549
+ console.print(
1550
+ "[yellow]No MCP tool calls found.[/yellow]\n"
1551
+ "[dim]MCP tools are recorded as mcp__<server>__<tool>. Run 'spooling sync' after using one, "
1552
+ "or try 'spooling mcps --cloud'.[/dim]"
1553
+ )
1554
+ return
1555
+ table = Table(title="MCP servers", show_lines=False)
1556
+ table.add_column("MCP server", style="cyan")
1557
+ table.add_column("Tools", justify="right")
1558
+ table.add_column("Calls", justify="right")
1559
+ table.add_column("Traces", justify="right")
1560
+ table.add_column("Errors", justify="right")
1561
+ table.add_column("Avg time", justify="right", style="dim")
1562
+ for r in rows:
1563
+ table.add_row(r["name"], _fmt_num(r["tools"]), _fmt_num(r["calls"]), _fmt_num(r["traces"]),
1564
+ _error_cell(r["errors"]), _fmt_ms(r["avg_ms"]))
1565
+ console.print(table)
1566
+ console.print("[dim]spooling mcps <server> lists a server's tools.[/dim]")
1567
+
1568
+
1445
1569
  @cli.command("agents")
1446
1570
  @click.option("--tool", "tools", multiple=True, help="Only agents that called this tool (repeatable)")
1571
+ @click.option("--mcp", "mcps", multiple=True, help="Only agents that called a tool of an MCP server matching this (repeatable)")
1572
+ @click.option("--search", "-s", default=None, help="Only agents whose name contains this text")
1447
1573
  @click.option("--cloud", "cloud_mode", is_flag=True, help="Use the cloud workspace instead of local data")
1448
1574
  @_filter_opts
1449
- def agents_cmd(tools, cloud_mode, days, project, provider, limit, as_json):
1575
+ def agents_cmd(tools, mcps, search, cloud_mode, days, project, provider, limit, as_json):
1450
1576
  """Show which agents (subagent types) ran, how often, and what they cost.
1451
1577
 
1452
1578
  \b
1453
1579
  spooling agents agents across all traces
1580
+ spooling agents -s expl agents whose name contains "expl"
1454
1581
  spooling agents --tool Bash agents whose runs called Bash
1455
- spooling agents --days 30 --json
1582
+ spooling agents --mcp github agents whose runs called a GitHub MCP tool
1456
1583
  spooling agents --cloud workspace-wide, from Spooling Cloud
1457
1584
  """
1458
1585
  if cloud_mode:
1459
- _reject_cloud_filters(tool=tools, days=days, project=project, provider=provider)
1586
+ _reject_cloud_filters(tool=tools, mcp=mcps, days=days, project=project, provider=provider)
1460
1587
  from spooling.cloud import cloud_agents
1461
- rows = cloud_agents()[:limit]
1588
+ rows = cloud_agents()
1589
+ if search:
1590
+ rows = [r for r in rows if search.lower() in r["agent_type"].lower()]
1591
+ rows = rows[:limit]
1462
1592
  if as_json:
1463
1593
  return _emit_json(rows)
1464
1594
  table = Table(title="Agents (cloud)", show_lines=False)
@@ -1466,22 +1596,28 @@ def agents_cmd(tools, cloud_mode, days, project, provider, limit, as_json):
1466
1596
  table.add_column("Runs", justify="right")
1467
1597
  for r in rows:
1468
1598
  table.add_row(r["agent_type"], _fmt_num(r["uses"]))
1469
- console.print(table if rows else "[yellow]No agent data in the cloud workspace yet.[/yellow]")
1599
+ console.print(table if rows else "[yellow]No matching agents in the cloud workspace.[/yellow]")
1470
1600
  return
1471
1601
 
1472
1602
  from spooling.insights import agent_stats
1473
- rows = agent_stats(days=days, project=project, provider=provider, tools=list(tools) or None, limit=limit)
1603
+ rows = agent_stats(days=days, project=project, provider=provider, tools=list(tools) or None,
1604
+ mcps=list(mcps) or None, search=search, limit=limit)
1474
1605
  if as_json:
1475
1606
  return _emit_json(rows)
1476
1607
  if not rows:
1477
1608
  console.print(
1478
- "[yellow]No agent spans found locally.[/yellow]\n"
1609
+ "[yellow]No matching agent spans found locally.[/yellow]\n"
1479
1610
  "[dim]Agent runs are recorded for providers that expose subagents (currently Claude Code, via its "
1480
1611
  "Agent/Task tool). Run 'spooling sync' after using one, or try 'spooling agents --cloud'.\n"
1481
1612
  "If you use Claude Code and still see this, run 'spooling sync --reparse -p claude-code'.[/dim]"
1482
1613
  )
1483
1614
  return
1484
- table = Table(title="Agents" + (f" that called {', '.join(tools)}" if tools else ""), show_lines=False)
1615
+ title = "Agents"
1616
+ if tools:
1617
+ title += f" that called {', '.join(tools)}"
1618
+ if mcps:
1619
+ title += f" that used MCP {', '.join(mcps)}"
1620
+ table = Table(title=title, show_lines=False)
1485
1621
  table.add_column("Agent", style="cyan")
1486
1622
  table.add_column("Runs", justify="right")
1487
1623
  table.add_column("Traces", justify="right")
@@ -1498,22 +1634,26 @@ def agents_cmd(tools, cloud_mode, days, project, provider, limit, as_json):
1498
1634
  @click.argument("query", required=False)
1499
1635
  @click.option("--agent", "agents", multiple=True, help="Only traces that ran this agent (repeatable)")
1500
1636
  @click.option("--tool", "tools", multiple=True, help="Only traces that used this tool (repeatable)")
1637
+ @click.option("--mcp", "mcps", multiple=True, help="Only traces that used an MCP server matching this (repeatable)")
1501
1638
  @click.option("--session", default=None, help="Session id (or unique prefix)")
1502
1639
  @_filter_opts
1503
- def traces_cmd(query, agents, tools, session, days, project, provider, limit, as_json):
1504
- """List traces, optionally filtered by agent, tool, or free text.
1640
+ def traces_cmd(query, agents, tools, mcps, session, days, project, provider, limit, as_json):
1641
+ """List traces, optionally filtered by agent, tool, MCP server, or free text.
1505
1642
 
1506
- QUERY matches title, project, session id, trace id or an agent name.
1643
+ QUERY matches title, project, session id, trace id, an agent name or an MCP
1644
+ server name. --agent and --tool match the exact name, ignoring case.
1507
1645
 
1508
1646
  \b
1509
1647
  spooling traces latest traces
1510
1648
  spooling traces ses_10dd find by session id
1511
1649
  spooling traces --agent explore traces that ran the explore agent
1650
+ spooling traces --mcp spool traces that used an MCP server matching "spool"
1512
1651
  spooling traces --tool Bash --days 7 traces that used Bash this week
1513
1652
  """
1514
1653
  from spooling.insights import list_traces
1515
1654
  rows = list_traces(query=query, agents=list(agents) or None, tools=list(tools) or None,
1516
- session=session, days=days, project=project, provider=provider, limit=limit)
1655
+ mcps=list(mcps) or None, session=session, days=days, project=project,
1656
+ provider=provider, limit=limit)
1517
1657
  if as_json:
1518
1658
  return _emit_json(rows)
1519
1659
  if not rows:
@@ -1524,17 +1664,21 @@ def traces_cmd(query, agents, tools, session, days, project, provider, limit, as
1524
1664
  table.add_column("Title", style="cyan", max_width=22, overflow="ellipsis", no_wrap=True)
1525
1665
  table.add_column("Session", style="dim", no_wrap=True)
1526
1666
  table.add_column("Agents", max_width=12, overflow="ellipsis", no_wrap=True)
1667
+ table.add_column("MCP", max_width=12, overflow="ellipsis", no_wrap=True)
1527
1668
  table.add_column("Spans", justify="right", no_wrap=True)
1528
1669
  table.add_column("Tools", justify="right", no_wrap=True)
1529
1670
  table.add_column("Cost", justify="right", no_wrap=True)
1671
+
1672
+ def short(names: list[str]) -> str:
1673
+ return ", ".join(names[:1]) + (f" +{len(names) - 1}" if len(names) > 1 else "") if names else "-"
1674
+
1530
1675
  for r in rows:
1531
- ag = r["agents"]
1532
- ag_txt = ", ".join(ag[:1]) + (f" +{len(ag) - 1}" if len(ag) > 1 else "") if ag else "-"
1533
1676
  table.add_row(
1534
1677
  str(r["started_at"] or "")[5:10],
1535
1678
  r["title"] or "(untitled)",
1536
1679
  (r["session_id"] or "")[:10],
1537
- ag_txt,
1680
+ short(r["agents"]),
1681
+ short(r["mcps"]),
1538
1682
  _fmt_num(r["span_count"]),
1539
1683
  _fmt_num(r["tool_count"]),
1540
1684
  f"${float(r['total_cost_usd'] or 0):.2f}",
@@ -627,7 +627,7 @@ _SYNC_OPTION = click.option(
627
627
  @click.option(
628
628
  "--project",
629
629
  default=None,
630
- help="Only push sessions whose project name matches exactly. Pair with `spooling stats --by project` to discover names.",
630
+ help="Only push sessions whose project name matches exactly. Pair with `spooling cost breakdown --by project` to discover names.",
631
631
  )
632
632
  @click.option(
633
633
  "--cwd",