metergraph-cli 0.0.0-stage → 0.2.0-preview.0

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/output.js ADDED
@@ -0,0 +1,726 @@
1
+ import {
2
+ CONNECTION_GUIDE_URL,
3
+ DEFAULT_ORIGIN,
4
+ DEFAULT_TIMEOUT_MS,
5
+ EXIT_CODE_MEANINGS,
6
+ EXIT_CODES,
7
+ HANDOFF_SKILL_CLIENTS,
8
+ LOGIN_DEFAULT_TIMEOUT_MS,
9
+ LOGIN_MAX_TIMEOUT_MS,
10
+ LOGIN_MIN_TIMEOUT_MS,
11
+ LOGIN_RUNTIMES,
12
+ MAX_TIMEOUT_MS,
13
+ METADATA_SCOPE,
14
+ MIN_TIMEOUT_MS,
15
+ PACKAGE_NAME,
16
+ READ_DEFAULT_DAYS,
17
+ READ_DEFAULT_LIMIT,
18
+ READ_DEFAULT_TIMEOUT_MS,
19
+ READ_MAX_DAYS,
20
+ READ_MAX_LIMIT,
21
+ READ_MAX_TIMEOUT_MS,
22
+ READ_MIN_TIMEOUT_MS,
23
+ SCHEMA_VERSION,
24
+ TRACES_DEFAULT_LIMIT,
25
+ SKILL_CLIENTS,
26
+ SKILL_RUNTIMES,
27
+ SUPPORTED_PROFILES,
28
+ VERSION,
29
+ } from "./constants.js";
30
+
31
+ // Every JSON result, success or failure, has the same top-level keys:
32
+ // schema_version, command, ok, outcome, exit_code, data, error
33
+ // error is null when ok is true, otherwise { code, reason, message } where
34
+ // code equals outcome. All strings come from this package, never from input
35
+ // or from a server.
36
+ export function envelope({ command, outcome, data = null, reason = null, message = null }) {
37
+ const ok = outcome === "ok";
38
+ return {
39
+ schema_version: SCHEMA_VERSION,
40
+ command,
41
+ ok,
42
+ outcome,
43
+ exit_code: EXIT_CODES[outcome],
44
+ data,
45
+ error: ok ? null : { code: outcome, reason, message },
46
+ };
47
+ }
48
+
49
+ export function toJsonLine(result) {
50
+ return `${JSON.stringify(result)}\n`;
51
+ }
52
+
53
+ const DOCTOR_OPTIONS = [
54
+ {
55
+ name: "--url",
56
+ value: "ORIGIN",
57
+ summary: `Origin to probe. Default ${DEFAULT_ORIGIN}.`,
58
+ },
59
+ {
60
+ name: "--timeout-ms",
61
+ value: "N",
62
+ summary: `Total time allowed for the whole probe, ${MIN_TIMEOUT_MS} to ${MAX_TIMEOUT_MS}. Default ${DEFAULT_TIMEOUT_MS}.`,
63
+ },
64
+ { name: "--json", value: null, summary: "Print one JSON line on stdout." },
65
+ ];
66
+
67
+ const SKILL_OPTIONS = [
68
+ {
69
+ name: "--client",
70
+ value: "CLIENT",
71
+ summary: "Required. codex (.agents/skills), claude (.claude/skills) or cursor (.cursor/skills).",
72
+ },
73
+ {
74
+ name: "--runtime",
75
+ value: "RUNTIME",
76
+ summary: "Required. local or cloud: where the client runs. Recorded, not detected.",
77
+ },
78
+ { name: "--project", value: "DIR", summary: "Existing project directory. Default: the current directory." },
79
+ { name: "--json", value: null, summary: "Print one JSON line on stdout." },
80
+ ];
81
+
82
+ const SKILL_USAGE = [
83
+ "metergraph skill install --client CLIENT --runtime RUNTIME [--project DIR] [--json]",
84
+ "metergraph skill update --client CLIENT --runtime RUNTIME [--project DIR] [--json]",
85
+ ];
86
+
87
+ const CONFIG_DIR_OPTION = {
88
+ name: "--config-dir",
89
+ value: "DIR",
90
+ summary:
91
+ "Private per-user directory for the saved grant. Default: METERGRAPH_CONFIG_DIR, " +
92
+ "else ~/.config/metergraph (POSIX) or AppData\\Roaming\\Metergraph (Windows).",
93
+ };
94
+
95
+ const LOGIN_OPTIONS = [
96
+ {
97
+ name: "--runtime",
98
+ value: "RUNTIME",
99
+ summary: "Required. local: the browser runs on this machine. cloud and cloud-no-shell get a handoff.",
100
+ },
101
+ { name: "--url", value: "ORIGIN", summary: `Origin to sign in to. Default ${DEFAULT_ORIGIN}.` },
102
+ {
103
+ name: "--workspace",
104
+ value: "UUID",
105
+ summary: "Expected workspace ID. Sign in fails unless the browser grants exactly this workspace.",
106
+ },
107
+ { name: "--project", value: "DIR", summary: "Existing project directory to bind. Default: the current directory." },
108
+ CONFIG_DIR_OPTION,
109
+ {
110
+ name: "--timeout-ms",
111
+ value: "N",
112
+ summary: `Time to wait for the browser, ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}. Default ${LOGIN_DEFAULT_TIMEOUT_MS}.`,
113
+ },
114
+ { name: "--signup", value: null, summary: "Start at the hosted sign up page. Managed service only." },
115
+ { name: "--no-browser", value: null, summary: "Print the sign in URL on stderr instead of opening a browser. Not with --json." },
116
+ { name: "--reconnect", value: null, summary: "Allow switching a bound project to another origin or workspace." },
117
+ { name: "--json", value: null, summary: "Print one JSON line on stdout." },
118
+ ];
119
+
120
+ const LOGOUT_OPTIONS = [
121
+ { name: "--project", value: "DIR", summary: "Existing project directory. Default: the current directory." },
122
+ CONFIG_DIR_OPTION,
123
+ { name: "--json", value: null, summary: "Print one JSON line on stdout." },
124
+ ];
125
+
126
+ const LOGIN_USAGE =
127
+ "metergraph login --runtime local [--url ORIGIN] [--workspace UUID] [--project DIR] [--config-dir DIR] " +
128
+ "[--timeout-ms N] [--signup] [--no-browser] [--reconnect] [--json]";
129
+ const LOGOUT_USAGE = "metergraph logout [--project DIR] [--config-dir DIR] [--json]";
130
+ const SETUP_USAGE = "metergraph setup --runtime local (--client codex|claude|cursor | --skip-skill) " +
131
+ "[--url ORIGIN] [--workspace UUID] [--project DIR] [--config-dir DIR] [--env-file .env] " +
132
+ "[--deployment managed|customer-local|byoc|oss] [--confirm-prerequisites] [--agent-token-file FILE] " +
133
+ "[--timeout-ms N] [--signup] [--reconnect] [--no-browser] [--repair] [--json]";
134
+ const JSON_OPTION = { name: "--json", value: null, summary: "Print one JSON line on stdout." };
135
+ const VERIFY_USAGE =
136
+ "metergraph verify (--trace-id ID | --request-id ID) --since TIME --until TIME " +
137
+ "[--source application|synthetic|demo|import|unspecified] [--days N] [--timeout-ms N] " +
138
+ "[--poll-ms N] [--max-attempts N] [--open] [--no-browser] [--project DIR] [--config-dir DIR] [--json]";
139
+ const VERIFY_OPTIONS = [
140
+ { name: "--trace-id", value: "ID", summary: "Exact trace ID from the application invocation." },
141
+ { name: "--request-id", value: "ID", summary: "Exact request ID, used only when a trace ID is unavailable." },
142
+ { name: "--since", value: "TIME", summary: "Start of the invocation in ISO 8601 form." },
143
+ { name: "--until", value: "TIME", summary: "End of the invocation in ISO 8601 form." },
144
+ { name: "--source", value: "SOURCE", summary: "Explicit provenance label. Default unspecified; this is a caller claim." },
145
+ { name: "--days", value: "N", summary: "Metadata lookback, 1 to 90 days. Derived from --since by default." },
146
+ { name: "--timeout-ms", value: "N", summary: "Total verification deadline, 100 to 60000. Default 30000." },
147
+ { name: "--poll-ms", value: "N", summary: "Poll interval, 100 to 10000. Default 1000." },
148
+ { name: "--max-attempts", value: "N", summary: "Maximum Metadata queries, 1 to 60. Default 30." },
149
+ { name: "--open", value: null, summary: "Open only a verified server link bound to the signed in workspace." },
150
+ { name: "--no-browser", value: null, summary: "Never launch a browser." },
151
+ { name: "--project", value: "DIR", summary: "Signed in project directory. Default current directory." },
152
+ CONFIG_DIR_OPTION,
153
+ JSON_OPTION,
154
+ ];
155
+
156
+ const READ_BASE_OPTIONS = [
157
+ { name: "--project", value: "DIR", summary: "Signed in project directory. Default: the current directory." },
158
+ CONFIG_DIR_OPTION,
159
+ {
160
+ name: "--timeout-ms",
161
+ value: "N",
162
+ summary:
163
+ `Total time for every request of the command, ${READ_MIN_TIMEOUT_MS} to ${READ_MAX_TIMEOUT_MS}. ` +
164
+ `Default ${READ_DEFAULT_TIMEOUT_MS}.`,
165
+ },
166
+ ];
167
+ const DAYS_OPTION = {
168
+ name: "--days",
169
+ value: "N",
170
+ summary: `Window of the last N days, 1 to ${READ_MAX_DAYS}. Default ${READ_DEFAULT_DAYS}.`,
171
+ };
172
+ const limitOption = (fallback, extra = "") => ({
173
+ name: "--limit",
174
+ value: "N",
175
+ summary: `Rows to return, 1 to ${READ_MAX_LIMIT}. Default ${fallback}.${extra}`,
176
+ });
177
+ const READ_BASE_USAGE = "[--project DIR] [--config-dir DIR] [--timeout-ms N] [--json]";
178
+ const REFUSED_NOTE =
179
+ "--environment, --workload, --since, --until, --sql, --query, --content, --include-content, --debug and --replay " +
180
+ "are recognized and refused with exit code 6 before any request.";
181
+
182
+ // The read commands. Each uses the project's saved Metadata grant, makes GET
183
+ // requests to fixed paths on the bound origin and never opens a browser.
184
+ export const READ_HELP = Object.freeze([
185
+ {
186
+ name: "status",
187
+ usage: `metergraph status ${READ_BASE_USAGE}`,
188
+ summary:
189
+ "Show whether this project is configured, the service is reachable and reports the bound deployment " +
190
+ "profile, and the grant is verified for the bound workspace, with its Metadata capabilities. " +
191
+ "Never claims application traffic is verified.",
192
+ options: [...READ_BASE_OPTIONS, JSON_OPTION],
193
+ },
194
+ {
195
+ name: "context",
196
+ usage: `metergraph context ${READ_BASE_USAGE}`,
197
+ summary: "Show the verified workspace: ID, slug, name, Metadata retention and access scope.",
198
+ options: [...READ_BASE_OPTIONS, JSON_OPTION],
199
+ },
200
+ {
201
+ name: "capabilities",
202
+ usage: `metergraph capabilities ${READ_BASE_USAGE}`,
203
+ summary: "Show which agent reads the service offers this grant, their privacy class and the service's bounds.",
204
+ options: [...READ_BASE_OPTIONS, JSON_OPTION],
205
+ },
206
+ {
207
+ name: "usage",
208
+ usage: `metergraph usage [--days N] [--limit N] ${READ_BASE_USAGE}`,
209
+ summary: "Show daily calls, errors, cost, tokens and latency per route for a recent window. Metadata only.",
210
+ options: [...READ_BASE_OPTIONS, DAYS_OPTION, limitOption(READ_DEFAULT_LIMIT), JSON_OPTION],
211
+ },
212
+ {
213
+ name: "routes",
214
+ usage: `metergraph routes [--limit N] ${READ_BASE_USAGE}`,
215
+ summary:
216
+ "List routes with call counts and evaluation contract versions. Descriptions, constraints and contract " +
217
+ "bodies are not printed.",
218
+ options: [
219
+ ...READ_BASE_OPTIONS,
220
+ limitOption(
221
+ READ_DEFAULT_LIMIT,
222
+ " The service has no route limit, so extra rows are cut locally and reported as truncated.",
223
+ ),
224
+ JSON_OPTION,
225
+ ],
226
+ },
227
+ {
228
+ name: "traces",
229
+ usage:
230
+ `metergraph traces [--days N] [--limit N] [--route NAME] [--status success|error] ` +
231
+ `[--cursor CURSOR] ${READ_BASE_USAGE}`,
232
+ summary:
233
+ "List one page of trace metadata: status, span count, tokens, cost, routes, providers and models. " +
234
+ "No prompts, responses or tool calls.",
235
+ options: [
236
+ ...READ_BASE_OPTIONS,
237
+ DAYS_OPTION,
238
+ limitOption(TRACES_DEFAULT_LIMIT),
239
+ { name: "--route", value: "NAME", summary: "Only traces that include this route." },
240
+ { name: "--status", value: "STATUS", summary: "Only success or error traces." },
241
+ {
242
+ name: "--cursor",
243
+ value: "CURSOR",
244
+ summary: "next_cursor from an earlier result, to read the next page. Pages are never fetched automatically.",
245
+ },
246
+ JSON_OPTION,
247
+ ],
248
+ },
249
+ ]);
250
+
251
+ export function helpData(topic) {
252
+ return {
253
+ topic,
254
+ usage: [
255
+ "metergraph --help [--json]",
256
+ "metergraph --version [--json]",
257
+ "metergraph doctor [--url ORIGIN] [--timeout-ms N] [--json]",
258
+ ...SKILL_USAGE,
259
+ LOGIN_USAGE,
260
+ LOGOUT_USAGE,
261
+ SETUP_USAGE,
262
+ VERIFY_USAGE,
263
+ ...READ_HELP.map((entry) => entry.usage),
264
+ ],
265
+ commands: [
266
+ {
267
+ name: "doctor",
268
+ summary:
269
+ "Check that a Metergraph service is reachable, healthy and supported. Read only, sends no credentials.",
270
+ options: DOCTOR_OPTIONS,
271
+ },
272
+ {
273
+ name: "skill install",
274
+ summary:
275
+ "Copy the Metergraph skill bundled with this CLI into one client's project skill directory. " +
276
+ "Never replaces a skill it did not install. No network requests, no sign in.",
277
+ options: SKILL_OPTIONS,
278
+ },
279
+ {
280
+ name: "skill update",
281
+ summary:
282
+ "Replace a skill this CLI installed, and that is unchanged since, with the bundled revision.",
283
+ options: SKILL_OPTIONS,
284
+ },
285
+ {
286
+ name: "login",
287
+ summary:
288
+ "Sign in through the browser and bind this project to one verified workspace with a Metadata-only grant. " +
289
+ "Does not create an application ingest key; no manual API key is required.",
290
+ options: LOGIN_OPTIONS,
291
+ },
292
+ {
293
+ name: "logout",
294
+ summary:
295
+ "Ask the service to revoke this project's grant, then remove the saved grant and the project binding.",
296
+ options: LOGOUT_OPTIONS,
297
+ },
298
+ {
299
+ name: "setup",
300
+ summary: "Guide browser sign in and workspace choice, approve an ingest-only key, and install the selected client skill. Does not verify application traffic.",
301
+ options: [
302
+ { name: "--runtime", value: "RUNTIME", summary: "Required. local only; other runtimes get a handoff." },
303
+ { name: "--url", value: "ORIGIN", summary: "Deployment origin. Defaults to an existing project binding, else the hosted origin." },
304
+ { name: "--workspace", value: "UUID", summary: "Expected workspace; the browser must approve this exact workspace." },
305
+ { name: "--project", value: "DIR", summary: "Project directory. Default: current directory." },
306
+ CONFIG_DIR_OPTION,
307
+ { name: "--env-file", value: "FILE", summary: "Project-relative env file. Default: .env." },
308
+ { name: "--client", value: "CLIENT", summary: "Install the bundled skill for codex, claude or cursor." },
309
+ { name: "--skip-skill", value: null, summary: "Explicitly leave client skill installation pending." },
310
+ { name: "--timeout-ms", value: "N", summary: "Time to wait for browser approval." },
311
+ { name: "--signup", value: null, summary: "Start at hosted sign up when the project needs login." },
312
+ { name: "--reconnect", value: null, summary: "Permit switching an existing project binding." },
313
+ { name: "--no-browser", value: null, summary: "Print approval URL on stderr; requires terminal output." },
314
+ { name: "--repair", value: null, summary: "Explicitly approve replacement of an acknowledged key that no longer verifies." },
315
+ { name: "--deployment", value: "MODEL", summary: "managed, customer-local, byoc or oss. Default managed." },
316
+ { name: "--confirm-prerequisites", value: null, summary: "Attest deployment prerequisites are met; it does not verify bundle publication." },
317
+ { name: "--agent-token-file", value: "FILE", summary: "Optional separate Metadata token for local/BYOC; required for OSS handoff." },
318
+ JSON_OPTION,
319
+ ],
320
+ },
321
+ {
322
+ name: "verify",
323
+ summary: "Poll Metadata for one exact trace in an explicit invocation window. Never sends application traffic.",
324
+ options: VERIFY_OPTIONS,
325
+ },
326
+ ...READ_HELP.map(({ name, summary, options }) => ({ name, summary, options })),
327
+ ],
328
+ skill_clients: Object.keys(SKILL_CLIENTS),
329
+ skill_runtimes: [...SKILL_RUNTIMES],
330
+ login_runtimes: [...LOGIN_RUNTIMES],
331
+ supported_profiles: [...SUPPORTED_PROFILES],
332
+ exit_codes: Object.entries(EXIT_CODES).map(([outcome, code]) => ({
333
+ code,
334
+ outcome,
335
+ meaning: EXIT_CODE_MEANINGS[outcome],
336
+ })),
337
+ };
338
+ }
339
+
340
+ export function helpText(topic) {
341
+ const lines = [];
342
+ if (topic === "doctor") {
343
+ lines.push(
344
+ "Usage: metergraph doctor [--url ORIGIN] [--timeout-ms N] [--json]",
345
+ "",
346
+ "Makes unauthenticated, read-only GET requests to /healthz, /v1/deployment and",
347
+ "/v1/agent/capabilities on one origin. Sends no credentials and follows no redirects.",
348
+ "",
349
+ "Options:",
350
+ );
351
+ for (const option of DOCTOR_OPTIONS) {
352
+ const flag = option.value ? `${option.name} ${option.value}` : option.name;
353
+ lines.push(` ${flag.padEnd(18)}${option.summary}`);
354
+ }
355
+ lines.push(
356
+ "",
357
+ "ORIGIN must be a bare https origin such as https://metergraph.example.com.",
358
+ "Plain http is accepted only for localhost, 127.0.0.1 and [::1].",
359
+ "",
360
+ "A healthy, supported service that requires sign in exits with code 3.",
361
+ "Doctor never sends credentials, so it never reports a connected workspace.",
362
+ );
363
+ } else if (topic === "setup") {
364
+ lines.push(`Usage: ${SETUP_USAGE}`, "", "Opens the service's browser sign in and workspace choice when needed.",
365
+ "The browser approves an ingest-only key for the exact workspace. The CLI stores it in",
366
+ "a private env file, confirms delivery, and installs the chosen client skill unless",
367
+ "--skip-skill is explicit. Non-hosted setup requires an explicit origin, workspace and",
368
+ "operator prerequisite attestation; OSS remains an operator handoff. No application",
369
+ "traffic is claimed until an exact instrumented invocation is separately verified.",
370
+ "", "Options:");
371
+ for (const option of helpData(null).commands.find((entry) => entry.name === "setup").options) {
372
+ const flag = option.value ? `${option.name} ${option.value}` : option.name;
373
+ lines.push(` ${flag.padEnd(25)}${option.summary}`);
374
+ }
375
+ } else if (topic === "login" || topic === "logout") {
376
+ const login = topic === "login";
377
+ lines.push(`Usage: ${login ? LOGIN_USAGE : LOGOUT_USAGE}`, "");
378
+ if (login) {
379
+ lines.push(
380
+ "Opens your browser on the service's own sign in and workspace consent pages, then",
381
+ "binds this project to the workspace you approve. The CLI receives a delegated grant",
382
+ `limited to the Metadata scope (${METADATA_SCOPE}) and verifies the workspace, profile`,
383
+ "and scope with the service before saving anything. Your browser keeps its own sign in.",
384
+ "The grant is saved in your private config directory; the project gets only",
385
+ ".metergraph/project.json, which holds no credentials.",
386
+ "",
387
+ "Login does not create an application ingest key, and no manual API key is required.",
388
+ "The service records the grant as a Metadata-only OAuth connection, separate from any",
389
+ "API or ingest key you manage. Login reads no telemetry or content and never asks for",
390
+ "Debug or Replay access. Rerunning it on a signed in project changes nothing.",
391
+ "",
392
+ );
393
+ } else {
394
+ lines.push(
395
+ "Asks the service to revoke this project's grant, removes the saved grant from your",
396
+ "config directory and removes .metergraph/project.json. Other files are kept.",
397
+ "If the service does not confirm revocation, local removal still happens and the",
398
+ "command exits with code 13.",
399
+ "",
400
+ );
401
+ }
402
+ lines.push("Options:");
403
+ for (const option of login ? LOGIN_OPTIONS : LOGOUT_OPTIONS) {
404
+ const flag = option.value ? `${option.name} ${option.value}` : option.name;
405
+ lines.push(` ${flag.padEnd(18)}${option.summary}`);
406
+ }
407
+ } else if (topic === "verify") {
408
+ lines.push(`Usage: ${VERIFY_USAGE}`, "", "Uses the saved Metadata grant to poll for one exact, processed trace.",
409
+ "The source label is supplied by the caller. A Metadata match alone does not prove application traffic.",
410
+ "Opening requires a server link that selects the verified workspace; older links get a manual handoff.",
411
+ "", "Options:");
412
+ for (const option of VERIFY_OPTIONS) {
413
+ const flag = option.value ? `${option.name} ${option.value}` : option.name;
414
+ lines.push(` ${flag.padEnd(22)}${option.summary}`);
415
+ }
416
+ } else if (READ_HELP.some((entry) => entry.name === topic)) {
417
+ const entry = READ_HELP.find((candidate) => candidate.name === topic);
418
+ lines.push(
419
+ `Usage: ${entry.usage}`,
420
+ "",
421
+ entry.summary,
422
+ "",
423
+ "Uses this project's saved Metadata grant (see \"metergraph help login\"). Sends GET requests only, to",
424
+ "fixed paths on the bound origin, follows no redirects and reads bounded responses. Never opens a",
425
+ "browser, signs in, writes project files, reads retained content, replays or calls a model provider.",
426
+ "It may refresh its own saved grant, and the service may record the access (for example last used",
427
+ "times); it never changes workspace configuration or telemetry and never sends ingest data.",
428
+ REFUSED_NOTE,
429
+ "",
430
+ "Options:",
431
+ );
432
+ for (const option of entry.options) {
433
+ const flag = option.value ? `${option.name} ${option.value}` : option.name;
434
+ lines.push(` ${flag.padEnd(18)}${option.summary}`);
435
+ }
436
+ } else if (topic === "skill") {
437
+ lines.push(
438
+ "Usage:",
439
+ ...SKILL_USAGE.map((usage) => ` ${usage}`),
440
+ "",
441
+ "Copies the Metergraph skill bundled with this CLI into one client's project skill",
442
+ "directory and records ownership in .metergraph/skill-installations.json. Writes",
443
+ "nothing else. Makes no network requests, does not sign in and does not configure",
444
+ "MCP, client settings, AGENTS.md or CLAUDE.md.",
445
+ "",
446
+ "Options:",
447
+ );
448
+ for (const option of SKILL_OPTIONS) {
449
+ const flag = option.value ? `${option.name} ${option.value}` : option.name;
450
+ lines.push(` ${flag.padEnd(18)}${option.summary}`);
451
+ }
452
+ lines.push(
453
+ "",
454
+ "install never replaces an existing skill. update replaces only a skill this CLI",
455
+ "installed and that is unchanged since. There is no force option.",
456
+ "",
457
+ "Claude Desktop (--client claude-desktop), ChatGPT (--client chatgpt) and cloud",
458
+ "runtimes without a shell (--runtime cloud-no-shell) cannot load project skill files.",
459
+ "They exit with code 6, point to the connection guide and write nothing.",
460
+ "",
461
+ "Discovery stays pending until the client itself loads the skill.",
462
+ );
463
+ } else {
464
+ lines.push(
465
+ `metergraph ${VERSION} (preview)`,
466
+ "",
467
+ "Usage:",
468
+ " metergraph --help [--json] Show this help",
469
+ " metergraph --version [--json] Show the CLI version",
470
+ " metergraph doctor [options] Check a Metergraph service, read only",
471
+ " metergraph skill install|update Install or update the agent skill in a project",
472
+ " metergraph login [options] Sign in and bind a project to a workspace",
473
+ " metergraph logout [options] Revoke and remove a project's sign in",
474
+ " metergraph setup [options] Approve and write a private ingest key",
475
+ " metergraph verify [options] Find one exact processed trace",
476
+ " metergraph status [options] Show configured, reachable and verified state",
477
+ " metergraph context [options] Show the verified workspace",
478
+ " metergraph capabilities [opts] Show the agent reads offered to this grant",
479
+ " metergraph usage [options] Daily usage per route, Metadata only",
480
+ " metergraph routes [options] Routes and evaluation contract versions",
481
+ " metergraph traces [options] One page of trace metadata",
482
+ "",
483
+ 'Run "metergraph help doctor" for doctor options.',
484
+ 'Run "metergraph help skill" for skill options.',
485
+ 'Run "metergraph help login" or "metergraph help logout" for sign in options.',
486
+ 'Run "metergraph help verify" for exact-trace verification options.',
487
+ 'Run "metergraph help COMMAND" for status, context, capabilities, usage, routes or traces.',
488
+ );
489
+ }
490
+ lines.push("", "Exit codes:");
491
+ for (const [outcome, code] of Object.entries(EXIT_CODES)) {
492
+ lines.push(` ${String(code).padEnd(3)}${outcome.padEnd(25)}${EXIT_CODE_MEANINGS[outcome]}`);
493
+ }
494
+ return `${lines.join("\n")}\n`;
495
+ }
496
+
497
+ export function skillText(result, message) {
498
+ const report = result.data;
499
+ const label = SKILL_CLIENTS[report.client]?.label ?? HANDOFF_SKILL_CLIENTS[report.client];
500
+ const lines = [`Metergraph ${result.command}: ${label}, ${report.runtime} runtime`];
501
+ if (report.path !== null) lines.push(`Path: ${report.path}`);
502
+ if (result.ok) {
503
+ lines.push(
504
+ `Status: ${report.status}`,
505
+ `Source revision: ${report.source.revision} (sha256 ${report.source.sha256})`,
506
+ `Discovery: pending until ${label} loads the skill`,
507
+ "Authenticated: no",
508
+ "",
509
+ message,
510
+ `Next: ${report.next_action.message}`,
511
+ );
512
+ } else {
513
+ lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`, message);
514
+ if (report.next_action?.kind === "connection_guide") {
515
+ lines.push(`Connection guide: ${report.next_action.url}`);
516
+ }
517
+ }
518
+ return `${lines.join("\n")}\n`;
519
+ }
520
+
521
+ export function versionData() {
522
+ return { name: PACKAGE_NAME, version: VERSION };
523
+ }
524
+
525
+ const CHECK_LABELS = {
526
+ health: "Health",
527
+ deployment: "Deployment profile",
528
+ capabilities: "Agent capabilities",
529
+ };
530
+
531
+ const REASON_TEXT = {
532
+ timeout: "the probe did not finish within the time limit",
533
+ dns_lookup_failed: "the host name could not be resolved",
534
+ connection_refused: "the connection was refused",
535
+ connection_reset: "the connection was closed unexpectedly",
536
+ host_unreachable: "the host could not be reached",
537
+ invalid_http_response: "the server did not send a valid HTTP response",
538
+ tls_error: "the TLS connection could not be verified",
539
+ network_error: "a network error occurred",
540
+ redirect: "the server answered with a redirect, which is never followed",
541
+ service_unavailable: "the service reported that it is unavailable",
542
+ server_error: "the service answered with a server error",
543
+ reported_unhealthy: "the service reported that it is not healthy",
544
+ unexpected_status: "the service answered with an unexpected status",
545
+ response_too_large: "the response was larger than the allowed limit",
546
+ invalid_response: "the response was not the expected JSON",
547
+ deployment_endpoint_missing:
548
+ "the service does not report a deployment profile; this preview supports only services that do",
549
+ unrecognized_profile: "the service reported a deployment profile this CLI does not support",
550
+ unexpected_auth_challenge: "the service did not ask for a bearer token",
551
+ unexpected_unauthenticated_access: "the service answered without asking for authentication",
552
+ bearer_token_required: "authentication required",
553
+ probe_incomplete: "the probe did not complete",
554
+ };
555
+
556
+ export function doctorMessage(outcome, reason) {
557
+ if (outcome === "authentication_required") {
558
+ return "The service is reachable and supported, and it requires authentication. No workspace is connected.";
559
+ }
560
+ const detail = REASON_TEXT[reason] ?? "the probe failed";
561
+ return `${detail.charAt(0).toUpperCase()}${detail.slice(1)}.`;
562
+ }
563
+
564
+ export function doctorText(result) {
565
+ const report = result.data;
566
+ const lines = ["Metergraph doctor (read only, no credentials sent)", `Origin: ${report.origin}`, ""];
567
+ for (const check of report.checks) {
568
+ let line = ` ${check.result.padEnd(8)}${CHECK_LABELS[check.name]} (${check.path})`;
569
+ if (check.name === "deployment" && report.deployment_profile !== null) {
570
+ line += `: ${report.deployment_profile}`;
571
+ } else if (check.reason !== null) {
572
+ line += `: ${REASON_TEXT[check.reason] ?? check.reason}`;
573
+ }
574
+ lines.push(line);
575
+ }
576
+ lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`);
577
+ if (result.error !== null) lines.push(result.error.message);
578
+ if (result.outcome === "authentication_required") {
579
+ lines.push(
580
+ "Doctor sends no credentials. To connect an application, follow the connection guide:",
581
+ CONNECTION_GUIDE_URL,
582
+ );
583
+ }
584
+ return `${lines.join("\n")}\n`;
585
+ }
586
+
587
+ // Fixed text for every login, logout and session reason. Reasons shared
588
+ // with doctor fall back to its wording.
589
+ const AUTH_MESSAGES = {
590
+ runtime_not_supported:
591
+ "Sign in needs a browser on the same machine as the CLI, so cloud runtimes cannot complete it. " +
592
+ "Follow the connection guide instead. Nothing was written.",
593
+ ssh_session:
594
+ "This is a remote shell session. A browser on your own machine cannot reach this machine's loopback " +
595
+ "address, so sign in cannot finish here. Run login where your browser runs. Nothing was written.",
596
+ cloud_workspace:
597
+ "This is a cloud development environment. Its loopback address is not reachable from your browser, " +
598
+ "so sign in cannot finish here. Follow the connection guide instead. Nothing was written.",
599
+ ci_environment:
600
+ "This is a CI environment, where no person can approve sign in in a browser. Nothing was written.",
601
+ no_browser_requires_terminal:
602
+ "--no-browser prints the sign in URL for a person to open, which --json cannot do. " +
603
+ "Run login without --json in a terminal. Nothing was done.",
604
+ bound_to_other_origin:
605
+ "This project is bound to a different origin. Nothing was changed. Use --reconnect to switch it.",
606
+ bound_to_other_workspace:
607
+ "This project is bound to a different workspace. Nothing was changed and no new grant was kept. " +
608
+ "Use --reconnect to switch it.",
609
+ signup_unsupported:
610
+ "Sign up is available only on the hosted managed service. For other deployments, ask the person " +
611
+ "who runs it for an invitation, then run login without --signup.",
612
+ access_denied: "Authorization was declined in the browser. Nothing was saved.",
613
+ authorization_error: "The service reported an authorization error. Nothing was saved.",
614
+ callback_invalid: "The browser returned an invalid authorization response. Nothing was saved.",
615
+ callback_issuer_mismatch: "The browser returned a response from a different issuer. Nothing was saved.",
616
+ timeout: "The operation did not finish within the time limit. Nothing was saved.",
617
+ cancelled: "Sign in was cancelled. Nothing was saved.",
618
+ browser_unavailable:
619
+ "The browser could not be opened. Run login again with --no-browser, without --json, and open the URL it prints.",
620
+ oauth_metadata_missing: "This service does not support CLI sign in yet. Nothing was written.",
621
+ metadata_scope_unsupported:
622
+ "This service does not offer Metadata-only access for the CLI yet. The CLI does not fall back to broader access.",
623
+ endpoint_not_allowed: "The service advertised an authorization endpoint outside its own origin or known paths.",
624
+ resource_mismatch: "The service's protected resource does not match this origin.",
625
+ issuer_mismatch: "The service's authorization issuer does not match this origin.",
626
+ pkce_unsupported: "The service does not support PKCE with S256.",
627
+ public_client_unsupported: "The service does not support public clients.",
628
+ revocation_unsupported: "The service does not advertise grant revocation for public clients.",
629
+ oauth_metadata_invalid: "The service's authorization metadata is not usable.",
630
+ registration_rejected: "The service refused to register the CLI as a client.",
631
+ registration_invalid: "The service's client registration did not match what the CLI requested.",
632
+ code_rejected: "The service refused the authorization code. Nothing was saved.",
633
+ token_response_invalid: "The service's token response is not usable. Nothing was saved.",
634
+ token_type_invalid: "The service did not issue a bearer grant. Nothing was saved.",
635
+ token_claims_invalid: "The issued grant does not name a valid workspace and expiry. Nothing was saved.",
636
+ client_mismatch: "The issued grant is for a different client. Nothing was saved.",
637
+ scope_mismatch: "The issued grant is not limited to exactly the Metadata scope. It was not kept.",
638
+ workspace_mismatch: "The browser granted a different workspace than --workspace. It was not kept.",
639
+ workspace_context_mismatch: "The service reported a different workspace than the grant names. It was not kept.",
640
+ profile_mismatch: "The service reported a different deployment profile than before sign in. It was not kept.",
641
+ deployment_profile_mismatch:
642
+ "The service or saved binding has a different deployment profile from the selected setup route. Nothing was changed.",
643
+ workspace_response_invalid: "The service's workspace response is not usable. Nothing was saved.",
644
+ capabilities_response_invalid: "The service's capabilities response is not usable. Nothing was saved.",
645
+ content_access_granted:
646
+ "The grant would allow content, replay or provider access, which login never accepts. It was not kept.",
647
+ access_rejected: "The service refused the new grant. Nothing was saved.",
648
+ invalid_project: "--project must name an existing directory.",
649
+ invalid_config_dir: "METERGRAPH_CONFIG_DIR must be an absolute path.",
650
+ unsafe_path: "A path in .metergraph is a symbolic link or is not a regular file or directory. Nothing was changed.",
651
+ binding_invalid: "The project binding .metergraph/project.json is not valid. Nothing was changed.",
652
+ binding_locked:
653
+ "Another login or logout may be running. If none is, delete .metergraph/project.lock and retry.",
654
+ binding_changed: "The project binding changed while login was running. Nothing was changed.",
655
+ binding_write_failed:
656
+ "The project binding could not be written. The new grant was removed and the service was asked to revoke it.",
657
+ binding_partial_write:
658
+ "The project binding could not be written and the new grant could not be removed from the config directory. " +
659
+ "The service was asked to revoke it. Run logout or login again.",
660
+ binding_remove_failed:
661
+ "The saved grant was handled as reported, but .metergraph/project.json could not be removed. " +
662
+ "Run logout again to remove it.",
663
+ read_failed: "The project files could not be read. Nothing was changed.",
664
+ credential_store_unavailable: "The private config directory could not be used. Nothing was saved.",
665
+ credential_path_unsafe:
666
+ "A path in the private config directory is a symbolic link or is not a regular file or directory. Nothing was changed.",
667
+ credential_path_not_owned: "The private config directory is owned by another user. Nothing was changed.",
668
+ credential_permissions_unsafe:
669
+ "The private config directory or its parent can be read or changed by other users. " +
670
+ "Restrict it to your user (for example chmod 700), then retry. Nothing was changed.",
671
+ credential_write_failed: "The grant could not be saved. Nothing was bound.",
672
+ credential_protection_failed: "The grant could not be protected for the current Windows user. Nothing was bound.",
673
+ credential_locked:
674
+ "Another command is using this project's saved grant. If none is running, delete its .lock file in the config directory.",
675
+ credential_unreadable: "The saved grant cannot be read. Run login again.",
676
+ not_signed_in: "This project is not signed in. Run login.",
677
+ credential_missing: "The saved grant for this project is missing. Run login again.",
678
+ credential_context_mismatch: "The saved grant does not match this project's binding. Run login again.",
679
+ reconnect_required:
680
+ "An earlier refresh did not finish, so the saved grant may no longer be valid. Run login again.",
681
+ refresh_interrupted:
682
+ "Refreshing the grant did not finish cleanly. It is not retried with a possibly used token. Run login again.",
683
+ refresh_not_saved: "The refreshed grant could not be saved. Run login again.",
684
+ grant_rejected: "The service no longer accepts this grant. Run login again.",
685
+ access_revoked: "The service refused the saved grant: it was revoked or access to the workspace was lost. Run login again.",
686
+ revocation_unconfirmed:
687
+ "Local sign in was removed, but the service did not confirm that the grant was revoked.",
688
+ };
689
+
690
+ export function authMessage(outcome, reason) {
691
+ return AUTH_MESSAGES[reason] ?? doctorMessage(outcome, reason);
692
+ }
693
+
694
+ export function authText(result, message) {
695
+ const report = result.data;
696
+ const lines = [`Metergraph ${result.command}`];
697
+ if (report.origin !== null) lines.push(`Origin: ${report.origin}`);
698
+ if (report.deployment_profile) lines.push(`Deployment profile: ${report.deployment_profile}`);
699
+ if (report.workspace !== null) lines.push(`Workspace: ${report.workspace.id}`);
700
+ if (result.command === "login") {
701
+ if (result.ok) {
702
+ const status = { signed_in: "signed in", reused: "already signed in", reconnected: "reconnected" }[report.status];
703
+ lines.push(
704
+ `Status: ${status}`,
705
+ `Scopes: ${report.scopes.join(" ")}`,
706
+ `Binding: ${report.binding}`,
707
+ `Grant storage: ${report.credential_protection === "dpapi" ? "Windows DPAPI, current user" : "owner-only file"}`,
708
+ );
709
+ if (report.previous_grant_revocation === "unconfirmed") {
710
+ lines.push("The previous grant was removed locally; the service did not confirm its revocation.");
711
+ }
712
+ }
713
+ } else {
714
+ lines.push(
715
+ `Saved grant: ${report.local_credentials}`,
716
+ `Binding: ${report.binding}`,
717
+ `Server revocation: ${report.revocation.replace("_", " ")}`,
718
+ );
719
+ if (result.ok && report.binding === "none") lines.push("This project was not signed in.");
720
+ }
721
+ if (!result.ok) lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`, message);
722
+ const next = report.next_action;
723
+ if (next?.message) lines.push(`Next: ${next.message}`);
724
+ if (next?.url) lines.push(`Connection guide: ${next.url}`);
725
+ return `${lines.join("\n")}\n`;
726
+ }