metergraph-cli 0.2.1 → 0.2.3
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.
- package/README.md +60 -11
- package/assets/skill/SKILL.md +52 -3
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +13 -13
- package/assets/skills/metergraph-analyze/SKILL.md +62 -4
- package/assets/skills/metergraph-investigate/SKILL.md +222 -35
- package/assets/skills/metergraph-onboarding/SKILL.md +7 -0
- package/assets/skills/metergraph-report/SKILL.md +19 -2
- package/bin/metergraph.js +1 -1
- package/package.json +1 -1
- package/src/agent-key.js +160 -0
- package/src/approval-handoff.js +5 -0
- package/src/args.js +572 -17
- package/src/auth-browser.js +5 -19
- package/src/auth-callback.js +24 -15
- package/src/auth-login.js +55 -12
- package/src/auth-oauth.js +77 -7
- package/src/callback-page.js +58 -0
- package/src/cli.js +190 -45
- package/src/constants.js +73 -3
- package/src/environment.js +169 -0
- package/src/experiments-contract.js +462 -0
- package/src/experiments-output.js +189 -0
- package/src/experiments.js +397 -0
- package/src/origin.js +6 -0
- package/src/output.js +437 -131
- package/src/pricing-args.js +163 -0
- package/src/pricing-output.js +206 -0
- package/src/pricing-reference.js +195 -0
- package/src/pricing.js +307 -0
- package/src/read-contract.js +976 -16
- package/src/read-output.js +449 -30
- package/src/read.js +312 -60
- package/src/setup-deployment.js +11 -3
- package/src/setup-interactive.js +341 -0
- package/src/setup-refusal.js +172 -0
- package/src/setup-state.js +4 -2
- package/src/setup.js +227 -53
- package/src/skill-bundle.js +2 -2
- package/src/style.js +101 -0
- package/src/transport.js +13 -7
- package/src/usage-aggregates.js +197 -0
- package/src/verify-output.js +21 -11
|
@@ -1,50 +1,183 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: metergraph-investigate
|
|
3
|
-
description: Investigate a failed or anomalous LLM trace, usage spike or incident through
|
|
3
|
+
description: Investigate a failed or anomalous LLM trace, usage spike, cost question (which route, provider or model costs most, a daily trend, a change from the prior period), incident or whether a code change moved cost, reading through the metergraph CLI first (fewer tokens) or a connected Metergraph Agent Access MCP server, answering only from bounded, cited evidence. Use after setup is verified; use the metergraph skill first when neither the CLI nor MCP can read the workspace.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Investigate with Metergraph evidence
|
|
7
7
|
|
|
8
8
|
This skill expands the bounded investigation section of the public `metergraph`
|
|
9
|
-
skill. It adds no tools, scopes or authority.
|
|
10
|
-
connected, or the user has not chosen a client, runtime and deployment, stop and
|
|
11
|
-
use the `metergraph` skill for setup routing. Never ask for, echo or store a
|
|
9
|
+
skill. It adds no tools, scopes or authority. Never ask for, echo or store a
|
|
12
10
|
key.
|
|
13
11
|
|
|
12
|
+
Read through the `metergraph` CLI first when it is installed and can read the
|
|
13
|
+
workspace: it returns the same Metadata evidence as the MCP tools in far fewer
|
|
14
|
+
tokens. Use the MCP tools for what the CLI does not cover (incidents, ingestion
|
|
15
|
+
health, reports, retained content and replay) and in clients that have no
|
|
16
|
+
shell. Stop and use the `metergraph` skill for setup routing only when neither
|
|
17
|
+
the CLI nor a Metergraph MCP server can read the workspace, or the user has not
|
|
18
|
+
chosen a client, runtime and deployment.
|
|
19
|
+
|
|
14
20
|
## 1. Discover before reading data
|
|
15
21
|
|
|
22
|
+
**With the CLI** (preferred when `metergraph --help` lists `status`,
|
|
23
|
+
`capabilities` and `usage`; an older CLI can run but lack them):
|
|
24
|
+
|
|
25
|
+
1. Run `metergraph status --json`. Continue when it exits 0 with
|
|
26
|
+
`data.authenticated: true` and `data.workspace.match: true`. When `match`
|
|
27
|
+
is null because no workspace was pinned (a coding-agent key without
|
|
28
|
+
`--workspace`), name `data.workspace.actual` and continue only after the
|
|
29
|
+
person confirms it is the workspace they mean. When it prints
|
|
30
|
+
`data.next_action` (for example sign in again, or the coding-agent Metadata
|
|
31
|
+
key path for a remote session), follow it; never ask for the key in chat.
|
|
32
|
+
2. Run `metergraph capabilities --json` and build the plan from
|
|
33
|
+
`data.result.agent` (each capability's `available`, `privacy_class` and
|
|
34
|
+
`required_scope`) and `data.result.bounds` (`max_days`, `max_rows`).
|
|
35
|
+
3. Before using any option, check that `metergraph help <command>` lists it.
|
|
36
|
+
An option an older CLI lacks is a reason to use the matching MCP tool, not
|
|
37
|
+
to guess a flag. Always pass `--json`: the evidence is under `data.result`,
|
|
38
|
+
with `data.notices` and `data.next_action` beside it, and the exit code is
|
|
39
|
+
the outcome (for example 14 when the deployment does not offer the read).
|
|
40
|
+
|
|
41
|
+
**With MCP** (no CLI, no shell, or a read the CLI does not offer):
|
|
42
|
+
|
|
16
43
|
1. Call `metergraph_get_workspace_context`, then `metergraph_get_capabilities`.
|
|
17
44
|
Do not call any other Metergraph tool first.
|
|
18
|
-
2.
|
|
19
|
-
`provenance.deployment_profile` match the installation they intend. A staging
|
|
20
|
-
or demo workspace is not their production workspace. Stop on a mismatch,
|
|
21
|
-
an authorization error, or revoked or expired access, and tell the human to
|
|
22
|
-
reconnect through their client's secure flow.
|
|
23
|
-
3. Build the tool plan only from what the server returns: the workspace
|
|
45
|
+
2. Build the tool plan only from what the server returns: the workspace
|
|
24
46
|
context's `access.scopes`, and each capability under the capabilities
|
|
25
47
|
response's `agent` object with its `available`, `privacy_class` (metadata,
|
|
26
48
|
content or replay), `required_scope`, `mutates` and `external_calls`, plus
|
|
27
49
|
`bounds` (`max_days`, `max_rows`). Treat the tools your client lists as the
|
|
28
|
-
only callable tools.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
only callable tools.
|
|
51
|
+
|
|
52
|
+
Either way, confirm with the user that the workspace ID, the workspace name
|
|
53
|
+
and the deployment profile match the installation they intend. A staging or
|
|
54
|
+
demo workspace is not their production workspace. A mismatch, an
|
|
55
|
+
authorization error, or revoked or expired access belongs to that one path:
|
|
56
|
+
when the CLI cannot read the intended workspace but a connected MCP server
|
|
57
|
+
can (or the other way round), use the path that can. Stop and tell the human
|
|
58
|
+
to reconnect through their client's secure flow only when neither the CLI nor
|
|
59
|
+
MCP can verify the intended workspace. A capability that is absent, or
|
|
60
|
+
has `available: false`, is a blocker to report, not something to guess, retry
|
|
61
|
+
under another name or replace with a dashboard API, generic SQL or an ad hoc
|
|
62
|
+
shell command. Some deployments are content-blind or have no replay,
|
|
63
|
+
incidents, ingestion health or reports.
|
|
33
64
|
|
|
34
65
|
## 2. Fix the question and the bound
|
|
35
66
|
|
|
36
|
-
Restate the question, the route or workload if known, and an explicit
|
|
37
|
-
|
|
38
|
-
|
|
67
|
+
Restate the question, the route or workload if known, and an explicit UTC
|
|
68
|
+
window. For grouped usage, windows are UTC: `days: N` means the current UTC
|
|
69
|
+
day so far plus the `N - 1` whole UTC days before it, so today is always
|
|
70
|
+
partial; `since` (inclusive) and `until` (exclusive) give an exact window.
|
|
71
|
+
Plain `metergraph usage --days N` (no grouping) is instead the last N x 24
|
|
72
|
+
hours, rolling and not aligned to UTC days; say so when you report it. Keep
|
|
73
|
+
every window within 90 days and within the capabilities' `bounds.max_days`.
|
|
74
|
+
Default to the shortest window that answers the question. Then choose the
|
|
75
|
+
smallest tool that can answer:
|
|
76
|
+
|
|
77
|
+
| Question | CLI (check its help first) | MCP tool | Bound |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| Which route, provider or model costs most; spend with one provider or model; a daily trend | `metergraph usage --group-by ... --json` with `--route`, `--provider`, `--model` or `--environment` | `metergraph_get_usage_aggregates` with `group_by` and the same exact filters | `--days`/`days` or `--since`/`--until`, small limit |
|
|
80
|
+
| Daily cost per route, when grouped usage is unavailable | `metergraph usage --days N --json` | `metergraph_get_usage` | days, small limit |
|
|
81
|
+
| Which routes exist or are replay eligible | `metergraph routes --json` | `metergraph_list_routes` | none needed |
|
|
82
|
+
| Is data arriving | none; use MCP | `metergraph_get_ingestion_health` | `days` |
|
|
83
|
+
| Known alert or anomaly | none; use MCP | `metergraph_list_incidents` | small `limit` |
|
|
84
|
+
| Cost or calls for one prompt, workflow or other tag value | `metergraph usage --group-by metadata.KEY --json`, or `--metadata KEY=VALUE` | `metergraph_get_usage_aggregates` with `group_by` including `metadata.<key>`, or `metadata` (exact pairs) | days, small limit |
|
|
85
|
+
| One failed call | `metergraph traces --status error --limit 1 --json` (also `--route`, `--metadata`) | `metergraph_query_traces` with `status` set to the failure status, and `route`, `trace_id`, `request_id` or `metadata` | limit 1 to 5 |
|
|
86
|
+
| One costly call (it may have succeeded) | `metergraph traces --route NAME --limit 5 --json` with no `--status`, then compare cost on that page | `metergraph_query_traces` with `route`, `trace_id`, `request_id` or `metadata`, no `status` | limit 1 to 5 |
|
|
87
|
+
| Which routes are slow (route level, not one call) | `metergraph usage --group-by route --sort p95-latency --json`; CLI `traces` returns no latency | `metergraph_get_usage_aggregates` grouped by route | days, small limit |
|
|
88
|
+
| One slow call | none; CLI `traces` has no latency, so ask for its trace or request ID and use MCP, or send the person to the trace in the app | `metergraph_query_traces` with that `trace_id` or `request_id` | limit 1 |
|
|
89
|
+
| A finished analysis | none; use MCP | `metergraph_list_reports`, then `metergraph_get_report` | small `limit` |
|
|
39
90
|
|
|
40
|
-
|
|
91
|
+
Use grouped usage (`metergraph usage --group-by`, or
|
|
92
|
+
`metergraph_get_usage_aggregates`) only when capabilities list
|
|
93
|
+
`usage_aggregates` as available. On an older deployment it is absent: say so,
|
|
94
|
+
answer what `metergraph_get_usage` can (cost per route and day), and do not
|
|
95
|
+
rebuild provider or model totals by paging traces.
|
|
96
|
+
|
|
97
|
+
### Cost questions
|
|
98
|
+
|
|
99
|
+
`metergraph_get_usage_aggregates` groups calls by any ordered subset of
|
|
100
|
+
`provider`, `model`, `route` and `day` (the call's UTC calendar day), plus at
|
|
101
|
+
most one `metadata.<key>`. Cost is
|
|
102
|
+
attributed per call, so a trace that used two providers adds each call's cost
|
|
103
|
+
to that call's provider. Groups come back highest cost first, at most `limit`
|
|
104
|
+
of them; `totals` cover every matching call whatever the limit. From a shell,
|
|
105
|
+
the `metergraph` CLI sends the same read and prints the same evidence. Check
|
|
106
|
+
`metergraph help usage` first: use these commands only when it lists
|
|
107
|
+
`--group-by`. An older CLI has only `metergraph usage --days N` (cost per route
|
|
108
|
+
and day); use the tool instead, or say a newer CLI is needed.
|
|
109
|
+
|
|
110
|
+
| Question | Tool arguments | CLI |
|
|
41
111
|
| --- | --- | --- |
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
112
|
+
| Most expensive route today | `{"group_by": ["route"], "days": 1}` | `metergraph usage --group-by route --today --json` |
|
|
113
|
+
| Spend with one provider | `{"group_by": ["provider"], "provider": ["<name>"], "days": 7}` | `metergraph usage --group-by provider --provider <name> --days 7 --json` |
|
|
114
|
+
| Spend by model | `{"group_by": ["model"], "days": 7}` | `metergraph usage --group-by model --days 7 --json` |
|
|
115
|
+
| One route's daily trend | `{"group_by": ["day"], "route": ["<route>"], "days": 14}` | `metergraph usage --route <route> --group-by day --days 14 --json` |
|
|
116
|
+
| Cost by prompt ID | `{"group_by": ["metadata.prompt_id"], "days": 7}` | `metergraph usage --group-by metadata.prompt_id --days 7 --json` |
|
|
117
|
+
| Today against yesterday | two reads: `days: 1`, then `since`/`until` for the prior UTC day | `metergraph usage --group-by route --today --compare previous --json` |
|
|
118
|
+
|
|
119
|
+
Rank only what was returned. When `coverage.rows.truncated` is true (more
|
|
120
|
+
groups matched than `limit`), a ranking by calls, errors or latency covers
|
|
121
|
+
only the listed groups. Before naming a "top" group, raise `limit` to
|
|
122
|
+
`coverage.rows.matched` when that is at most 200 and at most the
|
|
123
|
+
capabilities' `bounds.max_rows`; otherwise narrow the read. Name the latency
|
|
124
|
+
you report:
|
|
125
|
+
`latency.avg_ms` is the mean over `latency.samples` calls that reported one,
|
|
126
|
+
and `latency.p95_ms` is an estimate from a fixed histogram
|
|
127
|
+
(`p95_method: histogram_estimate`), not an exact percentile.
|
|
128
|
+
|
|
129
|
+
A "costs most" answer needs exact pricing. When `pricing.cost` is
|
|
130
|
+
`lower_bound` (the overall `pricing`, or a group's own), some calls are not
|
|
131
|
+
fully priced, so a group with more unpriced calls can look cheaper than it is.
|
|
132
|
+
Name the group with the highest recorded cost as a lower bound and say the
|
|
133
|
+
ranking is not definitive until pricing is exact; never call it the most
|
|
134
|
+
expensive outright.
|
|
135
|
+
|
|
136
|
+
A prior-period comparison needs two reads with stated windows. Comparing
|
|
137
|
+
today so far with all of yesterday is not like for like: say so, or compare
|
|
138
|
+
with yesterday up to the same UTC time (`--compare previous-same-time` in the
|
|
139
|
+
CLI). A group missing from a truncated window is unknown, not zero.
|
|
140
|
+
|
|
141
|
+
A route is not a prompt. One route (for example `support-reply`) can carry
|
|
142
|
+
several prompt IDs in its metadata (SDK tags), and one prompt ID can run on
|
|
143
|
+
several routes. To answer "which prompt costs most", group by the metadata key
|
|
144
|
+
alone, so each prompt is one row across every route. Add `route` to `group_by`
|
|
145
|
+
only to show how a route splits by prompt. Keep the same `days` on every
|
|
146
|
+
follow-up read, so the examples come from the window you just summarised:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{"name": "metergraph_get_usage_aggregates", "arguments": {"days": 7, "group_by": ["metadata.prompt_id"]}}
|
|
150
|
+
{"name": "metergraph_query_traces", "arguments": {"days": 7, "metadata": {"prompt_id": "reply-v3"}, "limit": 3}}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Metadata values are exact, case-sensitive opaque strings: `reply-v3` does not
|
|
154
|
+
match `reply-v30`, a call without the key never matches a filter, and several
|
|
155
|
+
filter keys must all match. In a read grouped by a metadata key, calls in the
|
|
156
|
+
window that have no value for the grouped key form the group whose value is
|
|
157
|
+
`null`, and `coverage.reasons` names `metadata_key_missing` (or
|
|
158
|
+
`metadata_value_ineligible` for a non-string or oversize value). Report that
|
|
159
|
+
group rather than dropping it. If you also filtered on that same key, no such
|
|
160
|
+
calls remain, so there is no `null` group. A `metadata_key_mostly_missing`
|
|
161
|
+
warning means most calls or cost have no value for the key; say so, and
|
|
162
|
+
suggest checking the key's spelling. `metadata_groups.unlisted` accounts for
|
|
163
|
+
every call not listed with a value: `other_values` (values past the limit, with
|
|
164
|
+
a group count), `missing_key` and `ineligible_value`. Report the spend in
|
|
165
|
+
`missing_key` plus `ineligible_value` as not attributed to any value of the
|
|
166
|
+
key, so a prompt ranking never reads as the whole spend. When
|
|
167
|
+
`coverage.rows.truncated` is true, the listed groups are not the whole window:
|
|
168
|
+
quote `totals` for the window and `coverage.rows.matched` as the number of
|
|
169
|
+
matched groups. That counts groups, not distinct values of the key: with
|
|
170
|
+
`group_by: ["route", "metadata.prompt_id"]` one prompt used on three routes is
|
|
171
|
+
three groups. For a count of distinct values, read with the metadata key as
|
|
172
|
+
the only dimension.
|
|
173
|
+
|
|
174
|
+
The CLI can run the same reads (`metergraph usage --group-by
|
|
175
|
+
metadata.prompt_id --days 7 --json`, where `--group-by-metadata prompt_id` is
|
|
176
|
+
the same dimension and the way to give a key that contains a comma, and
|
|
177
|
+
`metergraph traces --days 7 --metadata prompt_id=reply-v3 --json`) only in versions that
|
|
178
|
+
have these options. Check that `metergraph usage --help` lists `--group-by`
|
|
179
|
+
(and `metergraph traces --help` lists `--metadata`) before you suggest them,
|
|
180
|
+
and otherwise use the tools above.
|
|
48
181
|
|
|
49
182
|
Follow `next_cursor` only while you are still inside the agreed bound, and say
|
|
50
183
|
how many pages you read. Never request every trace, every report or unbounded
|
|
@@ -69,14 +202,23 @@ point to the human action in the Metergraph app instead.
|
|
|
69
202
|
|
|
70
203
|
## 4. Answer only from returned evidence
|
|
71
204
|
|
|
72
|
-
Every answer cites, from the tool responses you actually
|
|
205
|
+
Every answer cites, from the CLI results or tool responses you actually
|
|
206
|
+
received (a CLI result carries the same fields under `data.result`, plus
|
|
207
|
+
`data.notices`):
|
|
73
208
|
|
|
74
209
|
- **Workspace**: workspace name and `provenance.workspace_id`, plus
|
|
75
210
|
`deployment_profile`.
|
|
76
|
-
- **Window**: `window.since` to `window.until` (or `days`) for each
|
|
211
|
+
- **Window**: `window.since` to `window.until` in UTC (or `days`) for each
|
|
212
|
+
read, and whether the last day is partial.
|
|
77
213
|
- **Provenance**: the tool name, `provenance.source` and `generated_at`.
|
|
78
214
|
- **Evidence and completeness**: `evidence.rows`, `evidence.complete`,
|
|
79
|
-
`page.truncated` and whether a `next_cursor` was left unread
|
|
215
|
+
`page.truncated` and whether a `next_cursor` was left unread; for usage,
|
|
216
|
+
`coverage.completeness` with its `reasons`, `coverage.rows.matched` against
|
|
217
|
+
the rows returned, and whether a total is the whole population
|
|
218
|
+
(`totals.scope`) or only the returned rows.
|
|
219
|
+
- **Pricing and attribution**: `pricing.cost` (`exact` or a `lower_bound`) and
|
|
220
|
+
`attribution.level`. A trace cost is a whole-trace total and is never split
|
|
221
|
+
by provider.
|
|
80
222
|
- **Warnings**: every returned `warnings[].code` with its message. Say "no
|
|
81
223
|
warnings returned" when the list is empty.
|
|
82
224
|
- **Identifiers and links**: trace, request, incident or analysis IDs, and
|
|
@@ -87,16 +229,61 @@ window, incomplete or truncated evidence, capability unavailable in this
|
|
|
87
229
|
deployment, classification pending, ambiguous request match
|
|
88
230
|
(`request_match.ambiguous`), failed analysis, and missing report. An empty page
|
|
89
231
|
does not prove there was no traffic; check ingestion health or a longer bounded
|
|
90
|
-
window before saying so. Do not round, extrapolate or invent totals.
|
|
232
|
+
window before saying so. Do not round, extrapolate or invent totals. Never
|
|
233
|
+
attribute a cost change, or the absence of one, to a code change from source
|
|
234
|
+
code or a git diff; see section 5.
|
|
235
|
+
|
|
236
|
+
## 5. Code changes and cost: never decide from the diff
|
|
237
|
+
|
|
238
|
+
When the question is whether a commit, pull request, deploy or config change
|
|
239
|
+
raised, lowered or did not change cost, a git diff is never the evidence. Read
|
|
240
|
+
it to form a hypothesis (a different model, a longer prompt, a new retry loop, a
|
|
241
|
+
changed `max_tokens`, an added tool call), then say it is a hypothesis. Do not
|
|
242
|
+
answer "this commit increased cost", "this commit saved money" or "this commit
|
|
243
|
+
has no cost impact" from code alone, including a diff that touches no model or
|
|
244
|
+
prompt code: wrappers, retries, caching, routing, traffic and provider prices
|
|
245
|
+
change spend without appearing in the diff.
|
|
246
|
+
|
|
247
|
+
A cost conclusion needs returned Metergraph usage evidence:
|
|
248
|
+
|
|
249
|
+
1. Establish when the change reached the traffic Metergraph observes, from
|
|
250
|
+
recorded commit or deployment identity (a deploy record, or the commit the
|
|
251
|
+
telemetry itself carries). Merge time is not deploy time. Without that
|
|
252
|
+
record, any before-and-after reading is only a time correlation: call it
|
|
253
|
+
that, give the windows, and do not attribute the difference to the commit.
|
|
254
|
+
If no one knows when it shipped, say the comparison cannot be anchored.
|
|
255
|
+
2. Read usage for the same route or workload over comparable, bounded UTC
|
|
256
|
+
windows before and after that point (`metergraph_get_usage_aggregates`
|
|
257
|
+
with explicit `since` and `until` and the same filters, or
|
|
258
|
+
`metergraph_get_usage` where grouped usage is unavailable), and cite both
|
|
259
|
+
reads as in section 4. Windows of equal length avoid comparing a partial
|
|
260
|
+
day with a whole one. Filter both reads to the environment the deployment
|
|
261
|
+
record names (`environment`), so traffic in another environment that
|
|
262
|
+
shares the route is not credited to the change. If that environment
|
|
263
|
+
cannot be isolated (for example the grant or deployment has no
|
|
264
|
+
environment filter), say the cost impact is unknown.
|
|
265
|
+
3. Compare per-call cost and token counts as well as totals, so a traffic
|
|
266
|
+
change is not mistaken for a code change. Name what else changed in the
|
|
267
|
+
window when you know it (traffic, provider prices, other deploys).
|
|
268
|
+
4. If either window is incomplete, truncated or warned, if the windows are not
|
|
269
|
+
comparable, or if the returned data cannot separate traffic by the change,
|
|
270
|
+
state that the cost impact is unknown and name the next bounded check. Do
|
|
271
|
+
not fill the gap from the diff.
|
|
272
|
+
|
|
273
|
+
Present the diff and the usage evidence separately: what the code suggests,
|
|
274
|
+
and what Metergraph measured.
|
|
91
275
|
|
|
92
|
-
##
|
|
276
|
+
## 6. Worked workflow: one failed trace
|
|
93
277
|
|
|
94
|
-
1. Discover:
|
|
95
|
-
|
|
96
|
-
|
|
278
|
+
1. Discover: `metergraph status --json` and `metergraph capabilities --json`
|
|
279
|
+
(or the context and capabilities tools). Confirm the workspace with the user.
|
|
280
|
+
2. Find: `metergraph traces --status error --days N --limit 1 --json` (or
|
|
281
|
+
`metergraph_query_traces` with `status` set to the failure status, the
|
|
282
|
+
agreed `days` and `limit` 1). If the user gave a request ID and the match is
|
|
97
283
|
ambiguous, list the candidates and ask which one.
|
|
98
|
-
3. Explain from metadata: route, model, provider, tokens, cost,
|
|
99
|
-
|
|
284
|
+
3. Explain from metadata: route, model, provider, tokens, cost, status and
|
|
285
|
+
span count, plus latency only when the read returns it (CLI `traces` does
|
|
286
|
+
not; never infer it from timestamps), with the citations above. If metadata is enough, stop here.
|
|
100
287
|
4. Optional content: with consent, `metergraph_get_trace` for that one trace to
|
|
101
288
|
read the error, tool calls and root-cause classification.
|
|
102
289
|
5. Optional replay: only with explicit approval and when the trace is replay
|
|
@@ -117,6 +117,13 @@ connection, paid eval rule or provider invocation just to manufacture proof.
|
|
|
117
117
|
still needed and the next eval selection action. A synthetic demonstration
|
|
118
118
|
does not meet real application or hosted pilot acceptance.
|
|
119
119
|
|
|
120
|
+
A person's first cost question after setup ("which route costs most today")
|
|
121
|
+
is a bounded usage read, not a trace scan: `metergraph usage --group-by route
|
|
122
|
+
--today --json` from a CLI whose `metergraph help usage` lists `--group-by`, or
|
|
123
|
+
`metergraph_get_usage_aggregates` when capabilities list `usage_aggregates`. Use `metergraph-investigate` for it. A
|
|
124
|
+
remote or CI agent reads with the coding-agent Metadata key path in the
|
|
125
|
+
`metergraph` skill, never with the application's ingest key.
|
|
126
|
+
|
|
120
127
|
Verify the named trace through the connected client's advertised MCP tools,
|
|
121
128
|
with an explicit source label and the same workspace/deployment before and
|
|
122
129
|
after any permitted content read. The older trace-debug contract has no
|
|
@@ -18,8 +18,25 @@ only with the tools the capabilities advertise.
|
|
|
18
18
|
## 1. Read it
|
|
19
19
|
|
|
20
20
|
1. `metergraph_get_analysis_run` with the run ID. Continue only when the status
|
|
21
|
-
is final. If
|
|
22
|
-
|
|
21
|
+
is final. If it is not, show where the run is, as described next, and stop. If
|
|
22
|
+
`report.available` is false on a completed run, the report is missing: say
|
|
23
|
+
so and give the run's page (below).
|
|
24
|
+
|
|
25
|
+
For a run that is not final, draw `run.stages.items` in the order returned,
|
|
26
|
+
using each item's `state` exactly as given: `✓` completed, `●` current, `✗`
|
|
27
|
+
failed, `–` skipped, `○` not started. For example:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
✓ queued → ✓ exporting → ● analyzing → ○ classified → ○ importing → ○ completed
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Under it, say in plain text which stages are done, which one is current and
|
|
34
|
+
since when (its `entered_at`, checked at `provenance.generated_at`), and
|
|
35
|
+
which are still to come. Never give a percentage, an ETA or a time remaining.
|
|
36
|
+
When `run.stages.available` is false, or `stages` is missing, draw nothing:
|
|
37
|
+
say the stage breakdown is unavailable (`unavailable_reason`, when present)
|
|
38
|
+
and report the `status` and timestamps instead. `metergraph-analyze`, when
|
|
39
|
+
installed, describes the same progress report in more detail.
|
|
23
40
|
2. `metergraph_get_report` with `report.analysis_id`. Read the `outcome`, each
|
|
24
41
|
workload's `state`, `sample_count` and `reason`, each `model_swap`
|
|
25
42
|
opportunity (`recommendation_status`, `quality_vs_reference`,
|
package/bin/metergraph.js
CHANGED
|
@@ -7,5 +7,5 @@ import { main } from "../src/cli.js";
|
|
|
7
7
|
process.stdout.on("error", () => {});
|
|
8
8
|
|
|
9
9
|
const argv = process.argv.slice(2);
|
|
10
|
-
const io = { stdout: process.stdout, stderr: process.stderr };
|
|
10
|
+
const io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin };
|
|
11
11
|
process.exitCode = handsOff(argv) ? await handOff(argv, io) : await main(argv, io);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metergraph-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Metergraph command line tool with a read-only connection probe, a project skill installer, Metadata-only sign in and bounded workspace Metadata reads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/agent-key.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { verifyContext } from "./auth-oauth.js";
|
|
2
|
+
import { DEFAULT_ORIGIN, MAX_BODY_BYTES, METADATA_SCOPE } from "./constants.js";
|
|
3
|
+
import { CREDENTIAL_MIN_LENGTH, readCredentialFile } from "./deployment-credential.js";
|
|
4
|
+
import { checkDeployment } from "./doctor.js";
|
|
5
|
+
import { AGENT_TOKEN_ENV, AGENT_URL_ENV } from "./environment.js";
|
|
6
|
+
import { parseOrigin } from "./origin.js";
|
|
7
|
+
import { send } from "./transport.js";
|
|
8
|
+
|
|
9
|
+
// The read commands' second credential: a coding-agent key with Metadata
|
|
10
|
+
// access that a person created on the Keys page and handed to the agent's
|
|
11
|
+
// environment as a secret. It is for agents where browser sign in cannot
|
|
12
|
+
// finish (SSH, cloud workspaces, CI, some containers).
|
|
13
|
+
//
|
|
14
|
+
// The key comes only from the METERGRAPH_AGENT_TOKEN environment variable or
|
|
15
|
+
// from a private file named by --agent-token-file (checked as setup checks
|
|
16
|
+
// its agent token file). There is no argv or stdin form. The key is never
|
|
17
|
+
// written anywhere, never printed, and sent only as a bearer header to the
|
|
18
|
+
// chosen origin, and only after that origin has answered the unauthenticated
|
|
19
|
+
// deployment probe as a supported Metergraph service. Before any read, the
|
|
20
|
+
// service's own workspace and capabilities documents must show exactly the
|
|
21
|
+
// Metadata scope, no content access, the expected deployment profile and,
|
|
22
|
+
// when one is pinned, the expected workspace. A broader key fails closed:
|
|
23
|
+
// there is no fallback to Debug or Replay access.
|
|
24
|
+
|
|
25
|
+
const TOKEN = new RegExp(`^[\\x21-\\x7e]{${CREDENTIAL_MIN_LENGTH},8192}$`);
|
|
26
|
+
|
|
27
|
+
const stop = (outcome, reason) => ({ ok: false, outcome, reason });
|
|
28
|
+
|
|
29
|
+
// Which key source this run has, or null. An explicit file wins over the
|
|
30
|
+
// environment variable.
|
|
31
|
+
export function agentKeySource(options, env) {
|
|
32
|
+
if (typeof options.agentTokenFile === "string") return { kind: "file", file: options.agentTokenFile };
|
|
33
|
+
if (typeof env[AGENT_TOKEN_ENV] === "string" && env[AGENT_TOKEN_ENV] !== "") return { kind: "env" };
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Where the key may be sent and what it must prove, before any request.
|
|
38
|
+
// A project binding pins the origin, workspace and profile; --url (or
|
|
39
|
+
// METERGRAPH_URL) and --workspace may repeat them but never contradict them.
|
|
40
|
+
// Without a binding, --workspace pins the workspace; without that, the
|
|
41
|
+
// key's own workspace is used and reported as unpinned.
|
|
42
|
+
// originConfirmed is true when this machine holds a saved sign in for the
|
|
43
|
+
// binding's origin, which proves the person chose that origin here.
|
|
44
|
+
export function keyTarget({ binding, options, env, originConfirmed = false }) {
|
|
45
|
+
let requested = options.origin ?? null;
|
|
46
|
+
let source = requested === null ? null : "flag";
|
|
47
|
+
const rawEnv = env[AGENT_URL_ENV];
|
|
48
|
+
if (requested === null && typeof rawEnv === "string" && rawEnv !== "") {
|
|
49
|
+
requested = parseOrigin(rawEnv);
|
|
50
|
+
if (requested === null) return stop("invalid_input", "metergraph_url_invalid");
|
|
51
|
+
source = "env";
|
|
52
|
+
}
|
|
53
|
+
const pinned = options.workspace ?? null;
|
|
54
|
+
if (binding !== null) {
|
|
55
|
+
if (requested !== null && requested !== binding.origin) return stop("conflict", "bound_to_other_origin");
|
|
56
|
+
// The binding is repository content, so it may come from anyone who can
|
|
57
|
+
// commit. A key is sent to the origin it names only when that is the
|
|
58
|
+
// hosted service, this machine signed in to it, or the person confirmed
|
|
59
|
+
// the same origin with --url or METERGRAPH_URL; otherwise a committed
|
|
60
|
+
// file could direct the key to another server.
|
|
61
|
+
if (requested === null && binding.origin !== DEFAULT_ORIGIN && !originConfirmed) {
|
|
62
|
+
return stop("conflict", "agent_key_origin_unconfirmed");
|
|
63
|
+
}
|
|
64
|
+
if (pinned !== null && pinned !== binding.workspace_id) return stop("conflict", "bound_to_other_workspace");
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
origin: binding.origin,
|
|
68
|
+
workspaceId: binding.workspace_id,
|
|
69
|
+
profile: binding.deployment_profile,
|
|
70
|
+
originSource: "binding",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
origin: requested ?? DEFAULT_ORIGIN,
|
|
76
|
+
workspaceId: pinned,
|
|
77
|
+
profile: null,
|
|
78
|
+
originSource: source ?? "default",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Resolves with the same shape as auth-session's sessionFor:
|
|
83
|
+
// { ok: true, session, documents, knownCredentials }
|
|
84
|
+
// { ok: false, outcome, reason }
|
|
85
|
+
// verifiedProfile is the profile the caller already read from GET
|
|
86
|
+
// /v1/deployment under the same deadline, or null to read it here.
|
|
87
|
+
// access is "metadata" for the read commands, or "experiment_start" for
|
|
88
|
+
// experiments start, which needs Debug access and the start grant instead
|
|
89
|
+
// (verifyContext) and uses the key for that one request.
|
|
90
|
+
export async function keySession({ target, source, env, signal, verifiedProfile = null, access = "metadata" }) {
|
|
91
|
+
// A key already in the environment is checked for shape before any
|
|
92
|
+
// request; a key file is opened only once the origin has proven itself.
|
|
93
|
+
let key = source.kind === "env" ? readAgentKey(source, env) : null;
|
|
94
|
+
if (key !== null && !key.ok) return key;
|
|
95
|
+
let profile = verifiedProfile;
|
|
96
|
+
if (profile === null) {
|
|
97
|
+
const deployment = checkDeployment(
|
|
98
|
+
await send(target.origin, "/v1/deployment", { signal, maxBytes: MAX_BODY_BYTES }),
|
|
99
|
+
);
|
|
100
|
+
if (deployment.outcome !== null) return stop(deployment.outcome, deployment.reason);
|
|
101
|
+
profile = deployment.profile;
|
|
102
|
+
}
|
|
103
|
+
if (target.profile !== null && profile !== target.profile) return stop("verification_failed", "profile_mismatch");
|
|
104
|
+
|
|
105
|
+
// Only now, with the origin proven to be a supported Metergraph service,
|
|
106
|
+
// is the key read.
|
|
107
|
+
if (key === null) key = readAgentKey(source, env);
|
|
108
|
+
if (!key.ok) return key;
|
|
109
|
+
const { token } = key;
|
|
110
|
+
|
|
111
|
+
const verified = await verifyContext(
|
|
112
|
+
target.origin,
|
|
113
|
+
token,
|
|
114
|
+
{ profile, workspaceId: target.workspaceId, anyWorkspace: target.workspaceId === null, access },
|
|
115
|
+
signal,
|
|
116
|
+
);
|
|
117
|
+
if (!verified.ok) {
|
|
118
|
+
if (signal?.aborted) return stop("authorization_failed", "cancelled");
|
|
119
|
+
// 401: the key is unknown, expired or revoked. 403: the key is valid but
|
|
120
|
+
// lacks Metadata access to this workspace (for example an ingest key).
|
|
121
|
+
if (verified.denied && verified.status === 403) return stop("permission_denied", "agent_key_insufficient_scope");
|
|
122
|
+
if (verified.denied) return stop("login_required", "agent_key_rejected");
|
|
123
|
+
return stop(verified.outcome, verified.reason);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
session: {
|
|
128
|
+
origin: target.origin,
|
|
129
|
+
workspaceId: verified.workspaceId,
|
|
130
|
+
profile,
|
|
131
|
+
scopes: access === "metadata" ? [METADATA_SCOPE] : verified.scopes,
|
|
132
|
+
accessToken: token,
|
|
133
|
+
},
|
|
134
|
+
documents: verified.documents,
|
|
135
|
+
// Internal only, for checking output before it is printed.
|
|
136
|
+
knownCredentials: [token],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// { ok: true, token } or { ok: false, outcome, reason } with a fixed reason.
|
|
141
|
+
// No path, value or system error text is ever returned.
|
|
142
|
+
export function readAgentKey(source, env) {
|
|
143
|
+
if (source.kind === "file") {
|
|
144
|
+
const read = readCredentialFile(source.file);
|
|
145
|
+
if (read.ok) return read;
|
|
146
|
+
// The file's own reasons, renamed so they are never mistaken for the
|
|
147
|
+
// private sign in store's reasons: credential_missing becomes
|
|
148
|
+
// agent_token_file_missing, and so on.
|
|
149
|
+
const reason = read.reason.startsWith("credential_") ? read.reason.slice("credential_".length) : read.reason;
|
|
150
|
+
return stop(read.outcome, read.reason === "platform_credential_handoff" ? read.reason : `agent_token_file_${reason}`);
|
|
151
|
+
}
|
|
152
|
+
let value = env[AGENT_TOKEN_ENV];
|
|
153
|
+
if (typeof value !== "string") return stop("login_required", "agent_token_missing");
|
|
154
|
+
if (value.endsWith("\r\n")) value = value.slice(0, -2);
|
|
155
|
+
else if (value.endsWith("\n")) value = value.slice(0, -1);
|
|
156
|
+
// One token only: whitespace means a label such as "Bearer", a second
|
|
157
|
+
// token or a second line.
|
|
158
|
+
if (!TOKEN.test(value)) return stop("invalid_input", "agent_token_malformed");
|
|
159
|
+
return { ok: true, token: value };
|
|
160
|
+
}
|
package/src/approval-handoff.js
CHANGED
|
@@ -19,6 +19,11 @@ import { envelope, toJsonLine } from "./output.js";
|
|
|
19
19
|
const BIN = fileURLToPath(new URL("../bin/metergraph.js", import.meta.url));
|
|
20
20
|
const WAITER = "METERGRAPH_APPROVAL_WAITER";
|
|
21
21
|
|
|
22
|
+
// True in the detached waiter that handOff starts.
|
|
23
|
+
export function isApprovalWaiter(env = process.env) {
|
|
24
|
+
return env[WAITER] === "1";
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export function handsOff(argv, env = process.env) {
|
|
23
28
|
if (env[WAITER] === "1") return false;
|
|
24
29
|
const parsed = parseArgs(argv);
|