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.
- {spooling-0.2.12/spooling.egg-info → spooling-0.2.14}/PKG-INFO +60 -10
- {spooling-0.2.12 → spooling-0.2.14}/README.md +59 -9
- {spooling-0.2.12 → spooling-0.2.14}/pyproject.toml +1 -1
- {spooling-0.2.12 → spooling-0.2.14}/spooling/cli.py +172 -28
- {spooling-0.2.12 → spooling-0.2.14}/spooling/cloud.py +1 -1
- spooling-0.2.14/spooling/insights.py +427 -0
- {spooling-0.2.12 → spooling-0.2.14/spooling.egg-info}/PKG-INFO +60 -10
- {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/SOURCES.txt +1 -0
- spooling-0.2.14/tests/test_docs.py +31 -0
- spooling-0.2.14/tests/test_insights.py +407 -0
- spooling-0.2.12/spooling/insights.py +0 -253
- spooling-0.2.12/tests/test_insights.py +0 -218
- {spooling-0.2.12 → spooling-0.2.14}/LICENSE +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/setup.cfg +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/__init__.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/agent.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/classifiers.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/config.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/__init__.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/base.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/bigquery.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/clickhouse.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/factory.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/mongodb.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/mysql.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/postgresql.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/rest/__init__.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/rest/base.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/rest/shopify_connectors.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/snowflake.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/connectors/types.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/db.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/embeddings.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/evals.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/experiments.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/ingest.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/mcp_server.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/optimize.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/parser.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/pricing.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/__init__.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/antigravity.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/base.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/claude_code.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/codex.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/copilot.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/cursor.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/gemini.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/github.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/gitlab.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/kiro.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/opencode.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/session_file.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/providers/windsurf.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/redact.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/remote_otel.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/schema.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/sdk.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/search.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/server.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/sessions.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/stats.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/subscription_pricing.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/tracing.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/tunnel.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/tunnel_auth.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling/watcher.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/requires.txt +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/spooling.egg-info/top_level.txt +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/tests/test_agent_spans.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/tests/test_claude_code_provider.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/tests/test_cloud_errors.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/tests/test_mcp_auth_middleware.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/tests/test_session_ids.py +0 -0
- {spooling-0.2.12 → spooling-0.2.14}/tests/test_storage_optimizations.py +0 -0
- {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.
|
|
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
|
|
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 --
|
|
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
|
|
171
|
-
spooling traces ses_10dd # Search title, project, session id, trace id or
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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 --
|
|
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
|
|
135
|
-
spooling traces ses_10dd # Search title, project, session id, trace id or
|
|
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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
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 --
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 --
|
|
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()
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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",
|