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/README.md +49 -5
- package/bin/metergraph.js +1 -1
- package/package.json +1 -1
- package/src/agent-key.js +6 -3
- package/src/args.js +218 -22
- package/src/auth-oauth.js +58 -5
- package/src/cli.js +98 -10
- package/src/constants.js +41 -0
- package/src/experiments-contract.js +462 -0
- package/src/experiments-output.js +189 -0
- package/src/experiments.js +397 -0
- package/src/output.js +112 -1
- 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 +23 -3
- package/src/read.js +15 -1
- package/src/setup-deployment.js +7 -0
- package/src/setup-interactive.js +341 -0
- package/src/transport.js +5 -3
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.
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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
|
});
|