metergraph-cli 0.2.2 → 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/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,21 @@ 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";
18
22
  import { readMessage, readText } from "./read-output.js";
19
23
  import { listSkillPack, runSkill, runSkillPack } from "./skill.js";
20
24
  import { runSetup } from "./setup.js";
21
25
  import { containsKnownCredential, preflightNonHostedSetup, prerequisiteGuide, resumeNonHostedRoute } from "./setup-deployment.js";
26
+ import {
27
+ ASKABLE,
28
+ agentAccessOffer,
29
+ canAsk,
30
+ commandLine,
31
+ fillSetupArgv,
32
+ offerAgentAccess,
33
+ terminalPrompter,
34
+ } from "./setup-interactive.js";
22
35
  import { VERSION } from "./constants.js";
23
36
  import { createStyle, fields, headline, paragraph, render, steps, toneFor } from "./style.js";
24
37
  import { runVerify } from "./verify.js";
@@ -26,12 +39,13 @@ import { verifyMessage, verifyText } from "./verify-output.js";
26
39
 
27
40
  // Runs one command and resolves with the exit code. With --json, stdout gets
28
41
  // 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 }) {
42
+ // stdout and usage or internal errors go to stderr. Only setup reads stdin,
43
+ // and only to ask a person at a terminal (see setup-interactive.js).
44
+ export async function main(argv, { stdout, stderr, stdin = null }) {
31
45
  const json = argv.includes("--json");
32
46
  let result;
33
47
  try {
34
- result = await run(argv, { stdout, stderr });
48
+ result = await run(argv, { stdout, stderr, stdin });
35
49
  } catch {
36
50
  result = envelope({
37
51
  command: null,
@@ -45,9 +59,33 @@ export async function main(argv, { stdout, stderr }) {
45
59
  return result.exit_code;
46
60
  }
47
61
 
48
- async function run(argv, { stdout, stderr }) {
49
- const parsed = parseArgs(argv);
62
+ async function run(argv, { stdout, stderr, stdin }) {
63
+ let parsed = parseArgs(argv);
50
64
  const style = createStyle(stdout);
65
+ const say = (...lines) => stdout.write(lines.map((line) => `${line}\n`).join(""));
66
+ const asking = canAsk(argv, { stdin, stdout });
67
+
68
+ // A person at a terminal is asked for what setup is missing; the answers
69
+ // become flags and go through the same parsing as a script's.
70
+ if (parsed.command === "setup" && !parsed.ok && ASKABLE.has(parsed.code) && asking) {
71
+ const prompter = terminalPrompter(stdin, stdout);
72
+ let filled;
73
+ try {
74
+ filled = await fillSetupArgv(argv, { ask: prompter.ask, say });
75
+ } finally {
76
+ prompter.close();
77
+ }
78
+ if (filled === null) {
79
+ stderr.write("Setup was cancelled. Nothing was changed.\n");
80
+ return envelope({ command: "setup", outcome: "cancelled", reason: "questions_cancelled",
81
+ message: "Setup was cancelled. Nothing was changed." });
82
+ }
83
+ argv = filled;
84
+ parsed = parseArgs(argv);
85
+ // Only a command that parses is shown: a value the parser refuses may be
86
+ // a mistyped credential, and its error never echoes input.
87
+ if (parsed.ok) say("", `Running: ${commandLine(filled)}`, "");
88
+ }
51
89
 
52
90
  if (!parsed.ok) {
53
91
  const result = envelope({
@@ -73,6 +111,14 @@ async function run(argv, { stdout, stderr }) {
73
111
  return result;
74
112
  }
75
113
 
114
+ if (parsed.command === "pricing") {
115
+ const response = await runPricing(parsed);
116
+ const result = envelope({ command: `pricing ${parsed.action}`, ...response });
117
+ stdout.write(parsed.json ? toJsonLine(result) :
118
+ parsed.format === "html" && result.ok ? pricingHtml(result.data) : pricingText(result, parsed.format, style));
119
+ return result;
120
+ }
121
+
76
122
  if (parsed.command === "login" || parsed.command === "logout") {
77
123
  // Human progress goes to stderr, never with --json, so stdout carries
78
124
  // only the final result.
@@ -106,18 +152,30 @@ async function run(argv, { stdout, stderr }) {
106
152
  ? await runSetup({ ...options, expectedProfile: routed.profile ?? null, announce: pending.announce }, progress)
107
153
  : routed;
108
154
  if (pending.result) return pending.result;
109
- const { outcome, reason, data } = response;
155
+ const { outcome, reason } = response;
156
+ // The customer-local bundle can give the agent more than Metadata
157
+ // without a sign in; say how.
158
+ const offer = outcome === "ok" && routed.profile === "local";
159
+ const data = offer ? { ...response.data, agent_access: agentAccessOffer(options.origin) } : response.data;
110
160
  let result = envelope({ command: "setup", outcome, reason, data,
111
161
  message: outcome === "ok" ? null : "Setup did not complete. Review the reason and retry safely." });
112
162
  if (containsKnownCredential(result, routed)) {
113
163
  result = envelope({ command: "setup", outcome: "unsupported", reason: "credential_echo" });
114
164
  }
115
- const human = setupText(result, style);
165
+ const human = setupText(result, style, { asking });
116
166
  if (containsKnownCredential(human, routed)) {
117
167
  result = envelope({ command: "setup", outcome: "unsupported", reason: "credential_echo" });
118
168
  stdout.write(parsed.json ? toJsonLine(result) : "Setup stopped.\n");
119
- } else {
120
- stdout.write(parsed.json ? toJsonLine(result) : human);
169
+ return result;
170
+ }
171
+ stdout.write(parsed.json ? toJsonLine(result) : human);
172
+ if (offer && asking && result.ok) {
173
+ const prompter = terminalPrompter(stdin, stdout);
174
+ try {
175
+ await offerAgentAccess({ client: options.client, origin: options.origin, prompter, say });
176
+ } finally {
177
+ prompter.close();
178
+ }
121
179
  }
122
180
  return result;
123
181
  }
@@ -136,6 +194,22 @@ async function run(argv, { stdout, stderr }) {
136
194
  return result;
137
195
  }
138
196
 
197
+ if (parsed.command === "experiments") {
198
+ // start is the one write: its own key session (experiments.js). The other
199
+ // actions are Metadata reads through runRead's shared session.
200
+ const { outcome, reason, data } = parsed.action === "start" ? await runExperimentStart(parsed) : await runRead(parsed);
201
+ const message = experimentsMessage(outcome, reason);
202
+ const result = envelope({
203
+ command: `experiments ${parsed.action}`,
204
+ outcome,
205
+ reason,
206
+ data,
207
+ message: outcome === "ok" ? null : message,
208
+ });
209
+ stdout.write(parsed.json ? toJsonLine(result) : experimentsText(result, message, style, parsed.action));
210
+ return result;
211
+ }
212
+
139
213
  if (parsed.command === "verify") {
140
214
  const { outcome, reason, data } = await runVerify(parsed);
141
215
  const message = verifyMessage(outcome, reason);
@@ -212,7 +286,7 @@ const SETUP_TITLES = {
212
286
  operator_handoff: "Setup is waiting on the operator",
213
287
  };
214
288
 
215
- function setupText(result, style) {
289
+ function setupText(result, style, { asking = false } = {}) {
216
290
  const data = result.data;
217
291
  const status = data?.status ?? "not_ready";
218
292
  const title = SETUP_TITLES[status] ?? (result.ok ? `Setup: ${status}` : "Setup did not complete");
@@ -241,9 +315,23 @@ function setupText(result, style) {
241
315
  lines.push(...remoteLines(data, style));
242
316
  lines.push(...budgetLines(data, style));
243
317
  if (result.ok) lines.push(...steps(style, [["", "Instrument your application and verify an exact trace."]]));
318
+ // At a terminal the offer is a question asked next instead.
319
+ if (data?.agent_access && !asking) lines.push(...agentAccessLines(data.agent_access, style));
244
320
  return render(lines);
245
321
  }
246
322
 
323
+ // How to give the agent more than Metadata on the customer-local bundle.
324
+ function agentAccessLines(access, style) {
325
+ const offer = access.offer;
326
+ return ["", ...steps(style, [
327
+ ["", "Your coding agent has Metadata access. To let it write evaluations and start analyses, " +
328
+ "run this in the bundle directory. It shows what it grants and asks you to confirm, then writes " +
329
+ "the key to that file without printing it."],
330
+ ["In the bundle directory", style.bold(offer.command)],
331
+ ["Bundle without bin/agent-key", offer.keys_page_url],
332
+ ]), ""];
333
+ }
334
+
247
335
  // What an operator handoff is waiting for, in fixed text: each unconfirmed
248
336
  // prerequisite with its guidance, and the next action.
249
337
  function handoffLines(data, style) {
package/src/constants.js CHANGED
@@ -99,8 +99,15 @@ export const READ_PATHS = Object.freeze({
99
99
  routes: "/v1/agent/routes",
100
100
  traces: "/v1/agent/traces",
101
101
  });
102
+ const PRICING_QUERY_KEYS = Object.freeze(["model", "provider", "channel", "region", "at", "limit",
103
+ "input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens", "cache_write_5m_tokens", "cache_write_1h_tokens", "batch"]);
102
104
  export const READ_QUERY_KEYS = Object.freeze({
105
+ "/v1/pricing/lookup": PRICING_QUERY_KEYS,
106
+ "/v1/pricing/cost": PRICING_QUERY_KEYS,
107
+ "/v1/pricing/history": PRICING_QUERY_KEYS,
108
+ "/v1/pricing/market": PRICING_QUERY_KEYS,
103
109
  "/v1/agent/usage": Object.freeze(["days", "limit", "metadata"]),
110
+ "/v1/agent/experiments": Object.freeze(["workload_id", "limit"]),
104
111
  "/v1/agent/usage/aggregates": Object.freeze([
105
112
  "group_by", "days", "since", "until", "limit", "route", "provider", "model", "environment", "metadata",
106
113
  ]),
@@ -124,6 +131,32 @@ export const USAGE_MAX_FILTER_VALUES = 20;
124
131
  export const USAGE_SORTS = Object.freeze(["cost", "cost-per-call", "calls", "errors", "avg-latency", "p95-latency", "day"]);
125
132
  export const USAGE_COMPARISONS = Object.freeze(["previous", "previous-same-time"]);
126
133
 
134
+ // Experiments: replay a workload's recent production calls on 1 to 3
135
+ // treatments and compare them with production. The list, estimate and
136
+ // summary are Metadata reads at these fixed paths (one experiment is
137
+ // EXPERIMENT_PATHS.list plus "/" and its UUID). Starting one is the only
138
+ // write this CLI makes to a workspace: it needs a separate coding-agent key
139
+ // with Debug access and the EXPERIMENT_START_SCOPE grant, used for that one
140
+ // request and nothing else. The limits match the service's own.
141
+ export const EXPERIMENT_PATHS = Object.freeze({
142
+ list: "/v1/agent/experiments",
143
+ estimate: "/v1/agent/experiments/estimate",
144
+ start: "/v1/agent/experiments",
145
+ });
146
+ export const EXPERIMENT_START_SCOPE = "agent:analysis-run";
147
+ // Exactly the scopes a start key carries: Debug and the start grant. A key
148
+ // with any other scope (Replay, other workflow grants, Metadata, ingest) is
149
+ // refused, so the key in an agent's environment can do no more than this.
150
+ export const EXPERIMENT_START_KEY_SCOPES = Object.freeze(["agent:read", EXPERIMENT_START_SCOPE]);
151
+ export const EXPERIMENT_LAST_N = Object.freeze({ min: 10, max: 40, default: 20 });
152
+ export const EXPERIMENT_MAX_TREATMENTS = 3;
153
+ export const EXPERIMENT_MAX_TRACKED_FIELDS = 10;
154
+ export const EXPERIMENT_LIST_DEFAULT_LIMIT = 20;
155
+ export const EXPERIMENT_LIST_MAX_LIMIT = 50;
156
+ export const EXPERIMENT_TEXT_LIMITS = Object.freeze({ workload: 500, provider: 60, model: 200, label: 40, path: 200 });
157
+ export const EXPERIMENT_MAX_TOKENS = 32000;
158
+ export const EXPERIMENT_REASONING_EFFORTS = Object.freeze(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
159
+
127
160
  // Read commands share one total deadline for every request they make,
128
161
  // including a token refresh. It is never reset per request or page.
129
162
  export const READ_DEFAULT_TIMEOUT_MS = 15000;
@@ -171,6 +204,8 @@ export const EXIT_CODES = Object.freeze({
171
204
  rate_limited: 16,
172
205
  cancelled: 17,
173
206
  action_required: 18,
207
+ refused: 19,
208
+ unconfirmed: 20,
174
209
  });
175
210
 
176
211
  export const EXIT_CODE_MEANINGS = Object.freeze({
@@ -211,4 +246,10 @@ export const EXIT_CODE_MEANINGS = Object.freeze({
211
246
  "A person must act before this command can finish; data.next_action says what to do. " +
212
247
  "With --json --no-browser the command exits as soon as the approval URL exists, and a background " +
213
248
  "process waits for approval until --timeout-ms. After approval, run the same command again to continue.",
249
+ refused:
250
+ "The service refused the experiment for a stated reason, such as its budget, the daily cap, experiments " +
251
+ "already running or a request it cannot price; data.refusal says which. Nothing was started.",
252
+ unconfirmed:
253
+ "An experiment start reached the service but its result was not confirmed, so an experiment may have " +
254
+ "been recorded and may run. List the workload's experiments before starting again: each start creates one.",
214
255
  });