metergraph-cli 0.2.0 → 0.2.2
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 +21 -16
- package/assets/skill/SKILL.md +53 -4
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +16 -16
- package/assets/skills/metergraph-analyze/SKILL.md +62 -4
- package/assets/skills/metergraph-investigate/SKILL.md +222 -35
- package/assets/skills/metergraph-local-bundle/SKILL.md +4 -4
- package/assets/skills/metergraph-onboarding/SKILL.md +7 -0
- package/assets/skills/metergraph-report/SKILL.md +19 -2
- package/package.json +1 -1
- package/src/agent-key.js +157 -0
- package/src/approval-handoff.js +5 -0
- package/src/args.js +373 -14
- package/src/auth-browser.js +5 -19
- package/src/auth-callback.js +24 -15
- package/src/auth-login.js +55 -12
- package/src/auth-oauth.js +20 -3
- package/src/callback-page.js +58 -0
- package/src/cli.js +94 -37
- package/src/constants.js +32 -3
- package/src/environment.js +169 -0
- package/src/origin.js +6 -0
- package/src/output.js +325 -130
- package/src/read-contract.js +953 -13
- package/src/read-output.js +449 -30
- package/src/read.js +297 -59
- package/src/setup-deployment.js +4 -3
- package/src/setup-refusal.js +172 -0
- package/src/setup-state.js +4 -2
- package/src/setup.js +227 -53
- package/src/skill-bundle.js +2 -2
- package/src/style.js +101 -0
- package/src/transport.js +8 -4
- package/src/usage-aggregates.js +197 -0
- package/src/verify-output.js +21 -11
package/src/output.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
SUPPORTED_PROFILES,
|
|
28
28
|
VERSION,
|
|
29
29
|
} from "./constants.js";
|
|
30
|
+
import { PLAIN, fields, headline, paragraph, render, section, steps, toneFor } from "./style.js";
|
|
30
31
|
|
|
31
32
|
// Every JSON result, success or failure, has the same top-level keys:
|
|
32
33
|
// schema_version, command, ok, outcome, exit_code, data, error
|
|
@@ -115,6 +116,20 @@ const AGENT_APPROVAL_HELP = [
|
|
|
115
116
|
"and after they approve, run the same command again to continue.",
|
|
116
117
|
];
|
|
117
118
|
|
|
119
|
+
// One recommended sequence per place an agent runs. login and setup detect
|
|
120
|
+
// SSH, cloud workspaces and CI and answer with a structured next_action.
|
|
121
|
+
const AGENT_PATH_HELP = [
|
|
122
|
+
"Where the agent runs:",
|
|
123
|
+
" Local shell or local container setup --runtime local --client C --json --no-browser,",
|
|
124
|
+
" rerun after each approval, then status --json",
|
|
125
|
+
" SSH, cloud workspace or CI A person creates a coding-agent key with Metadata access",
|
|
126
|
+
" on the Keys page and saves it as the secret",
|
|
127
|
+
" METERGRAPH_AGENT_TOKEN; then status --json, usage --json",
|
|
128
|
+
" Metadata key, no saved sign in The same: reads use METERGRAPH_AGENT_TOKEN, or",
|
|
129
|
+
" --agent-token-file PATH, after verifying the workspace",
|
|
130
|
+
"Never pass a key as an argument. Debug and Replay keys are refused, not reduced.",
|
|
131
|
+
];
|
|
132
|
+
|
|
118
133
|
const CONFIG_DIR_OPTION = {
|
|
119
134
|
name: "--config-dir",
|
|
120
135
|
value: "DIR",
|
|
@@ -127,7 +142,9 @@ const LOGIN_OPTIONS = [
|
|
|
127
142
|
{
|
|
128
143
|
name: "--runtime",
|
|
129
144
|
value: "RUNTIME",
|
|
130
|
-
summary:
|
|
145
|
+
summary:
|
|
146
|
+
"Required. Where this terminal runs, not which Metergraph service. local: your own machine, " +
|
|
147
|
+
"including for the hosted service. cloud, cloud-no-shell: a cloud sandbox; get the connection guide.",
|
|
131
148
|
},
|
|
132
149
|
{ name: "--url", value: "ORIGIN", summary: `Origin to sign in to. Default ${DEFAULT_ORIGIN}.` },
|
|
133
150
|
{
|
|
@@ -195,24 +212,61 @@ const READ_BASE_OPTIONS = [
|
|
|
195
212
|
`Total time for every request of the command, ${READ_MIN_TIMEOUT_MS} to ${READ_MAX_TIMEOUT_MS}. ` +
|
|
196
213
|
`Default ${READ_DEFAULT_TIMEOUT_MS}.`,
|
|
197
214
|
},
|
|
215
|
+
{
|
|
216
|
+
name: "--url",
|
|
217
|
+
value: "ORIGIN",
|
|
218
|
+
summary:
|
|
219
|
+
"Where a Metadata key is sent when the project is not bound. Default METERGRAPH_URL, else the hosted service. " +
|
|
220
|
+
"A bound project's origin cannot be changed here.",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "--workspace",
|
|
224
|
+
value: "UUID",
|
|
225
|
+
summary: "The workspace the credential must be for. Without it, an unbound key's own workspace is used.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "--agent-token-file",
|
|
229
|
+
value: "PATH",
|
|
230
|
+
summary:
|
|
231
|
+
"Private (0600) file holding a coding-agent key with Metadata access. Used instead of a saved sign in. " +
|
|
232
|
+
"Without it, METERGRAPH_AGENT_TOKEN is used when this project has no saved sign in on this machine.",
|
|
233
|
+
},
|
|
198
234
|
];
|
|
199
235
|
const DAYS_OPTION = {
|
|
200
236
|
name: "--days",
|
|
201
237
|
value: "N",
|
|
202
238
|
summary: `Window of the last N days, 1 to ${READ_MAX_DAYS}. Default ${READ_DEFAULT_DAYS}.`,
|
|
203
239
|
};
|
|
240
|
+
const METADATA_OPTIONS = [
|
|
241
|
+
{
|
|
242
|
+
name: "--metadata",
|
|
243
|
+
value: "KEY=VALUE",
|
|
244
|
+
summary:
|
|
245
|
+
"Only calls whose metadata (SDK tags) has exactly this string value: case-sensitive, no prefix or " +
|
|
246
|
+
"trimming. Repeat for up to 5 keys, all must match. A call without the key never matches; KEY= " +
|
|
247
|
+
"matches only an empty value. Splits at the first =, so a value may contain =.",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "--metadata-json",
|
|
251
|
+
value: "JSON",
|
|
252
|
+
summary:
|
|
253
|
+
'The same filters as one JSON object, for keys that contain = (for example {"a=b":"v"}). Not with --metadata.',
|
|
254
|
+
},
|
|
255
|
+
];
|
|
204
256
|
const limitOption = (fallback, extra = "") => ({
|
|
205
257
|
name: "--limit",
|
|
206
258
|
value: "N",
|
|
207
259
|
summary: `Rows to return, 1 to ${READ_MAX_LIMIT}. Default ${fallback}.${extra}`,
|
|
208
260
|
});
|
|
209
|
-
const READ_BASE_USAGE =
|
|
261
|
+
const READ_BASE_USAGE =
|
|
262
|
+
"[--url ORIGIN] [--workspace UUID] [--agent-token-file PATH] [--project DIR] [--config-dir DIR] [--timeout-ms N] [--json]";
|
|
210
263
|
const REFUSED_NOTE =
|
|
211
264
|
"--environment, --workload, --since, --until, --sql, --query, --content, --include-content, --debug and --replay " +
|
|
212
|
-
"are recognized and refused with exit code 6 before any request.";
|
|
265
|
+
"are recognized and refused with exit code 6 before any request, except --environment, --since and --until on usage.";
|
|
213
266
|
|
|
214
|
-
// The read commands. Each uses the project's saved Metadata grant
|
|
215
|
-
//
|
|
267
|
+
// 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.
|
|
216
270
|
export const READ_HELP = Object.freeze([
|
|
217
271
|
{
|
|
218
272
|
name: "status",
|
|
@@ -237,9 +291,82 @@ export const READ_HELP = Object.freeze([
|
|
|
237
291
|
},
|
|
238
292
|
{
|
|
239
293
|
name: "usage",
|
|
240
|
-
usage:
|
|
241
|
-
|
|
242
|
-
|
|
294
|
+
usage:
|
|
295
|
+
`metergraph usage [--days N | --today | --since TIME [--until TIME]] [--group-by DIMS] ` +
|
|
296
|
+
`[--route NAME]... [--provider NAME]... [--model NAME]... [--environment NAME]... ` +
|
|
297
|
+
`[--sort ORDER] [--compare previous|previous-same-time] [--limit N] ` +
|
|
298
|
+
`[--metadata KEY=VALUE]... [--metadata-json JSON] [--group-by-metadata KEY] ${READ_BASE_USAGE}`,
|
|
299
|
+
summary:
|
|
300
|
+
"Show calls, errors, cost, tokens and latency. With --group-by, a filter, a UTC window, --sort or --compare " +
|
|
301
|
+
"it reads grouped usage: cost by provider, model, route or UTC day, computed per call by the service, with " +
|
|
302
|
+
"totals over every matching call and coverage, pricing and attribution evidence. Without them it lists cost " +
|
|
303
|
+
"per route and day for the last N days. A route is not a prompt: one route can carry several prompt IDs, " +
|
|
304
|
+
'so "metergraph usage --group-by metadata.prompt_id" splits cost by prompt. "metergraph routes" lists ' +
|
|
305
|
+
"route definitions, not their cost.",
|
|
306
|
+
examples: [
|
|
307
|
+
["Most expensive route today", "metergraph usage --group-by route --today"],
|
|
308
|
+
["Spend with one provider this week", "metergraph usage --group-by provider --provider NAME --days 7"],
|
|
309
|
+
["Spend by model", "metergraph usage --group-by model --days 7"],
|
|
310
|
+
["One route's daily trend", "metergraph usage --route NAME --group-by day --days 14"],
|
|
311
|
+
["Cost by prompt ID", "metergraph usage --group-by metadata.prompt_id --days 7"],
|
|
312
|
+
["Today against yesterday", "metergraph usage --group-by route --today --compare previous"],
|
|
313
|
+
],
|
|
314
|
+
options: [
|
|
315
|
+
...READ_BASE_OPTIONS,
|
|
316
|
+
{
|
|
317
|
+
name: "--days",
|
|
318
|
+
value: "N",
|
|
319
|
+
summary:
|
|
320
|
+
`Window, 1 to ${READ_MAX_DAYS}. Default ${READ_DEFAULT_DAYS}. Grouped: the current UTC day so far and the ` +
|
|
321
|
+
"N - 1 whole UTC days before it (window.kind utc_days). Otherwise the last N x 24 hours, rolling and not " +
|
|
322
|
+
"aligned to UTC days (window.kind rolling).",
|
|
323
|
+
},
|
|
324
|
+
{ name: "--today", value: null, summary: "Grouped: the current UTC day so far (--days 1)." },
|
|
325
|
+
{
|
|
326
|
+
name: "--since",
|
|
327
|
+
value: "TIME",
|
|
328
|
+
summary:
|
|
329
|
+
"Grouped: window start, inclusive. A UTC date (YYYY-MM-DD, 00:00 UTC) or an ISO 8601 timestamp with Z " +
|
|
330
|
+
`or an offset. At most ${READ_MAX_DAYS} days before --until.`,
|
|
331
|
+
},
|
|
332
|
+
{ name: "--until", value: "TIME", summary: "Grouped: window end, exclusive, in the same form. Default now." },
|
|
333
|
+
{
|
|
334
|
+
name: "--group-by",
|
|
335
|
+
value: "DIMS",
|
|
336
|
+
summary: "Comma-separated, in order: provider, model, route, day (UTC calendar day). Default provider,model.",
|
|
337
|
+
},
|
|
338
|
+
{ name: "--route", value: "NAME", summary: "Grouped: only this route, exactly. Repeat for up to 20; (unrouted) selects calls without one." },
|
|
339
|
+
{ name: "--provider", value: "NAME", summary: "Grouped: only this provider, exactly. Repeat for up to 20." },
|
|
340
|
+
{ name: "--model", value: "NAME", summary: "Grouped: only this model, exactly. Repeat for up to 20." },
|
|
341
|
+
{ name: "--environment", value: "NAME", summary: "Grouped: only this environment. Repeat for several. A restricted grant reads its own environments only." },
|
|
342
|
+
{
|
|
343
|
+
name: "--sort",
|
|
344
|
+
value: "ORDER",
|
|
345
|
+
summary:
|
|
346
|
+
"cost (default), cost-per-call, calls, errors, avg-latency (mean over calls that reported latency), " +
|
|
347
|
+
"p95-latency (estimated from a fixed histogram) or day (oldest first; the default for --group-by day). " +
|
|
348
|
+
"The service returns the highest-cost groups, so another order over a cut list is marked partial.",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "--compare",
|
|
352
|
+
value: "MODE",
|
|
353
|
+
summary:
|
|
354
|
+
"Also read the period before and show the change per group. previous: the whole prior period, which is " +
|
|
355
|
+
"longer than a window ending part way through today (a warning says so). previous-same-time: the prior " +
|
|
356
|
+
"period to the same elapsed time. Not with --group-by day.",
|
|
357
|
+
},
|
|
358
|
+
limitOption(READ_DEFAULT_LIMIT, " Grouped: the most groups listed; totals still cover every call."),
|
|
359
|
+
...METADATA_OPTIONS,
|
|
360
|
+
{
|
|
361
|
+
name: "--group-by-metadata",
|
|
362
|
+
value: "KEY",
|
|
363
|
+
summary:
|
|
364
|
+
"Group by one metadata key: the same as ending --group-by with metadata.KEY, and the way to give a key " +
|
|
365
|
+
"that holds a comma. Calls without the key or with a non-string value are one group with no value, and " +
|
|
366
|
+
"the spend not attributed to any value is totalled.",
|
|
367
|
+
},
|
|
368
|
+
JSON_OPTION,
|
|
369
|
+
],
|
|
243
370
|
},
|
|
244
371
|
{
|
|
245
372
|
name: "routes",
|
|
@@ -260,7 +387,7 @@ export const READ_HELP = Object.freeze([
|
|
|
260
387
|
name: "traces",
|
|
261
388
|
usage:
|
|
262
389
|
`metergraph traces [--days N] [--limit N] [--route NAME] [--status success|error] ` +
|
|
263
|
-
`[--cursor CURSOR] ${READ_BASE_USAGE}`,
|
|
390
|
+
`[--metadata KEY=VALUE]... [--metadata-json JSON] [--cursor CURSOR] ${READ_BASE_USAGE}`,
|
|
264
391
|
summary:
|
|
265
392
|
"List one page of trace metadata: status, span count, tokens, cost, routes, providers and models. " +
|
|
266
393
|
"No prompts, responses or tool calls.",
|
|
@@ -270,6 +397,7 @@ export const READ_HELP = Object.freeze([
|
|
|
270
397
|
limitOption(TRACES_DEFAULT_LIMIT),
|
|
271
398
|
{ name: "--route", value: "NAME", summary: "Only traces that include this route." },
|
|
272
399
|
{ name: "--status", value: "STATUS", summary: "Only success or error traces." },
|
|
400
|
+
...METADATA_OPTIONS,
|
|
273
401
|
{
|
|
274
402
|
name: "--cursor",
|
|
275
403
|
value: "CURSOR",
|
|
@@ -280,6 +408,15 @@ export const READ_HELP = Object.freeze([
|
|
|
280
408
|
},
|
|
281
409
|
]);
|
|
282
410
|
|
|
411
|
+
// One " FLAG summary" line per option. The flag column keeps its usual
|
|
412
|
+
// width but grows to fit the longest flag, so a flag never runs into its
|
|
413
|
+
// summary.
|
|
414
|
+
function optionLines(options, minimum) {
|
|
415
|
+
const flags = options.map((option) => (option.value ? `${option.name} ${option.value}` : option.name));
|
|
416
|
+
const width = Math.max(minimum, ...flags.map((flag) => flag.length + 2));
|
|
417
|
+
return options.map((option, index) => ` ${flags[index].padEnd(width)}${option.summary}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
283
420
|
export function helpData(topic) {
|
|
284
421
|
return {
|
|
285
422
|
topic,
|
|
@@ -377,7 +514,12 @@ export function helpData(topic) {
|
|
|
377
514
|
summary: "Poll Metadata for one exact trace in an explicit invocation window. Never sends application traffic.",
|
|
378
515
|
options: VERIFY_OPTIONS,
|
|
379
516
|
},
|
|
380
|
-
...READ_HELP.map(({ name, summary, options }) => ({
|
|
517
|
+
...READ_HELP.map(({ name, summary, options, examples }) => ({
|
|
518
|
+
name,
|
|
519
|
+
summary,
|
|
520
|
+
options,
|
|
521
|
+
...(examples ? { examples: examples.map(([question, command]) => ({ question, command })) } : {}),
|
|
522
|
+
})),
|
|
381
523
|
],
|
|
382
524
|
skill_clients: Object.keys(SKILL_CLIENTS),
|
|
383
525
|
skill_runtimes: [...SKILL_RUNTIMES],
|
|
@@ -391,7 +533,7 @@ export function helpData(topic) {
|
|
|
391
533
|
};
|
|
392
534
|
}
|
|
393
535
|
|
|
394
|
-
export function helpText(topic) {
|
|
536
|
+
export function helpText(topic, style = PLAIN) {
|
|
395
537
|
const lines = [];
|
|
396
538
|
if (topic === "doctor") {
|
|
397
539
|
lines.push(
|
|
@@ -402,10 +544,7 @@ export function helpText(topic) {
|
|
|
402
544
|
"",
|
|
403
545
|
"Options:",
|
|
404
546
|
);
|
|
405
|
-
|
|
406
|
-
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
407
|
-
lines.push(` ${flag.padEnd(18)}${option.summary}`);
|
|
408
|
-
}
|
|
547
|
+
lines.push(...optionLines(DOCTOR_OPTIONS, 18));
|
|
409
548
|
lines.push(
|
|
410
549
|
"",
|
|
411
550
|
"ORIGIN must be a bare https origin such as https://metergraph.example.com.",
|
|
@@ -422,10 +561,7 @@ export function helpText(topic) {
|
|
|
422
561
|
"operator prerequisite attestation; OSS remains an operator handoff. No application",
|
|
423
562
|
"traffic is claimed until an exact instrumented invocation is separately verified.",
|
|
424
563
|
"", "Options:");
|
|
425
|
-
|
|
426
|
-
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
427
|
-
lines.push(` ${flag.padEnd(25)}${option.summary}`);
|
|
428
|
-
}
|
|
564
|
+
lines.push(...optionLines(helpData(null).commands.find((entry) => entry.name === "setup").options, 25));
|
|
429
565
|
lines.push("", ...AGENT_APPROVAL_HELP);
|
|
430
566
|
} else if (topic === "login" || topic === "logout") {
|
|
431
567
|
const login = topic === "login";
|
|
@@ -455,20 +591,14 @@ export function helpText(topic) {
|
|
|
455
591
|
);
|
|
456
592
|
}
|
|
457
593
|
lines.push("Options:");
|
|
458
|
-
|
|
459
|
-
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
460
|
-
lines.push(` ${flag.padEnd(18)}${option.summary}`);
|
|
461
|
-
}
|
|
594
|
+
lines.push(...optionLines(login ? LOGIN_OPTIONS : LOGOUT_OPTIONS, 18));
|
|
462
595
|
if (login) lines.push("", ...AGENT_APPROVAL_HELP);
|
|
463
596
|
} else if (topic === "verify") {
|
|
464
597
|
lines.push(`Usage: ${VERIFY_USAGE}`, "", "Uses the saved Metadata grant to poll for one exact, processed trace.",
|
|
465
598
|
"The source label is supplied by the caller. A Metadata match alone does not prove application traffic.",
|
|
466
599
|
"Opening requires a server link that selects the verified workspace; older links get a manual handoff.",
|
|
467
600
|
"", "Options:");
|
|
468
|
-
|
|
469
|
-
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
470
|
-
lines.push(` ${flag.padEnd(22)}${option.summary}`);
|
|
471
|
-
}
|
|
601
|
+
lines.push(...optionLines(VERIFY_OPTIONS, 22));
|
|
472
602
|
} else if (READ_HELP.some((entry) => entry.name === topic)) {
|
|
473
603
|
const entry = READ_HELP.find((candidate) => candidate.name === topic);
|
|
474
604
|
lines.push(
|
|
@@ -476,18 +606,23 @@ export function helpText(topic) {
|
|
|
476
606
|
"",
|
|
477
607
|
entry.summary,
|
|
478
608
|
"",
|
|
479
|
-
"Uses this project's saved Metadata grant (see \"metergraph help login\")
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
"
|
|
483
|
-
"
|
|
609
|
+
"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",
|
|
614
|
+
"reads bounded responses. Never opens a browser, signs in, writes project files or the key, reads",
|
|
615
|
+
"retained content, replays or calls a model provider. It may refresh its own saved grant, and the",
|
|
616
|
+
"service may record the access (for example last used times); it never changes workspace",
|
|
617
|
+
"configuration or telemetry and never sends ingest data.",
|
|
484
618
|
REFUSED_NOTE,
|
|
485
619
|
"",
|
|
486
620
|
"Options:",
|
|
487
621
|
);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
lines.push(
|
|
622
|
+
lines.push(...optionLines(entry.options, 25));
|
|
623
|
+
if (entry.examples) {
|
|
624
|
+
lines.push("", "Examples (add --json for one JSON line):");
|
|
625
|
+
for (const [question, command] of entry.examples) lines.push(` ${question}:`, ` ${command}`);
|
|
491
626
|
}
|
|
492
627
|
} else if (topic === "skill") {
|
|
493
628
|
lines.push(
|
|
@@ -501,10 +636,7 @@ export function helpText(topic) {
|
|
|
501
636
|
"",
|
|
502
637
|
"Options:",
|
|
503
638
|
);
|
|
504
|
-
|
|
505
|
-
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
506
|
-
lines.push(` ${flag.padEnd(18)}${option.summary}`);
|
|
507
|
-
}
|
|
639
|
+
lines.push(...optionLines(SKILL_OPTIONS, 18));
|
|
508
640
|
lines.push(
|
|
509
641
|
"",
|
|
510
642
|
"install never replaces an existing skill. update replaces only a skill this CLI",
|
|
@@ -533,10 +665,7 @@ export function helpText(topic) {
|
|
|
533
665
|
"",
|
|
534
666
|
"Options:",
|
|
535
667
|
);
|
|
536
|
-
|
|
537
|
-
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
538
|
-
lines.push(` ${flag.padEnd(18)}${option.summary}`);
|
|
539
|
-
}
|
|
668
|
+
lines.push(...optionLines(SKILL_OPTIONS, 18));
|
|
540
669
|
lines.push(
|
|
541
670
|
"",
|
|
542
671
|
"install never replaces an existing skill. update replaces only skills this CLI",
|
|
@@ -577,77 +706,93 @@ export function helpText(topic) {
|
|
|
577
706
|
'Run "metergraph help login" or "metergraph help logout" for sign in options.',
|
|
578
707
|
'Run "metergraph help verify" for exact-trace verification options.',
|
|
579
708
|
'Run "metergraph help COMMAND" for status, context, capabilities, usage, routes or traces.',
|
|
709
|
+
"",
|
|
710
|
+
...AGENT_PATH_HELP,
|
|
580
711
|
);
|
|
581
712
|
}
|
|
582
713
|
lines.push("", "Exit codes:");
|
|
583
714
|
for (const [outcome, code] of Object.entries(EXIT_CODES)) {
|
|
584
|
-
lines.push(` ${String(code).padEnd(3)}${outcome.padEnd(25)}${EXIT_CODE_MEANINGS[outcome]}`);
|
|
715
|
+
lines.push(` ${String(code).padEnd(3)}${outcome.padEnd(25)}${style.dim(EXIT_CODE_MEANINGS[outcome])}`);
|
|
585
716
|
}
|
|
586
|
-
|
|
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`;
|
|
587
719
|
}
|
|
588
720
|
|
|
589
|
-
export function skillText(result, message) {
|
|
721
|
+
export function skillText(result, message, style = PLAIN) {
|
|
590
722
|
const report = result.data;
|
|
591
723
|
const label = SKILL_CLIENTS[report.client]?.label ?? HANDOFF_SKILL_CLIENTS[report.client];
|
|
592
|
-
const
|
|
593
|
-
|
|
724
|
+
const verb = result.command === "skill update" ? "updated" : "installed";
|
|
725
|
+
const lines = [
|
|
726
|
+
result.ok
|
|
727
|
+
? headline(style, "ok", `Metergraph skill ${report.status === "unchanged" ? "already up to date" : verb} for ${label}`)
|
|
728
|
+
: headline(style, toneFor(result.outcome), `Metergraph skill not ${verb} for ${label}`, result.outcome),
|
|
729
|
+
"",
|
|
730
|
+
];
|
|
731
|
+
const rows = [["Runtime", report.runtime], ["Path", report.path]];
|
|
594
732
|
if (result.ok) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
"Authenticated
|
|
600
|
-
"",
|
|
601
|
-
message,
|
|
602
|
-
`Next: ${report.next_action.message}`,
|
|
733
|
+
rows.push(
|
|
734
|
+
["Status", report.status],
|
|
735
|
+
["Source", `${report.source.revision} ${style.dim(`(sha256 ${report.source.sha256})`)}`],
|
|
736
|
+
["Discovery", `pending until ${label} loads the skill`],
|
|
737
|
+
["Authenticated", "no"],
|
|
603
738
|
);
|
|
604
|
-
} else {
|
|
605
|
-
lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`, message);
|
|
606
|
-
if (report.next_action?.kind === "connection_guide") {
|
|
607
|
-
lines.push(`Connection guide: ${report.next_action.url}`);
|
|
608
|
-
}
|
|
609
739
|
}
|
|
610
|
-
|
|
740
|
+
lines.push(...fields(style, rows), "");
|
|
741
|
+
if (message) lines.push(...paragraph(style, message), "");
|
|
742
|
+
lines.push(...steps(style, [
|
|
743
|
+
["", result.ok ? report.next_action.message : null],
|
|
744
|
+
["Connection guide", !result.ok && report.next_action?.kind === "connection_guide" ? report.next_action.url : null],
|
|
745
|
+
]));
|
|
746
|
+
return render(lines);
|
|
611
747
|
}
|
|
612
748
|
|
|
613
|
-
export function skillsText(result, message) {
|
|
749
|
+
export function skillsText(result, message, style = PLAIN) {
|
|
614
750
|
const report = result.data;
|
|
615
751
|
const lines = [];
|
|
752
|
+
const source = report.source
|
|
753
|
+
? ["Source", `${report.source.repository} ${style.dim(`at ${report.source.commit.slice(0, 12)}`)}`]
|
|
754
|
+
: ["Source", null];
|
|
616
755
|
if (result.command === "skills list") {
|
|
617
|
-
lines.push(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
756
|
+
lines.push(
|
|
757
|
+
result.ok
|
|
758
|
+
? headline(style, "info", "Metergraph skills bundled with this CLI")
|
|
759
|
+
: headline(style, toneFor(result.outcome), "Could not list Metergraph skills", result.outcome),
|
|
760
|
+
"",
|
|
761
|
+
...fields(style, [source]),
|
|
762
|
+
);
|
|
763
|
+
if (!result.ok) return render([...lines, "", ...paragraph(style, message)]);
|
|
623
764
|
for (const skill of report.skills) {
|
|
624
|
-
lines.push("", `${skill.name}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
765
|
+
lines.push("", `${section(style, skill.name)} ${style.dim(skill.revision)}`);
|
|
766
|
+
lines.push(...fields(style, Object.entries(skill.installs).map(([client, state]) =>
|
|
767
|
+
[SKILL_CLIENTS[client].label, state === "installed" ? style.green(SKILLS_STATE_LABELS[state]) : (SKILLS_STATE_LABELS[state] ?? state)])));
|
|
628
768
|
}
|
|
629
|
-
return
|
|
769
|
+
return render(lines);
|
|
630
770
|
}
|
|
631
771
|
const label = SKILL_CLIENTS[report.client]?.label ?? HANDOFF_SKILL_CLIENTS[report.client] ?? report.client;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
772
|
+
const verb = result.command === "skills update" ? "updated" : "installed";
|
|
773
|
+
lines.push(
|
|
774
|
+
result.ok
|
|
775
|
+
? headline(style, "ok", `Metergraph skills ${verb} for ${label}`)
|
|
776
|
+
: headline(style, toneFor(result.outcome), `Metergraph skills not ${verb} for ${label}`, result.outcome),
|
|
777
|
+
"",
|
|
778
|
+
...fields(style, [["Runtime", report.runtime], source]),
|
|
779
|
+
);
|
|
780
|
+
if (report.skills.length > 0) {
|
|
781
|
+
lines.push("", ...fields(style, report.skills.map((skill) => {
|
|
782
|
+
const status = skill.status === "failed" ? style.red(`failed (${skill.reason})`) : skill.status;
|
|
783
|
+
return [skill.name, `${status} ${style.dim(skill.path ?? "")}`];
|
|
784
|
+
})));
|
|
637
785
|
}
|
|
638
786
|
if (result.ok) {
|
|
639
|
-
lines.push(
|
|
640
|
-
|
|
641
|
-
"Authenticated: no",
|
|
642
|
-
"",
|
|
643
|
-
`Next: ${report.next_action.message}`,
|
|
644
|
-
);
|
|
787
|
+
lines.push("", ...fields(style, [["Discovery", `pending until ${label} loads the skills`], ["Authenticated", "no"]]),
|
|
788
|
+
"", ...steps(style, [["", report.next_action.message]]));
|
|
645
789
|
} else {
|
|
646
|
-
lines.push("",
|
|
647
|
-
|
|
648
|
-
|
|
790
|
+
lines.push("", ...paragraph(style, message), "", ...steps(style, [
|
|
791
|
+
["", report.next_action?.kind === "plugin_marketplace" ? report.next_action.message : null],
|
|
792
|
+
["Connection guide", report.next_action?.kind === "connection_guide" ? report.next_action.url : null],
|
|
793
|
+
]));
|
|
649
794
|
}
|
|
650
|
-
return
|
|
795
|
+
return render(lines);
|
|
651
796
|
}
|
|
652
797
|
|
|
653
798
|
export function versionData() {
|
|
@@ -693,35 +838,48 @@ export function doctorMessage(outcome, reason) {
|
|
|
693
838
|
return `${detail.charAt(0).toUpperCase()}${detail.slice(1)}.`;
|
|
694
839
|
}
|
|
695
840
|
|
|
696
|
-
|
|
841
|
+
const CHECK_MARKS = { pass: ["✓", "green"], fail: ["✗", "red"], skipped: ["-", "dim"] };
|
|
842
|
+
|
|
843
|
+
export function doctorText(result, style = PLAIN) {
|
|
697
844
|
const report = result.data;
|
|
698
|
-
const
|
|
845
|
+
const title = result.outcome === "authentication_required"
|
|
846
|
+
? "Service is reachable and requires sign in"
|
|
847
|
+
: result.ok ? "Service is healthy" : "Service check failed";
|
|
848
|
+
const lines = [
|
|
849
|
+
headline(style, toneFor(result.outcome), title, result.outcome),
|
|
850
|
+
"",
|
|
851
|
+
...fields(style, [["Origin", report.origin]]),
|
|
852
|
+
` ${style.dim("Read only, no credentials sent")}`,
|
|
853
|
+
"",
|
|
854
|
+
];
|
|
855
|
+
const width = Math.max(...report.checks.map((check) => CHECK_LABELS[check.name].length));
|
|
699
856
|
for (const check of report.checks) {
|
|
700
|
-
|
|
857
|
+
const [mark, color] = CHECK_MARKS[check.result] ?? ["?", "dim"];
|
|
858
|
+
let line = ` ${style[color](mark)} ${CHECK_LABELS[check.name].padEnd(width)} ${style.dim(check.path)}`;
|
|
701
859
|
if (check.name === "deployment" && report.deployment_profile !== null) {
|
|
702
|
-
line +=
|
|
860
|
+
line += ` ${report.deployment_profile}`;
|
|
703
861
|
} else if (check.reason !== null) {
|
|
704
|
-
line +=
|
|
862
|
+
line += ` ${REASON_TEXT[check.reason] ?? check.reason}`;
|
|
705
863
|
}
|
|
706
864
|
lines.push(line);
|
|
707
865
|
}
|
|
708
|
-
lines.push("",
|
|
709
|
-
if (result.error !== null) lines.push(result.error.message);
|
|
866
|
+
if (result.error !== null) lines.push("", ...paragraph(style, result.error.message));
|
|
710
867
|
if (result.outcome === "authentication_required") {
|
|
711
|
-
lines.push(
|
|
712
|
-
"
|
|
713
|
-
CONNECTION_GUIDE_URL,
|
|
714
|
-
);
|
|
868
|
+
lines.push("", ...steps(style, [
|
|
869
|
+
["Sign in from this machine", "metergraph login --runtime local"],
|
|
870
|
+
["Connect an application", CONNECTION_GUIDE_URL],
|
|
871
|
+
]));
|
|
715
872
|
}
|
|
716
|
-
return
|
|
873
|
+
return render(lines);
|
|
717
874
|
}
|
|
718
875
|
|
|
719
876
|
// Fixed text for every login, logout and session reason. Reasons shared
|
|
720
877
|
// with doctor fall back to its wording.
|
|
721
878
|
const AUTH_MESSAGES = {
|
|
722
879
|
runtime_not_supported:
|
|
723
|
-
"
|
|
724
|
-
"
|
|
880
|
+
"--runtime says where this terminal runs, not which Metergraph service you sign in to. " +
|
|
881
|
+
"A cloud runtime is a remote sandbox, and sign in needs a browser on the same machine as the CLI, " +
|
|
882
|
+
"so it cannot finish there. Nothing was written.",
|
|
725
883
|
ssh_session:
|
|
726
884
|
"This is a remote shell session. A browser on your own machine cannot reach this machine's loopback " +
|
|
727
885
|
"address, so sign in cannot finish here. Run login where your browser runs. Nothing was written.",
|
|
@@ -823,36 +981,73 @@ export function authMessage(outcome, reason) {
|
|
|
823
981
|
return AUTH_MESSAGES[reason] ?? doctorMessage(outcome, reason);
|
|
824
982
|
}
|
|
825
983
|
|
|
826
|
-
|
|
984
|
+
// Short headlines for the reasons people meet most. Others use a generic one.
|
|
985
|
+
const AUTH_TITLES = {
|
|
986
|
+
runtime_not_supported: "Sign in cannot run from a cloud runtime",
|
|
987
|
+
ssh_session: "Sign in cannot run over SSH",
|
|
988
|
+
cloud_workspace: "Sign in cannot run in a cloud development environment",
|
|
989
|
+
ci_environment: "Sign in cannot run in CI",
|
|
990
|
+
browser_approval_required: "Waiting for approval in the browser",
|
|
991
|
+
access_denied: "Sign in was declined in the browser",
|
|
992
|
+
timeout: "Sign in timed out",
|
|
993
|
+
cancelled: "Sign in was cancelled",
|
|
994
|
+
bound_to_other_origin: "This project is bound to a different origin",
|
|
995
|
+
bound_to_other_workspace: "This project is bound to a different workspace",
|
|
996
|
+
not_signed_in: "This project is not signed in",
|
|
997
|
+
revocation_unconfirmed: "Signed out locally; revocation not confirmed",
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
export function authText(result, message, style = PLAIN) {
|
|
827
1001
|
const report = result.data;
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
lines.push(
|
|
836
|
-
`Status: ${status}`,
|
|
837
|
-
`Scopes: ${report.scopes.join(" ")}`,
|
|
838
|
-
`Binding: ${report.binding}`,
|
|
839
|
-
`Grant storage: ${report.credential_protection === "dpapi" ? "Windows DPAPI, current user" : "owner-only file"}`,
|
|
840
|
-
);
|
|
841
|
-
if (report.previous_grant_revocation === "unconfirmed") {
|
|
842
|
-
lines.push("The previous grant was removed locally; the service did not confirm its revocation.");
|
|
843
|
-
}
|
|
844
|
-
}
|
|
1002
|
+
const login = result.command === "login";
|
|
1003
|
+
const reason = result.error?.reason ?? null;
|
|
1004
|
+
let title;
|
|
1005
|
+
if (result.ok && login) {
|
|
1006
|
+
title = { signed_in: "Signed in", reused: "Already signed in", reconnected: "Reconnected" }[report.status];
|
|
1007
|
+
} else if (result.ok) {
|
|
1008
|
+
title = report.binding === "none" ? "This project was not signed in" : "Signed out";
|
|
845
1009
|
} else {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1010
|
+
title = AUTH_TITLES[reason] ?? (login ? "Sign in did not complete" : "Sign out did not complete");
|
|
1011
|
+
}
|
|
1012
|
+
const lines = [headline(style, toneFor(result.outcome), title, result.outcome), ""];
|
|
1013
|
+
const rows = [
|
|
1014
|
+
["Origin", report.origin],
|
|
1015
|
+
["Workspace", report.workspace?.id ?? null],
|
|
1016
|
+
["Profile", report.deployment_profile || null],
|
|
1017
|
+
];
|
|
1018
|
+
if (login && result.ok) {
|
|
1019
|
+
rows.push(
|
|
1020
|
+
["Scopes", report.scopes.join(" ")],
|
|
1021
|
+
["Binding", report.binding],
|
|
1022
|
+
["Grant storage", report.credential_protection === "dpapi" ? "Windows DPAPI, current user" : "owner-only file"],
|
|
1023
|
+
);
|
|
1024
|
+
} else if (!login) {
|
|
1025
|
+
rows.push(
|
|
1026
|
+
["Saved grant", report.local_credentials],
|
|
1027
|
+
["Binding", report.binding],
|
|
1028
|
+
["Server revocation", report.revocation.replace("_", " ")],
|
|
850
1029
|
);
|
|
851
|
-
if (result.ok && report.binding === "none") lines.push("This project was not signed in.");
|
|
852
1030
|
}
|
|
853
|
-
|
|
1031
|
+
lines.push(...fields(style, rows), "");
|
|
1032
|
+
if (login && result.ok && report.previous_grant_revocation === "unconfirmed") {
|
|
1033
|
+
lines.push(...paragraph(style, "The previous grant was removed locally; the service did not confirm its revocation."), "");
|
|
1034
|
+
}
|
|
1035
|
+
if (!result.ok) lines.push(...paragraph(style, message), "");
|
|
854
1036
|
const next = report.next_action;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1037
|
+
const guide = next?.url ?? null;
|
|
1038
|
+
// A remote handoff names the Keys page where a person creates the
|
|
1039
|
+
// Metadata key the agent then reads from METERGRAPH_AGENT_TOKEN.
|
|
1040
|
+
const keys = next?.kind === "use_agent_key"
|
|
1041
|
+
? next.person_steps.find((step) => step.action === "create_metadata_key")?.url ?? null
|
|
1042
|
+
: null;
|
|
1043
|
+
if (reason === "runtime_not_supported") {
|
|
1044
|
+
lines.push(...steps(style, [
|
|
1045
|
+
["Signing in from this machine?", style.bold("metergraph login --runtime local")],
|
|
1046
|
+
["Connecting a cloud agent?", guide],
|
|
1047
|
+
["Metadata key for the agent", keys],
|
|
1048
|
+
]));
|
|
1049
|
+
} else {
|
|
1050
|
+
lines.push(...steps(style, [["", next?.message ?? null], ["Keys page", keys], ["Connection guide", guide]]));
|
|
1051
|
+
}
|
|
1052
|
+
return render(lines);
|
|
858
1053
|
}
|