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/cli.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { runPricing } from "./pricing.js";
2
+ import { pricingText, pricingHtml } from "./pricing-output.js";
1
3
  import { READ_COMMANDS, parseArgs } from "./args.js";
2
4
  import { runLogin, runLogout } from "./auth-login.js";
3
5
  import { runDoctor } from "./doctor.js";
@@ -15,10 +17,23 @@ import {
15
17
  versionData,
16
18
  } from "./output.js";
17
19
  import { runRead } from "./read.js";
20
+ import { runExperimentStart } from "./experiments.js";
21
+ import { experimentsMessage, experimentsText } from "./experiments-output.js";
22
+ import { toolsMessage, toolsText } from "./tools-output.js";
18
23
  import { readMessage, readText } from "./read-output.js";
19
24
  import { listSkillPack, runSkill, runSkillPack } from "./skill.js";
20
25
  import { runSetup } from "./setup.js";
21
26
  import { containsKnownCredential, preflightNonHostedSetup, prerequisiteGuide, resumeNonHostedRoute } from "./setup-deployment.js";
27
+ import {
28
+ ASKABLE,
29
+ agentAccessOffer,
30
+ canAsk,
31
+ commandLine,
32
+ fillSetupArgv,
33
+ fillLoginArgv,
34
+ offerAgentAccess,
35
+ terminalPrompter,
36
+ } from "./setup-interactive.js";
22
37
  import { VERSION } from "./constants.js";
23
38
  import { createStyle, fields, headline, paragraph, render, steps, toneFor } from "./style.js";
24
39
  import { runVerify } from "./verify.js";
@@ -26,12 +41,13 @@ import { verifyMessage, verifyText } from "./verify-output.js";
26
41
 
27
42
  // Runs one command and resolves with the exit code. With --json, stdout gets
28
43
  // exactly one JSON line and stderr stays empty. Without it, results go to
29
- // stdout and usage or internal errors go to stderr. Never reads stdin.
30
- export async function main(argv, { stdout, stderr }) {
44
+ // stdout and usage or internal errors go to stderr. Only setup reads stdin,
45
+ // and only to ask a person at a terminal (see setup-interactive.js).
46
+ export async function main(argv, { stdout, stderr, stdin = null }) {
31
47
  const json = argv.includes("--json");
32
48
  let result;
33
49
  try {
34
- result = await run(argv, { stdout, stderr });
50
+ result = await run(argv, { stdout, stderr, stdin });
35
51
  } catch {
36
52
  result = envelope({
37
53
  command: null,
@@ -45,9 +61,38 @@ export async function main(argv, { stdout, stderr }) {
45
61
  return result.exit_code;
46
62
  }
47
63
 
48
- async function run(argv, { stdout, stderr }) {
49
- const parsed = parseArgs(argv);
64
+ async function run(argv, { stdout, stderr, stdin }) {
65
+ let parsed = parseArgs(argv);
50
66
  const style = createStyle(stdout);
67
+ const say = (...lines) => stdout.write(lines.map((line) => `${line}\n`).join(""));
68
+ const asking = canAsk(argv, { stdin, stdout, stderr });
69
+
70
+ if (asking && parsed.command === "login") {
71
+ argv = fillLoginArgv(argv);
72
+ parsed = parseArgs(argv);
73
+ }
74
+
75
+ // A person at a terminal is asked for what setup is missing; the answers
76
+ // become flags and go through the same parsing as a script's.
77
+ if (parsed.command === "setup" && !parsed.ok && ASKABLE.has(parsed.code) && asking) {
78
+ const prompter = terminalPrompter(stdin, stdout);
79
+ let filled;
80
+ try {
81
+ filled = await fillSetupArgv(argv, { ask: prompter.ask, say });
82
+ } finally {
83
+ prompter.close();
84
+ }
85
+ if (filled === null) {
86
+ stderr.write("Setup was cancelled. Nothing was changed.\n");
87
+ return envelope({ command: "setup", outcome: "cancelled", reason: "questions_cancelled",
88
+ message: "Setup was cancelled. Nothing was changed." });
89
+ }
90
+ argv = filled;
91
+ parsed = parseArgs(argv);
92
+ // Only a command that parses is shown: a value the parser refuses may be
93
+ // a mistyped credential, and its error never echoes input.
94
+ if (parsed.ok) say("", `Running: ${commandLine(filled)}`, "");
95
+ }
51
96
 
52
97
  if (!parsed.ok) {
53
98
  const result = envelope({
@@ -57,13 +102,20 @@ async function run(argv, { stdout, stderr }) {
57
102
  message: parsed.message,
58
103
  });
59
104
  if (parsed.json) stdout.write(toJsonLine(result));
60
- else stderr.write(`${createStyle(stderr).red("Error:")} ${parsed.message}\n`);
105
+ else {
106
+ const errorStyle = createStyle(stderr);
107
+ const [first, ...rest] = paragraph(errorStyle, `Error: ${parsed.message}`, "");
108
+ const help = parsed.command && parsed.command !== "help"
109
+ ? `metergraph ${parsed.command} --help` : "metergraph --help";
110
+ stderr.write(render([first.replace(/^Error:/, errorStyle.red("Error:")), ...rest,
111
+ "", ...steps(errorStyle, [["Help", help]])]));
112
+ }
61
113
  return result;
62
114
  }
63
115
 
64
116
  if (parsed.command === "help") {
65
- const result = envelope({ command: "help", outcome: "ok", data: helpData(parsed.topic) });
66
- stdout.write(parsed.json ? toJsonLine(result) : helpText(parsed.topic, style));
117
+ const result = envelope({ command: "help", outcome: "ok", data: helpData(parsed.topic, parsed.action) });
118
+ stdout.write(parsed.json ? toJsonLine(result) : helpText(parsed.topic, style, parsed.action, parsed.advanced));
67
119
  return result;
68
120
  }
69
121
 
@@ -73,6 +125,14 @@ async function run(argv, { stdout, stderr }) {
73
125
  return result;
74
126
  }
75
127
 
128
+ if (parsed.command === "pricing") {
129
+ const response = await runPricing(parsed);
130
+ const result = envelope({ command: `pricing ${parsed.action}`, ...response });
131
+ stdout.write(parsed.json ? toJsonLine(result) :
132
+ parsed.format === "html" && result.ok ? pricingHtml(result.data) : pricingText(result, parsed.format, style));
133
+ return result;
134
+ }
135
+
76
136
  if (parsed.command === "login" || parsed.command === "logout") {
77
137
  // Human progress goes to stderr, never with --json, so stdout carries
78
138
  // only the final result.
@@ -106,24 +166,36 @@ async function run(argv, { stdout, stderr }) {
106
166
  ? await runSetup({ ...options, expectedProfile: routed.profile ?? null, announce: pending.announce }, progress)
107
167
  : routed;
108
168
  if (pending.result) return pending.result;
109
- const { outcome, reason, data } = response;
169
+ const { outcome, reason } = response;
170
+ // The customer-local bundle can give the agent more than Metadata
171
+ // without a sign in; say how.
172
+ const offer = outcome === "ok" && routed.profile === "local";
173
+ const data = offer ? { ...response.data, agent_access: agentAccessOffer(options.origin) } : response.data;
110
174
  let result = envelope({ command: "setup", outcome, reason, data,
111
175
  message: outcome === "ok" ? null : "Setup did not complete. Review the reason and retry safely." });
112
176
  if (containsKnownCredential(result, routed)) {
113
177
  result = envelope({ command: "setup", outcome: "unsupported", reason: "credential_echo" });
114
178
  }
115
- const human = setupText(result, style);
179
+ const human = setupText(result, style, { asking });
116
180
  if (containsKnownCredential(human, routed)) {
117
181
  result = envelope({ command: "setup", outcome: "unsupported", reason: "credential_echo" });
118
182
  stdout.write(parsed.json ? toJsonLine(result) : "Setup stopped.\n");
119
- } else {
120
- stdout.write(parsed.json ? toJsonLine(result) : human);
183
+ return result;
184
+ }
185
+ stdout.write(parsed.json ? toJsonLine(result) : human);
186
+ if (offer && asking && result.ok) {
187
+ const prompter = terminalPrompter(stdin, stdout);
188
+ try {
189
+ await offerAgentAccess({ client: options.client, origin: options.origin, prompter, say });
190
+ } finally {
191
+ prompter.close();
192
+ }
121
193
  }
122
194
  return result;
123
195
  }
124
196
 
125
197
  if (READ_COMMANDS.includes(parsed.command)) {
126
- const { outcome, reason, data } = await runRead(parsed);
198
+ const { outcome, reason, data, validateOutput } = await runRead(parsed);
127
199
  const message = readMessage(outcome, reason);
128
200
  const result = envelope({
129
201
  command: parsed.command,
@@ -132,7 +204,44 @@ async function run(argv, { stdout, stderr }) {
132
204
  data,
133
205
  message: outcome === "ok" ? null : message,
134
206
  });
135
- stdout.write(parsed.json ? toJsonLine(result) : readText(result, message, style));
207
+ const text = parsed.json ? toJsonLine(result) : readText(result, message, style);
208
+ if (validateOutput && !validateOutput(text)) return refuseCredentialOutput(parsed, stdout, style, validateOutput);
209
+ stdout.write(text);
210
+ return result;
211
+ }
212
+
213
+ if (parsed.command === "experiments") {
214
+ // start is the one write: its own key session (experiments.js). The other
215
+ // actions are Metadata reads through runRead's shared session.
216
+ const { outcome, reason, data, validateOutput } = parsed.action === "start" ? await runExperimentStart(parsed) : await runRead(parsed);
217
+ const message = experimentsMessage(outcome, reason);
218
+ const result = envelope({
219
+ command: `experiments ${parsed.action}`,
220
+ outcome,
221
+ reason,
222
+ data,
223
+ message: outcome === "ok" ? null : message,
224
+ });
225
+ const text = parsed.json ? toJsonLine(result) : experimentsText(result, message, style, parsed.action);
226
+ if (validateOutput && !validateOutput(text)) return refuseCredentialOutput(parsed, stdout, style, validateOutput);
227
+ stdout.write(text);
228
+ return result;
229
+ }
230
+
231
+ if (parsed.command === "tools") {
232
+ // Any Agent Access tool, through runRead's verified session (tools.js).
233
+ const { outcome, reason, data, validateOutput } = await runRead(parsed);
234
+ const message = toolsMessage(outcome, reason);
235
+ const result = envelope({
236
+ command: `tools ${parsed.action}`,
237
+ outcome,
238
+ reason,
239
+ data,
240
+ message: outcome === "ok" ? null : message,
241
+ });
242
+ const text = parsed.json ? toJsonLine(result) : toolsText(result, message, style, parsed.action);
243
+ if (validateOutput && !validateOutput(text)) return refuseCredentialOutput(parsed, stdout, style, validateOutput);
244
+ stdout.write(text);
136
245
  return result;
137
246
  }
138
247
 
@@ -212,7 +321,7 @@ const SETUP_TITLES = {
212
321
  operator_handoff: "Setup is waiting on the operator",
213
322
  };
214
323
 
215
- function setupText(result, style) {
324
+ function setupText(result, style, { asking = false } = {}) {
216
325
  const data = result.data;
217
326
  const status = data?.status ?? "not_ready";
218
327
  const title = SETUP_TITLES[status] ?? (result.ok ? `Setup: ${status}` : "Setup did not complete");
@@ -241,9 +350,23 @@ function setupText(result, style) {
241
350
  lines.push(...remoteLines(data, style));
242
351
  lines.push(...budgetLines(data, style));
243
352
  if (result.ok) lines.push(...steps(style, [["", "Instrument your application and verify an exact trace."]]));
353
+ // At a terminal the offer is a question asked next instead.
354
+ if (data?.agent_access && !asking) lines.push(...agentAccessLines(data.agent_access, style));
244
355
  return render(lines);
245
356
  }
246
357
 
358
+ // How to give the agent more than Metadata on the customer-local bundle.
359
+ function agentAccessLines(access, style) {
360
+ const offer = access.offer;
361
+ return ["", ...steps(style, [
362
+ ["", "Your coding agent has Metadata access. To let it write evaluations and start analyses, " +
363
+ "run this in the bundle directory. It shows what it grants and asks you to confirm, then writes " +
364
+ "the key to that file without printing it."],
365
+ ["In the bundle directory", style.bold(offer.command)],
366
+ ["Bundle without bin/agent-key", offer.keys_page_url],
367
+ ]), ""];
368
+ }
369
+
247
370
  // What an operator handoff is waiting for, in fixed text: each unconfirmed
248
371
  // prerequisite with its guidance, and the next action.
249
372
  function handoffLines(data, style) {
@@ -302,3 +425,12 @@ function repositoryValue(repository) {
302
425
  default: return "not recorded (no owner/name git remote; pass --repository OWNER/NAME)";
303
426
  }
304
427
  }
428
+
429
+ function refuseCredentialOutput(parsed, stdout, style, validateOutput) {
430
+ const reason = "credential_in_metadata_response";
431
+ const message = readMessage("verification_failed", reason);
432
+ const result = envelope({ command: parsed.command, outcome: "verification_failed", reason, message });
433
+ const text = parsed.json ? toJsonLine(result) : `${message}\n`;
434
+ if (validateOutput(text)) stdout.write(text);
435
+ return result;
436
+ }
package/src/constants.js CHANGED
@@ -64,9 +64,38 @@ export const LOGIN_MIN_TIMEOUT_MS = 1000;
64
64
  export const LOGIN_MAX_TIMEOUT_MS = 900000;
65
65
  export const AUTH_HTTP_TIMEOUT_MS = 15000;
66
66
 
67
- // The only scope this CLI requests. It never asks for agent:read (Debug) or
67
+ // The scope sign in requests. It never asks for agent:read (Debug) or
68
68
  // agent:replay, and never falls back to them.
69
69
  export const METADATA_SCOPE = "agent:metadata";
70
+ // The Debug role and the workflow permissions an owner adds to it, by the
71
+ // names the Keys page uses. Writing and testing evaluations and starting
72
+ // analyses need Debug with all three. Replay is never used by this CLI.
73
+ export const DEBUG_SCOPE = "agent:read";
74
+ export const REPLAY_SCOPE = "agent:replay";
75
+ export const WORKFLOW_PERMISSIONS = Object.freeze([
76
+ Object.freeze({ field: "allow_evaluation_writes", scope: "agent:eval-write", label: "Allow evaluation writes" }),
77
+ Object.freeze({ field: "allow_evaluation_previews", scope: "agent:eval-preview", label: "Allow evaluation previews" }),
78
+ Object.freeze({ field: "allow_analysis_starts", scope: "agent:analysis-run", label: "Allow analysis starts" }),
79
+ ]);
80
+ export const WORKFLOW_SCOPES = Object.freeze(WORKFLOW_PERMISSIONS.map(({ scope }) => scope).sort());
81
+ // The permissions are offered where the deployment reports managed
82
+ // evaluations; customer-owned AWS (byoc-core) and the open-source server do not.
83
+ export const WORKFLOW_DEPLOYMENT_CAPABILITY = "evaluations";
84
+
85
+ // The scope login asks for and a saved sign in records: Metadata, or Debug
86
+ // followed by the chosen workflow permissions in WORKFLOW_SCOPES order.
87
+ export function loginScope(role, permissions = []) {
88
+ if (role !== "debug") return METADATA_SCOPE;
89
+ return [DEBUG_SCOPE, ...WORKFLOW_SCOPES.filter((scope) => permissions.includes(scope))].join(" ");
90
+ }
91
+
92
+ // Whether a saved sign in's scope is one login asks for, in that exact form.
93
+ export function isLoginScope(scope) {
94
+ if (scope === METADATA_SCOPE) return true;
95
+ if (typeof scope !== "string") return false;
96
+ const [first, ...rest] = scope.split(" ");
97
+ return first === DEBUG_SCOPE && scope === loginScope("debug", rest);
98
+ }
70
99
 
71
100
  // The service's agent access contract version, as sent in schema_version by
72
101
  // GET /v1/agent/workspace and GET /v1/agent/capabilities. It is the
@@ -99,8 +128,15 @@ export const READ_PATHS = Object.freeze({
99
128
  routes: "/v1/agent/routes",
100
129
  traces: "/v1/agent/traces",
101
130
  });
131
+ const PRICING_QUERY_KEYS = Object.freeze(["model", "provider", "channel", "region", "at", "limit",
132
+ "input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens", "cache_write_5m_tokens", "cache_write_1h_tokens", "batch"]);
102
133
  export const READ_QUERY_KEYS = Object.freeze({
134
+ "/v1/pricing/lookup": PRICING_QUERY_KEYS,
135
+ "/v1/pricing/cost": PRICING_QUERY_KEYS,
136
+ "/v1/pricing/history": PRICING_QUERY_KEYS,
137
+ "/v1/pricing/market": PRICING_QUERY_KEYS,
103
138
  "/v1/agent/usage": Object.freeze(["days", "limit", "metadata"]),
139
+ "/v1/agent/experiments": Object.freeze(["workload_id", "limit"]),
104
140
  "/v1/agent/usage/aggregates": Object.freeze([
105
141
  "group_by", "days", "since", "until", "limit", "route", "provider", "model", "environment", "metadata",
106
142
  ]),
@@ -124,6 +160,31 @@ export const USAGE_MAX_FILTER_VALUES = 20;
124
160
  export const USAGE_SORTS = Object.freeze(["cost", "cost-per-call", "calls", "errors", "avg-latency", "p95-latency", "day"]);
125
161
  export const USAGE_COMPARISONS = Object.freeze(["previous", "previous-same-time"]);
126
162
 
163
+ // Experiments: replay a workload's recent production calls on 1 to 3
164
+ // treatments and compare them with production. The list, estimate and
165
+ // summary are Metadata reads at these fixed paths (one experiment is
166
+ // EXPERIMENT_PATHS.list plus "/" and its UUID). Starting one needs a
167
+ // coding-agent key with Debug access and the EXPERIMENT_START_SCOPE grant.
168
+ // The limits match the service's own.
169
+ export const EXPERIMENT_PATHS = Object.freeze({
170
+ list: "/v1/agent/experiments",
171
+ estimate: "/v1/agent/experiments/estimate",
172
+ start: "/v1/agent/experiments",
173
+ });
174
+ export const EXPERIMENT_START_SCOPE = "agent:analysis-run";
175
+ // The scopes a start key must carry: Debug and the start grant. It may also
176
+ // carry the other workflow permissions; a key with
177
+ // Replay, Metadata only or any other scope is refused.
178
+ export const EXPERIMENT_START_KEY_SCOPES = Object.freeze([DEBUG_SCOPE, EXPERIMENT_START_SCOPE]);
179
+ export const EXPERIMENT_LAST_N = Object.freeze({ min: 10, max: 40, default: 20 });
180
+ export const EXPERIMENT_MAX_TREATMENTS = 3;
181
+ export const EXPERIMENT_MAX_TRACKED_FIELDS = 10;
182
+ export const EXPERIMENT_LIST_DEFAULT_LIMIT = 20;
183
+ export const EXPERIMENT_LIST_MAX_LIMIT = 50;
184
+ export const EXPERIMENT_TEXT_LIMITS = Object.freeze({ workload: 500, provider: 60, model: 200, label: 40, path: 200 });
185
+ export const EXPERIMENT_MAX_TOKENS = 32000;
186
+ export const EXPERIMENT_REASONING_EFFORTS = Object.freeze(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
187
+
127
188
  // Read commands share one total deadline for every request they make,
128
189
  // including a token refresh. It is never reset per request or page.
129
190
  export const READ_DEFAULT_TIMEOUT_MS = 15000;
@@ -171,6 +232,8 @@ export const EXIT_CODES = Object.freeze({
171
232
  rate_limited: 16,
172
233
  cancelled: 17,
173
234
  action_required: 18,
235
+ refused: 19,
236
+ unconfirmed: 20,
174
237
  });
175
238
 
176
239
  export const EXIT_CODE_MEANINGS = Object.freeze({
@@ -211,4 +274,10 @@ export const EXIT_CODE_MEANINGS = Object.freeze({
211
274
  "A person must act before this command can finish; data.next_action says what to do. " +
212
275
  "With --json --no-browser the command exits as soon as the approval URL exists, and a background " +
213
276
  "process waits for approval until --timeout-ms. After approval, run the same command again to continue.",
277
+ refused:
278
+ "The service refused the experiment for a stated reason, such as its budget, the daily cap, experiments " +
279
+ "already running or a request it cannot price; data.refusal says which. Nothing was started.",
280
+ unconfirmed:
281
+ "An experiment start reached the service but its result was not confirmed, so an experiment may have " +
282
+ "been recorded and may run. List the workload's experiments before starting again: each start creates one.",
214
283
  });