metergraph-cli 0.2.2 → 0.2.4

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 (66) hide show
  1. package/README.md +121 -13
  2. package/assets/skill/SKILL.md +124 -31
  3. package/assets/skill/manifest.json +3 -3
  4. package/assets/skills/manifest.json +31 -31
  5. package/assets/skills/metergraph-analyze/SKILL.md +161 -58
  6. package/assets/skills/metergraph-candidates/SKILL.md +50 -8
  7. package/assets/skills/metergraph-evals/SKILL.md +83 -16
  8. package/assets/skills/metergraph-investigate/SKILL.md +90 -34
  9. package/assets/skills/metergraph-iterate/SKILL.md +82 -12
  10. package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
  11. package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
  12. package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
  13. package/assets/skills/metergraph-report/SKILL.md +60 -15
  14. package/assets/skills/metergraph-workloads/SKILL.md +97 -13
  15. package/bin/metergraph.js +1 -1
  16. package/native/README.md +85 -0
  17. package/native/anchored-fs.c +243 -0
  18. package/native/bin/anchored-fs-darwin-arm64 +0 -0
  19. package/native/bin/anchored-fs-darwin-x64 +0 -0
  20. package/native/bin/anchored-fs-linux-arm64 +0 -0
  21. package/native/bin/anchored-fs-linux-x64 +0 -0
  22. package/native/build.mjs +55 -0
  23. package/native/manifest.json +9 -0
  24. package/native/targets.mjs +24 -0
  25. package/package.json +10 -3
  26. package/src/action-help.js +130 -0
  27. package/src/agent-key.js +22 -12
  28. package/src/anchored-fs.js +163 -0
  29. package/src/args.js +335 -31
  30. package/src/auth-binding.js +12 -7
  31. package/src/auth-callback.js +10 -2
  32. package/src/auth-login.js +111 -28
  33. package/src/auth-oauth.js +133 -36
  34. package/src/auth-session.js +19 -12
  35. package/src/auth-store.js +28 -7
  36. package/src/cli.js +147 -15
  37. package/src/constants.js +70 -1
  38. package/src/experiments-contract.js +462 -0
  39. package/src/experiments-output.js +190 -0
  40. package/src/experiments.js +406 -0
  41. package/src/output.js +374 -68
  42. package/src/pricing-args.js +163 -0
  43. package/src/pricing-output.js +206 -0
  44. package/src/pricing-reference.js +195 -0
  45. package/src/pricing.js +307 -0
  46. package/src/read-contract.js +64 -10
  47. package/src/read-output.js +66 -13
  48. package/src/read.js +105 -16
  49. package/src/repository-identity.js +2 -2
  50. package/src/setup-deployment.js +7 -0
  51. package/src/setup-env-git.js +1 -1
  52. package/src/setup-env.js +16 -9
  53. package/src/setup-interactive.js +356 -0
  54. package/src/setup-refusal.js +2 -2
  55. package/src/setup-state.js +9 -4
  56. package/src/setup.js +15 -3
  57. package/src/skill-bundle.js +2 -2
  58. package/src/skill-trusted.js +56 -0
  59. package/src/skill.js +28 -9
  60. package/src/style.js +33 -7
  61. package/src/tools-output.js +73 -0
  62. package/src/tools.js +249 -0
  63. package/src/trace-contract.js +1 -1
  64. package/src/transport.js +26 -3
  65. package/src/verify-output.js +2 -2
  66. package/src/verify.js +5 -2
package/src/output.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { actionHelpData, displayOptions } from "./action-help.js";
2
+ import { PRICING_OPTIONS } from "./pricing-args.js";
1
3
  import {
2
4
  CONNECTION_GUIDE_URL,
3
5
  DEFAULT_ORIGIN,
@@ -27,7 +29,7 @@ import {
27
29
  SUPPORTED_PROFILES,
28
30
  VERSION,
29
31
  } from "./constants.js";
30
- import { PLAIN, fields, headline, paragraph, render, section, steps, toneFor } from "./style.js";
32
+ import { PLAIN, fields, headline, paragraph, render, section, steps, toneFor, workspaceShown } from "./style.js";
31
33
 
32
34
  // Every JSON result, success or failure, has the same top-level keys:
33
35
  // schema_version, command, ok, outcome, exit_code, data, error
@@ -48,7 +50,20 @@ export function envelope({ command, outcome, data = null, reason = null, message
48
50
  }
49
51
 
50
52
  export function toJsonLine(result) {
51
- return `${JSON.stringify(result)}\n`;
53
+ return `${jsonText(result)}\n`;
54
+ }
55
+
56
+ // JSON.stringify escapes C0 controls but leaves C1 controls (a terminal may
57
+ // read U+009B as an escape sequence) and bidirectional overrides as they are.
58
+ // Inside a JSON string \uXXXX means the same character, so escaping them
59
+ // changes nothing a parser sees and keeps them away from the terminal.
60
+ const TERMINAL_UNSAFE = /[\u061c\u007f-\u009f\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
61
+
62
+ export function jsonText(value, indent = undefined) {
63
+ return JSON.stringify(value, null, indent).replace(
64
+ TERMINAL_UNSAFE,
65
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
66
+ );
52
67
  }
53
68
 
54
69
  const DOCTOR_OPTIONS = [
@@ -125,9 +140,14 @@ const AGENT_PATH_HELP = [
125
140
  " SSH, cloud workspace or CI A person creates a coding-agent key with Metadata access",
126
141
  " on the Keys page and saves it as the secret",
127
142
  " METERGRAPH_AGENT_TOKEN; then status --json, usage --json",
128
- " Metadata key, no saved sign in The same: reads use METERGRAPH_AGENT_TOKEN, or",
143
+ " A key, no saved sign in The same: reads use METERGRAPH_AGENT_TOKEN, or",
129
144
  " --agent-token-file PATH, after verifying the workspace",
130
- "Never pass a key as an argument. Debug and Replay keys are refused, not reduced.",
145
+ " Evaluations and analyses A workspace owner creates a key with the Debug role and",
146
+ " Allow evaluation writes, Allow evaluation previews and",
147
+ " Allow analysis starts; the same METERGRAPH_AGENT_TOKEN",
148
+ " serves the reads. status --json reports the role and each",
149
+ " permission in data.access",
150
+ "Never pass a key as an argument. Replay keys are refused.",
131
151
  ];
132
152
 
133
153
  const CONFIG_DIR_OPTION = {
@@ -143,14 +163,34 @@ const LOGIN_OPTIONS = [
143
163
  name: "--runtime",
144
164
  value: "RUNTIME",
145
165
  summary:
146
- "Required. Where this terminal runs, not which Metergraph service. local: your own machine, " +
166
+ "Required for scripts; inferred as local in an interactive local terminal. Where this terminal runs, not which Metergraph service. local: your own machine, " +
147
167
  "including for the hosted service. cloud, cloud-no-shell: a cloud sandbox; get the connection guide.",
148
168
  },
149
169
  { name: "--url", value: "ORIGIN", summary: `Origin to sign in to. Default ${DEFAULT_ORIGIN}.` },
150
170
  {
151
171
  name: "--workspace",
152
- value: "UUID",
153
- summary: "Expected workspace ID. Sign in fails unless the browser grants exactly this workspace.",
172
+ value: "SLUG",
173
+ summary:
174
+ "The workspace to sign in to, by its slug (on its Settings page) or its name. A service that can, offers " +
175
+ "approval for that workspace alone; sign in fails unless the browser grants it.",
176
+ },
177
+ {
178
+ name: "--role",
179
+ value: "ROLE",
180
+ summary:
181
+ "metadata (default): usage, cost and trace questions, no captured content. debug: also reads captured " +
182
+ "prompts and responses through Agent Access; the --allow-* options add workflow permissions to it.",
183
+ },
184
+ {
185
+ name: "--allow-evaluation-writes",
186
+ value: null,
187
+ summary: "With --role debug: Allow evaluation writes. Only a workspace owner approves the workflow permissions.",
188
+ },
189
+ { name: "--allow-evaluation-previews", value: null, summary: "With --role debug: Allow evaluation previews." },
190
+ {
191
+ name: "--allow-analysis-starts",
192
+ value: null,
193
+ summary: "With --role debug: Allow analysis starts, which replays captured prompts on model providers and spends money.",
154
194
  },
155
195
  { name: "--project", value: "DIR", summary: "Existing project directory to bind. Default: the current directory." },
156
196
  CONFIG_DIR_OPTION,
@@ -172,8 +212,9 @@ const LOGOUT_OPTIONS = [
172
212
  ];
173
213
 
174
214
  const LOGIN_USAGE =
175
- "metergraph login --runtime local [--url ORIGIN] [--workspace UUID] [--project DIR] [--config-dir DIR] " +
176
- "[--timeout-ms N] [--signup] [--no-browser] [--reconnect] [--json]";
215
+ "metergraph login --runtime local [--url ORIGIN] [--workspace SLUG] [--role metadata|debug] " +
216
+ "[--allow-evaluation-writes] [--allow-evaluation-previews] [--allow-analysis-starts] [--project DIR] " +
217
+ "[--config-dir DIR] [--timeout-ms N] [--signup] [--no-browser] [--reconnect] [--json]";
177
218
  const LOGOUT_USAGE = "metergraph logout [--project DIR] [--config-dir DIR] [--json]";
178
219
  const SETUP_USAGE = "metergraph setup --runtime local (--client codex|claude|cursor | --skip-skill) " +
179
220
  "[--url ORIGIN] [--workspace UUID] [--project DIR] [--config-dir DIR] [--env-file .env] " +
@@ -216,22 +257,73 @@ const READ_BASE_OPTIONS = [
216
257
  name: "--url",
217
258
  value: "ORIGIN",
218
259
  summary:
219
- "Where a Metadata key is sent when the project is not bound. Default METERGRAPH_URL, else the hosted service. " +
260
+ "Where a coding-agent key is sent when the project is not bound. Default METERGRAPH_URL, else the hosted service. " +
220
261
  "A bound project's origin cannot be changed here.",
221
262
  },
222
263
  {
223
264
  name: "--workspace",
224
- value: "UUID",
225
- summary: "The workspace the credential must be for. Without it, an unbound key's own workspace is used.",
265
+ value: "SLUG",
266
+ summary:
267
+ "The workspace the credential must be for, by its slug or name (or ID). Without it, an unbound key's own " +
268
+ "workspace is used.",
226
269
  },
227
270
  {
228
271
  name: "--agent-token-file",
229
272
  value: "PATH",
230
273
  summary:
231
- "Private (0600) file holding a coding-agent key with Metadata access. Used instead of a saved sign in. " +
274
+ "Private (0600) file holding a coding-agent key with the Metadata or Debug role. Used instead of a saved sign in. " +
232
275
  "Without it, METERGRAPH_AGENT_TOKEN is used when this project has no saved sign in on this machine.",
233
276
  },
234
277
  ];
278
+ const EXPERIMENTS_USAGE = [
279
+ "metergraph experiments list [--workload ID] [--limit N] [read options] [--json]",
280
+ "metergraph experiments estimate --workload ID --treatment SPEC [--treatment SPEC]... [--last-n N] " +
281
+ "[--sample-model MODEL] [--track PATH]... [read options] [--json]",
282
+ "metergraph experiments start (same as estimate) [--agent-token-file PATH] [--json]",
283
+ "metergraph experiments get --id UUID [read options] [--json]",
284
+ ];
285
+ const EXPERIMENTS_OPTIONS = [
286
+ { name: "--workload", value: "ID", summary: "The workload (route) whose recent production calls are replayed. Required for estimate and start; a filter for list." },
287
+ {
288
+ name: "--treatment",
289
+ value: "SPEC",
290
+ summary:
291
+ "1 to 3 times. PROVIDER/MODEL, optionally followed by ,label=NAME ,temperature=0..2 ,max_tokens=1..32000 " +
292
+ "or ,reasoning_effort=none|minimal|low|medium|high|xhigh|max. Unset settings follow production.",
293
+ },
294
+ { name: "--last-n", value: "N", summary: "Replay the last N replayable calls, 10 to 40. Default 20." },
295
+ { name: "--sample-model", value: "MODEL", summary: "Only production calls made with this exact model, for a single-model control." },
296
+ { name: "--track", value: "PATH", summary: "Up to 10 times. A JSON path such as $.priority whose values the dashboard compares." },
297
+ { name: "--id", value: "UUID", summary: "The experiment to read (get)." },
298
+ { name: "--limit", value: "N", summary: "Experiments to list, 1 to 50. Default 20." },
299
+ ...READ_BASE_OPTIONS.filter((option) => option.name !== "--agent-token-file"),
300
+ {
301
+ name: "--agent-token-file",
302
+ value: "PATH",
303
+ summary:
304
+ "Private (0600) file holding a coding-agent key. For list, estimate and get: a key with the Metadata or Debug role. " +
305
+ "For start: a key with Debug access and Allow analysis starts, used for that one request.",
306
+ },
307
+ JSON_OPTION,
308
+ ];
309
+ const TOOLS_USAGE = [
310
+ "metergraph tools list [--name TOOL] [read options] [--json]",
311
+ "metergraph tools call TOOL [--args JSON | --args-file PATH] [--yes] [read options] [--json]",
312
+ ];
313
+ const TOOLS_OPTIONS = [
314
+ { name: "--name", value: "TOOL", summary: "list only: show one tool, with its input schema under --json." },
315
+ { name: "--args", value: "JSON", summary: "call only: the tool's arguments as one JSON object. Default {}." },
316
+ { name: "--args-file", value: "PATH", summary: "call only: a file holding the arguments as one JSON object, up to 1 MiB." },
317
+ {
318
+ name: "--yes",
319
+ value: null,
320
+ summary:
321
+ "call only: run a tool that writes, starts work or calls a model provider. Pass it only after the person " +
322
+ "has approved this exact call.",
323
+ },
324
+ ...READ_BASE_OPTIONS,
325
+ JSON_OPTION,
326
+ ];
235
327
  const DAYS_OPTION = {
236
328
  name: "--days",
237
329
  value: "N",
@@ -259,21 +351,23 @@ const limitOption = (fallback, extra = "") => ({
259
351
  summary: `Rows to return, 1 to ${READ_MAX_LIMIT}. Default ${fallback}.${extra}`,
260
352
  });
261
353
  const READ_BASE_USAGE =
262
- "[--url ORIGIN] [--workspace UUID] [--agent-token-file PATH] [--project DIR] [--config-dir DIR] [--timeout-ms N] [--json]";
354
+ "[--url ORIGIN] [--workspace SLUG] [--agent-token-file PATH] [--project DIR] [--config-dir DIR] [--timeout-ms N] [--json]";
263
355
  const REFUSED_NOTE =
264
356
  "--environment, --workload, --since, --until, --sql, --query, --content, --include-content, --debug and --replay " +
265
357
  "are recognized and refused with exit code 6 before any request, except --environment, --since and --until on usage.";
266
358
 
267
359
  // The read commands. Each uses the project's saved Metadata grant or a
268
- // coding-agent key with Metadata access, makes GET requests to fixed paths on
269
- // the verified origin and never opens a browser.
360
+ // coding-agent key with the Metadata or Debug role, makes GET requests to fixed
361
+ // paths on the verified origin and never opens a browser.
270
362
  export const READ_HELP = Object.freeze([
271
363
  {
272
364
  name: "status",
273
365
  usage: `metergraph status ${READ_BASE_USAGE}`,
274
366
  summary:
275
367
  "Show whether this project is configured, the service is reachable and reports the bound deployment " +
276
- "profile, and the grant is verified for the bound workspace, with its Metadata capabilities. " +
368
+ "profile, and the grant is verified for the bound workspace, with its Metadata capabilities. Reports " +
369
+ "the credential's role (Metadata or Debug) and its workflow permissions (Allow evaluation writes, Allow " +
370
+ "evaluation previews, Allow analysis starts) in data.access, and how to grant them. " +
277
371
  "Never claims application traffic is verified.",
278
372
  options: [...READ_BASE_OPTIONS, JSON_OPTION],
279
373
  },
@@ -417,13 +511,14 @@ function optionLines(options, minimum) {
417
511
  return options.map((option, index) => ` ${flags[index].padEnd(width)}${option.summary}`);
418
512
  }
419
513
 
420
- export function helpData(topic) {
421
- return {
514
+ export function helpData(topic, action = null) {
515
+ const data = {
422
516
  topic,
423
517
  usage: [
424
518
  "metergraph --help [--json]",
425
519
  "metergraph --version [--json]",
426
520
  "metergraph doctor [--url ORIGIN] [--timeout-ms N] [--json]",
521
+ "metergraph pricing models|lookup|calculate|cost|history|market [options] [--json]",
427
522
  ...SKILL_USAGE,
428
523
  ...SKILLS_USAGE,
429
524
  LOGIN_USAGE,
@@ -431,8 +526,13 @@ export function helpData(topic) {
431
526
  SETUP_USAGE,
432
527
  VERIFY_USAGE,
433
528
  ...READ_HELP.map((entry) => entry.usage),
529
+ ...EXPERIMENTS_USAGE,
530
+ ...TOOLS_USAGE,
434
531
  ],
435
532
  commands: [
533
+ { name: "pricing", summary: "Public model discovery, lookup and calculation. Current market board; historical charts in local reference mode.",
534
+ options: [...PRICING_OPTIONS.map(name => ({ name, value: "VALUE", summary: "See metergraph help pricing and docs/pricing.md." })),
535
+ { name: "--batch", value: null, summary: "Use batch pricing for cost." }, JSON_OPTION] },
436
536
  {
437
537
  name: "doctor",
438
538
  summary:
@@ -455,7 +555,7 @@ export function helpData(topic) {
455
555
  {
456
556
  name: "skills install",
457
557
  summary:
458
- "Copy the Metergraph workflow skills bundled with this CLI, such as the model-swap loop, into one " +
558
+ "Copy the Metergraph workflow skills bundled with this CLI, such as model swap, into one " +
459
559
  "client's project skill directory. Never replaces a skill it did not install. No network requests, no sign in.",
460
560
  options: SKILL_OPTIONS,
461
561
  },
@@ -488,13 +588,13 @@ export function helpData(topic) {
488
588
  name: "setup",
489
589
  summary: "Guide browser sign in and workspace choice, approve an ingest-only key, and install the selected client skill. Does not verify application traffic.",
490
590
  options: [
491
- { name: "--runtime", value: "RUNTIME", summary: "Required. local only; other runtimes get a handoff." },
591
+ { name: "--runtime", value: "RUNTIME", summary: "Local terminal: inferred automatically. Scripts: required. Other runtimes get a handoff." },
492
592
  { name: "--url", value: "ORIGIN", summary: "Deployment origin. Defaults to an existing project binding, else the hosted origin." },
493
593
  { name: "--workspace", value: "UUID", summary: "Expected workspace; the browser must approve this exact workspace." },
494
594
  { name: "--project", value: "DIR", summary: "Project directory. Default: current directory." },
495
595
  CONFIG_DIR_OPTION,
496
596
  { name: "--env-file", value: "FILE", summary: "Project-relative env file. Default: .env." },
497
- { name: "--client", value: "CLIENT", summary: "Install the bundled skill for codex, claude or cursor." },
597
+ { name: "--client", value: "CLIENT", summary: "codex, claude or cursor. A local interactive setup asks when omitted." },
498
598
  { name: "--skip-skill", value: null, summary: "Explicitly leave client skill installation pending." },
499
599
  { name: "--timeout-ms", value: "N", summary: "Time to wait for browser approval." },
500
600
  { name: "--signup", value: null, summary: "Start at hosted sign up when the project needs login." },
@@ -514,6 +614,21 @@ export function helpData(topic) {
514
614
  summary: "Poll Metadata for one exact trace in an explicit invocation window. Never sends application traffic.",
515
615
  options: VERIFY_OPTIONS,
516
616
  },
617
+ {
618
+ name: "experiments",
619
+ summary:
620
+ "Estimate, start, list and read experiments: a workload's recent production calls replayed on 1 to 3 " +
621
+ "treatments, with built-in statistics per arm next to production. list, estimate and get are Metadata " +
622
+ "reads; start needs a key with Debug access and Allow analysis starts and spends within the server's budget.",
623
+ options: EXPERIMENTS_OPTIONS,
624
+ },
625
+ {
626
+ name: "tools",
627
+ summary:
628
+ "List the Agent Access tools this credential can use and call one, as an MCP client would. A tool that " +
629
+ "writes, starts work or calls a model provider runs only with --yes.",
630
+ options: TOOLS_OPTIONS,
631
+ },
517
632
  ...READ_HELP.map(({ name, summary, options, examples }) => ({
518
633
  name,
519
634
  summary,
@@ -531,10 +646,69 @@ export function helpData(topic) {
531
646
  meaning: EXIT_CODE_MEANINGS[outcome],
532
647
  })),
533
648
  };
649
+ if (action) data.action = { name: action, ...actionHelpData(topic, action, data.commands) };
650
+ return data;
534
651
  }
535
652
 
536
- export function helpText(topic, style = PLAIN) {
653
+ export function helpText(topic, style = PLAIN, action = null, advanced = false) {
537
654
  const lines = [];
655
+ if (action) {
656
+ const entry = actionHelpData(topic, action, helpData(topic).commands);
657
+ if (entry) return formatHelp([
658
+ `Usage: ${entry.usage}`, "", entry.summary, "", "Examples:",
659
+ ...entry.examples.map(example => ` ${example}`), "", "Options:",
660
+ ...optionLines(displayOptions(entry.options, topic, action, advanced), 20), "", entry.note, "",
661
+ ...(advanced ? [] : [`Connection settings: metergraph ${topic} ${action} --help --advanced`]),
662
+ ], style);
663
+ }
664
+ if (topic === "exit-codes") {
665
+ lines.push("Exit codes:", "", "For scripts. Human output describes the result and what to do next.", "");
666
+ for (const [outcome, code] of Object.entries(EXIT_CODES)) {
667
+ lines.push(` ${String(code).padEnd(4)}${outcome}`, ...paragraph(style, EXIT_CODE_MEANINGS[outcome], " "), "");
668
+ }
669
+ return formatHelp(lines, style);
670
+ }
671
+ if (topic === "agents") return formatHelp([
672
+ "Connect a coding agent", "", ...AGENT_PATH_HELP, "", ...AGENT_APPROVAL_HELP,
673
+ "", 'Use --json for structured results. Run "metergraph help exit-codes" for scripting outcomes.',
674
+ ], style);
675
+ if (topic === "pricing") {
676
+ return formatHelp(`Usage: metergraph pricing models|lookup|calculate|cost|history|market [options] [--json]
677
+
678
+ Public catalog prices. No account, project or token is required.
679
+ models [--model ID] [--provider ID | --channel ID] [--region ID] [--limit 1..100]
680
+ lookup --model ID [--provider ID | --channel ID] [--region ID] [--at YYYY-MM-DD]
681
+ calculate (or cost) Same selectors; --input-tokens N --output-tokens N [--batch]
682
+ --cache-read-tokens N; --trace-format anthropic.messages|openai.responses|
683
+ openai.chat_completions|otel.gen_ai. Native format inferred for direct
684
+ Anthropic/OpenAI; otherwise otel.gen_ai. Only supplied counts are sent.
685
+ Anthropic also accepts --cache-write-tokens N or
686
+ --cache-write-5m-tokens N / --cache-write-1h-tokens N.
687
+ market --channel ID [--region ID] [--limit 1..100] (current rates, no history)
688
+ history --model ID --channel ID [--region ID] [--format table|chart|html]
689
+ Requires --api reference --url LOOPBACK_ORIGIN; no public history API.
690
+
691
+ All actions: --url ORIGIN --timeout-ms 100..30000 --json --api public|reference
692
+ Default API: public; default origin: https://app.metergraph.dev.
693
+ --at selects UTC midnight; without it public lookup/calculation use server time.
694
+ Models and public market discovery are current-only. Filters on models run locally.
695
+ History/market region defaults to global. Omitted input/output counts stay unknown;
696
+ partial costs are known subtotals, not complete costs. OpenAI cache counts must
697
+ not exceed total input. --json cannot be combined with --format.
698
+
699
+ Examples:
700
+ metergraph pricing models --limit 10
701
+ metergraph pricing lookup --model gpt-5 --provider openai --region global --json
702
+ metergraph pricing calculate --model claude-sonnet-5 --provider anthropic --input-tokens 1000 --cache-read-tokens 800 --output-tokens 200
703
+ metergraph pricing market --channel openai-api --limit 5
704
+ metergraph pricing history --model openai/gpt-5.6-terra --channel openai-api --api reference --url http://127.0.0.1:8765 --format html > history.html
705
+
706
+ Public API: GET /v1/pricing/models; POST /v1/pricing/lookup and /calculate.
707
+ No credentials, traces, prompts or responses are loaded from project files.
708
+ The CLI constructs metadata-only provider-format traces from the supplied counts.
709
+ See docs/pricing.md for the supported contract, limitations and reference previews.
710
+ `.split("\n"), style);
711
+ }
538
712
  if (topic === "doctor") {
539
713
  lines.push(
540
714
  "Usage: metergraph doctor [--url ORIGIN] [--timeout-ms N] [--json]",
@@ -560,6 +734,8 @@ export function helpText(topic, style = PLAIN) {
560
734
  "--skip-skill is explicit. Non-hosted setup requires an explicit origin, workspace and",
561
735
  "operator prerequisite attestation; OSS remains an operator handoff. No application",
562
736
  "traffic is claimed until an exact instrumented invocation is separately verified.",
737
+ "", "At a terminal without --json, setup asks for any of these it needs that are",
738
+ "missing, prints the full command, then runs it. Scripts and agents get no questions.",
563
739
  "", "Options:");
564
740
  lines.push(...optionLines(helpData(null).commands.find((entry) => entry.name === "setup").options, 25));
565
741
  lines.push("", ...AGENT_APPROVAL_HELP);
@@ -570,15 +746,18 @@ export function helpText(topic, style = PLAIN) {
570
746
  lines.push(
571
747
  "Opens your browser on the service's own sign in and workspace consent pages, then",
572
748
  "binds this project to the workspace you approve. The CLI receives a delegated grant",
573
- `limited to the Metadata scope (${METADATA_SCOPE}) and verifies the workspace, profile`,
574
- "and scope with the service before saving anything. Your browser keeps its own sign in.",
749
+ `limited to the Metadata role (${METADATA_SCOPE}), or with --role debug to the Debug role and`,
750
+ "the workflow permissions the --allow-* options name, which only a workspace owner approves.",
751
+ "It verifies the workspace, profile and scope with the service before saving anything. Your",
752
+ "browser keeps its own sign in. Signing in again with another role or other permissions",
753
+ "replaces the grant.",
575
754
  "The grant is saved in your private config directory; the project gets only",
576
755
  ".metergraph/project.json, which holds no credentials.",
577
756
  "",
578
757
  "Login does not create an application ingest key, and no manual API key is required.",
579
- "The service records the grant as a Metadata-only OAuth connection, separate from any",
580
- "API or ingest key you manage. Login reads no telemetry or content and never asks for",
581
- "Debug or Replay access. Rerunning it on a signed in project changes nothing.",
758
+ "The service records the grant as an OAuth connection, separate from any API or ingest",
759
+ "key you manage. Login reads no telemetry or content and never asks for Replay access.",
760
+ "Rerunning it on a signed in project with the same role and permissions changes nothing.",
582
761
  "",
583
762
  );
584
763
  } else {
@@ -599,6 +778,52 @@ export function helpText(topic, style = PLAIN) {
599
778
  "Opening requires a server link that selects the verified workspace; older links get a manual handoff.",
600
779
  "", "Options:");
601
780
  lines.push(...optionLines(VERIFY_OPTIONS, 22));
781
+ } else if (topic === "tools") {
782
+ lines.push(
783
+ "Usage:",
784
+ ...TOOLS_USAGE.map((usage) => ` ${usage}`),
785
+ "",
786
+ "Every Agent Access tool the service offers this credential, through its MCP endpoint, with the same",
787
+ "saved sign in or coding-agent key as the read commands and the same checks first. The service decides",
788
+ "which tools a credential sees: a Metadata credential gets the Metadata tools; the Debug role adds trace",
789
+ "content and evaluation reads, and Allow evaluation writes, Allow evaluation previews and Allow analysis",
790
+ "starts add the tools they name. Replay keys are refused.",
791
+ "",
792
+ "list prints the tools with whether each only reads. Under --json each carries its title, description",
793
+ "and input schema. call sends one tools/call with --args or --args-file and prints the tool's structured",
794
+ "result in data.result. A tool the service does not mark read-only, or marks as calling a model provider,",
795
+ "needs --yes: without it the command exits 18 (action_required, confirmation_required) and sends",
796
+ "nothing. Pass --yes only after the person has approved that exact call. A refusal by the tool exits 19",
797
+ "(refused) with the service's error in data.tool_error; a write whose answer was lost exits 20",
798
+ "(unconfirmed).",
799
+ "",
800
+ "Options:",
801
+ );
802
+ lines.push(...optionLines(TOOLS_OPTIONS, 22));
803
+ } else if (topic === "experiments") {
804
+ lines.push(
805
+ "Usage:",
806
+ ...EXPERIMENTS_USAGE.map((usage) => ` ${usage}`),
807
+ "",
808
+ "An experiment replays a workload's last 10 to 40 replayable production calls on 1 to 3 treatments",
809
+ "(a provider, model and settings) and compares each with what production returned on the same calls:",
810
+ "cost per call, p50 latency and output tokens, output length, JSON parse rate, same top-level keys,",
811
+ "empty or refused and cut-off rates. The statistics show differences; they do not judge them.",
812
+ "",
813
+ "estimate prices it against the server's budget and daily cap and writes nothing. start takes the",
814
+ "same arguments, so it starts exactly what was priced; poll it with get until it is terminal.",
815
+ "Each start creates a new experiment, so a start that succeeded is never retried.",
816
+ "",
817
+ "list, estimate and get use this project's saved sign in or a coding-agent key, as the read",
818
+ "commands do, and never read inputs, outputs or tracked field values; those are in the dashboard.",
819
+ "start needs a coding-agent key with the Debug role and Allow analysis starts, which only a",
820
+ "workspace owner can grant, in --agent-token-file or METERGRAPH_AGENT_TOKEN.",
821
+ "The CLI checks the key before it sends the one start request. The service then replays the",
822
+ "sampled calls' captured prompts on each treatment's provider within its budget.",
823
+ "",
824
+ "Options:",
825
+ );
826
+ lines.push(...optionLines(EXPERIMENTS_OPTIONS, 22));
602
827
  } else if (READ_HELP.some((entry) => entry.name === topic)) {
603
828
  const entry = READ_HELP.find((candidate) => candidate.name === topic);
604
829
  lines.push(
@@ -607,10 +832,10 @@ export function helpText(topic, style = PLAIN) {
607
832
  entry.summary,
608
833
  "",
609
834
  "Uses this project's saved Metadata grant (see \"metergraph help login\"), or, where there is none on",
610
- "this machine, a coding-agent key with Metadata access from METERGRAPH_AGENT_TOKEN or",
611
- "--agent-token-file. A key is sent only after the origin answers as a supported Metergraph service,",
612
- "and nothing is read until the service shows exactly the Metadata scope, no content access and the",
613
- "expected workspace. Sends GET requests only, to fixed paths on that origin, follows no redirects and",
835
+ "this machine, a coding-agent key from METERGRAPH_AGENT_TOKEN or --agent-token-file, with the",
836
+ "Metadata role or the Debug role. A key is sent only after the origin answers as a supported",
837
+ "Metergraph service, and nothing is read until the service shows one of those roles (never",
838
+ "Replay) and the expected workspace. Sends GET requests only, to fixed paths on that origin, follows no redirects and",
614
839
  "reads bounded responses. Never opens a browser, signs in, writes project files or the key, reads",
615
840
  "retained content, replays or calls a model provider. It may refresh its own saved grant, and the",
616
841
  "service may record the access (for example last used times); it never changes workspace",
@@ -656,7 +881,7 @@ export function helpText(topic, style = PLAIN) {
656
881
  "Copies the Metergraph workflow skills bundled with this CLI into one client's project",
657
882
  "skill directory, one folder per skill, and records ownership of each in",
658
883
  ".metergraph/skills/NAME.json. The skills come from https://github.com/metergraph/skills",
659
- "at the commit this release pins. They cover investigation and the model-swap loop:",
884
+ "at the commit this release pins. They cover investigation and model swap:",
660
885
  "choose a workload, choose traces and models, define the eval, run the analysis,",
661
886
  "summarize the report and rerun. The setup skill is installed by \"metergraph skill\".",
662
887
  "",
@@ -680,42 +905,110 @@ export function helpText(topic, style = PLAIN) {
680
905
  "Discovery stays pending until the client itself loads the skills.",
681
906
  );
682
907
  } else {
683
- lines.push(
684
- `metergraph ${VERSION} (preview)`,
685
- "",
686
- "Usage:",
687
- " metergraph --help [--json] Show this help",
688
- " metergraph --version [--json] Show the CLI version",
689
- " metergraph doctor [options] Check a Metergraph service, read only",
690
- " metergraph skill install|update Install or update the agent skill in a project",
691
- " metergraph skills install|update|list Workflow skills, such as the model-swap loop",
692
- " metergraph login [options] Sign in and bind a project to a workspace",
693
- " metergraph logout [options] Revoke and remove a project's sign in",
694
- " metergraph setup [options] Approve and write a private ingest key",
695
- " metergraph verify [options] Find one exact processed trace",
696
- " metergraph status [options] Show configured, reachable and verified state",
697
- " metergraph context [options] Show the verified workspace",
698
- " metergraph capabilities [opts] Show the agent reads offered to this grant",
699
- " metergraph usage [options] Daily usage per route, Metadata only",
700
- " metergraph routes [options] Routes and evaluation contract versions",
701
- " metergraph traces [options] One page of trace metadata",
702
- "",
703
- 'Run "metergraph help doctor" for doctor options.',
704
- 'Run "metergraph help skill" for skill options.',
705
- 'Run "metergraph help skills" for workflow skill options.',
706
- 'Run "metergraph help login" or "metergraph help logout" for sign in options.',
707
- 'Run "metergraph help verify" for exact-trace verification options.',
708
- 'Run "metergraph help COMMAND" for status, context, capabilities, usage, routes or traces.',
709
- "",
710
- ...AGENT_PATH_HELP,
711
- );
908
+ return rootHelp(style);
712
909
  }
713
- lines.push("", "Exit codes:");
714
- for (const [outcome, code] of Object.entries(EXIT_CODES)) {
715
- lines.push(` ${String(code).padEnd(3)}${outcome.padEnd(25)}${style.dim(EXIT_CODE_MEANINGS[outcome])}`);
910
+ // Reference material is opt-in; everyday help stays focused on this command.
911
+ return commandHelp(topic, lines, style, advanced);
912
+ }
913
+
914
+ function commandHelp(topic, lines, style, advanced) {
915
+ const optionsAt = lines.indexOf("Options:");
916
+ if (optionsAt < 0) return formatHelp(lines, style);
917
+ const optionsEnd = lines.indexOf("", optionsAt + 1);
918
+ const usageEnd = lines.indexOf("");
919
+ const examples = {
920
+ setup: ["metergraph setup", "metergraph setup --runtime local --client codex --json --no-browser"],
921
+ login: ["metergraph login", "metergraph login --runtime local --no-browser"],
922
+ logout: ["metergraph logout"], doctor: ["metergraph doctor"],
923
+ verify: ["metergraph verify --trace-id TRACE_ID --since START_ISO_TIME --until END_ISO_TIME"],
924
+ status: ["metergraph status"], context: ["metergraph context"], capabilities: ["metergraph capabilities"],
925
+ usage: ["metergraph usage --days 7"], routes: ["metergraph routes"], traces: ["metergraph traces --limit 10"],
926
+ skill: ["metergraph skill install --client codex"], skills: ["metergraph skills list"],
927
+ experiments: ["metergraph experiments list", "metergraph experiments estimate --help"],
928
+ }[topic] ?? [];
929
+ const notes = lines.slice(usageEnd + 1, optionsAt);
930
+ const command = helpData(topic).commands.find(entry => entry.name === topic);
931
+ const options = command ? ["Options:", ...optionLines(displayOptions(command.options, topic, null, advanced), 22)]
932
+ : lines.slice(optionsAt, optionsEnd < 0 ? lines.length : optionsEnd);
933
+ const synopsis = {
934
+ setup: "metergraph setup [--client CLIENT | --skip-skill] [options]",
935
+ login: "metergraph login [--runtime local] [options]",
936
+ logout: "metergraph logout [options]",
937
+ verify: "metergraph verify (--trace-id ID | --request-id ID) --since TIME --until TIME [options]",
938
+ ...Object.fromEntries(READ_HELP.map(entry => [entry.name, `metergraph ${entry.name} [options]`])),
939
+ }[topic];
940
+ return formatHelp([
941
+ ...(synopsis ? [`Usage: ${synopsis}`] : lines.slice(0, usageEnd)), "", "Examples:", ...examples.map(example => ` ${example}`),
942
+ ...(topic === "setup" || topic === "login" ? ["", "Local terminals guide missing choices. Scripts require explicit flags.",
943
+ "With --json --no-browser, a step that needs browser approval exits 18 (action_required) with the URL in " +
944
+ "data.next_action.url; approve it, then run the same command again."] : []),
945
+ "", ...options,
946
+ ...(advanced ? ["", "Details", ...notes, ...lines.slice(optionsEnd < 0 ? lines.length : optionsEnd)]
947
+ : ["", `Connection settings and details: metergraph ${topic} --help --advanced`]),
948
+ ], style);
949
+ }
950
+
951
+ function rootHelp(style) {
952
+ const groups = [
953
+ ["Get started", [
954
+ ["setup", "Connect your app and install an agent skill"],
955
+ ["login", "Sign in to a workspace"],
956
+ ["status", "Check this project's connection"],
957
+ ]],
958
+ ["Explore", [
959
+ ["usage", "View costs and token usage"],
960
+ ["traces", "Browse trace metadata"],
961
+ ["routes", "List routes and evaluation contracts"],
962
+ ["experiments", "Estimate and run model comparisons"],
963
+ ["tools", "List and call the tools your coding agent can use"],
964
+ ["pricing", "Look up public model prices; no account needed"],
965
+ ]],
966
+ ["Manage", [
967
+ ["verify", "Verify one processed trace"],
968
+ ["doctor", "Check service health"],
969
+ ["context", "Show the connected workspace"],
970
+ ["capabilities", "Show available workspace reads"],
971
+ ["skill", "Install or update the setup skill"],
972
+ ["skills", "Install, update or list workflow skills"],
973
+ ["logout", "Sign out of this project"],
974
+ ]],
975
+ ];
976
+ const lines = [style.bold("Metergraph") + " " + style.dim(`${VERSION} · preview`),
977
+ "Understand your AI usage and compare models.", "", "Usage: metergraph <command> [options]", ""];
978
+ for (const [title, commands] of groups) {
979
+ lines.push(section(style, title), ...fields(style, commands), "");
716
980
  }
717
- // Section labels ("Usage:", "Options:", "Exit codes:") in bold.
718
- return `${lines.map((line) => line.replace(/^(Usage|Options|Exit codes):/, (label) => style.bold(label))).join("\n")}\n`;
981
+ lines.push(section(style, "Try it"),
982
+ " metergraph setup --runtime local --client codex",
983
+ " metergraph pricing models --limit 10", "",
984
+ 'Run "metergraph <command> --help" for options and examples.',
985
+ 'Use --json for scripts; --version to show the version.',
986
+ 'Scripting reference: metergraph help exit-codes.',
987
+ 'Connecting a cloud agent? Run "metergraph help agents".');
988
+ return formatHelp(lines, style);
989
+ }
990
+
991
+ function formatHelp(lines, style) {
992
+ return render(lines.flatMap((line) => {
993
+ if (!line) return [""];
994
+ if (line.length <= style.width && !/^(Usage|Options|Examples|Exit codes):/.test(line)) return [line];
995
+ if (/^(Usage|Options|Examples|Exit codes):/.test(line)) {
996
+ const [first, ...rest] = paragraph(style, line, "");
997
+ return [style.bold(first), ...rest];
998
+ }
999
+ // Preserve option and command columns, with a hanging indent on small terminals.
1000
+ const row = /^( \S.*?) {2,}(\S.*)$/.exec(line);
1001
+ if (row && Number.isFinite(style.width)) {
1002
+ const prefix = row[1] + " ";
1003
+ if (prefix.length > style.width / 2) {
1004
+ return [row[1], ...paragraph(style, row[2], " ")];
1005
+ }
1006
+ const wrapped = paragraph(style, row[2], " ".repeat(prefix.length));
1007
+ return [prefix + wrapped[0].trimStart(), ...wrapped.slice(1)];
1008
+ }
1009
+ return paragraph(style, line.trimStart(), line.match(/^ */)[0]);
1010
+ }));
1011
+
719
1012
  }
720
1013
 
721
1014
  export function skillText(result, message, style = PLAIN) {
@@ -876,6 +1169,8 @@ export function doctorText(result, style = PLAIN) {
876
1169
  // Fixed text for every login, logout and session reason. Reasons shared
877
1170
  // with doctor fall back to its wording.
878
1171
  const AUTH_MESSAGES = {
1172
+ local_docker_transport_required:
1173
+ "Local credentials and consent require a verified Docker transport. This CLI has no configured Docker transport; nothing was sent. Update the local bundle and use its Docker-mediated bin/agent-key command, or connect this CLI to an authenticated HTTPS deployment.",
879
1174
  runtime_not_supported:
880
1175
  "--runtime says where this terminal runs, not which Metergraph service you sign in to. " +
881
1176
  "A cloud runtime is a remote sandbox, and sign in needs a browser on the same machine as the CLI, " +
@@ -910,6 +1205,7 @@ const AUTH_MESSAGES = {
910
1205
  oauth_metadata_missing: "This service does not support CLI sign in yet. Nothing was written.",
911
1206
  metadata_scope_unsupported:
912
1207
  "This service does not offer Metadata-only access for the CLI yet. The CLI does not fall back to broader access.",
1208
+ debug_scope_unsupported: "This service does not offer Debug access for CLI sign in. Nothing was saved.",
913
1209
  endpoint_not_allowed: "The service advertised an authorization endpoint outside its own origin or known paths.",
914
1210
  resource_mismatch: "The service's protected resource does not match this origin.",
915
1211
  issuer_mismatch: "The service's authorization issuer does not match this origin.",
@@ -926,6 +1222,14 @@ const AUTH_MESSAGES = {
926
1222
  client_mismatch: "The issued grant is for a different client. Nothing was saved.",
927
1223
  scope_mismatch: "The issued grant is not limited to exactly the Metadata scope. It was not kept.",
928
1224
  workspace_mismatch: "The browser granted a different workspace than --workspace. It was not kept.",
1225
+ workspace_hint_ambiguous:
1226
+ "More than one of your workspaces has the name --workspace gave, so the consent page offered no approval. " +
1227
+ "Nothing was granted.",
1228
+ workspace_hint_unknown:
1229
+ "None of your workspaces has the slug or name --workspace gave, so the consent page offered no approval. " +
1230
+ "Nothing was granted.",
1231
+ workspace_name_unsupported:
1232
+ "This service cannot match a workspace by name. Use its slug, which its Settings page shows. Nothing was sent.",
929
1233
  workspace_context_mismatch: "The service reported a different workspace than the grant names. It was not kept.",
930
1234
  profile_mismatch: "The service reported a different deployment profile than before sign in. It was not kept.",
931
1235
  deployment_profile_mismatch:
@@ -936,6 +1240,8 @@ const AUTH_MESSAGES = {
936
1240
  "The grant would allow content, replay or provider access, which login never accepts. It was not kept.",
937
1241
  access_rejected: "The service refused the new grant. Nothing was saved.",
938
1242
  invalid_project: "--project must name an existing directory.",
1243
+ secure_writes_unavailable:
1244
+ "Secure automatic file writes are unavailable on this platform or the bundled filesystem helper failed verification. Nothing was written.",
939
1245
  invalid_config_dir: "METERGRAPH_CONFIG_DIR must be an absolute path.",
940
1246
  unsafe_path: "A path in .metergraph is a symbolic link or is not a regular file or directory. Nothing was changed.",
941
1247
  binding_invalid: "The project binding .metergraph/project.json is not valid. Nothing was changed.",
@@ -1012,7 +1318,7 @@ export function authText(result, message, style = PLAIN) {
1012
1318
  const lines = [headline(style, toneFor(result.outcome), title, result.outcome), ""];
1013
1319
  const rows = [
1014
1320
  ["Origin", report.origin],
1015
- ["Workspace", report.workspace?.id ?? null],
1321
+ ["Workspace", workspaceShown(report.workspace)],
1016
1322
  ["Profile", report.deployment_profile || null],
1017
1323
  ];
1018
1324
  if (login && result.ok) {