metergraph-cli 0.2.0-preview.4 → 0.2.0-preview.6

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.
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: metergraph-onboarding
3
+ description: Guide a coding agent from an empty local or hosted MeterGraph workspace to a verified application trace, then hand off to workload and eval selection.
4
+ ---
5
+
6
+ # Set up MeterGraph through the first usable trace
7
+
8
+ Confirm the user's deployment choice, application repository, target workspace,
9
+ coding client and whether they want SDK instrumentation or an existing supported
10
+ import. Use the actual MeterGraph origin the user selected. Do not guess an
11
+ origin from a credential. Record client/version, application revision, setup
12
+ start time and fixture source. Keep demo, synthetic, import and application
13
+ traffic distinct.
14
+
15
+ ## Establish the deployment and workspace
16
+
17
+ For hosted MeterGraph, connect with OAuth. No operator-issued key is needed.
18
+ Add the hosted endpoint as an HTTP MCP server with no credential or headers,
19
+ for example `claude mcp add --transport http metergraph
20
+ https://app.metergraph.dev/v1/agent/mcp`. Then use the client's own
21
+ authentication step (in Claude Code, `/mcp`, choose `metergraph`, then
22
+ Authenticate). The human completes browser sign-in and consent in the browser,
23
+ in the workspace they intend to use, and confirms the workspace shown on the
24
+ consent page. The consent page names the requested role; a client following
25
+ the server's challenge requests Metadata. If the client cannot
26
+ complete OAuth, or the user needs retained content or replay, describe the
27
+ Keys-page scoped connection as a manual alternative and obtain the user's
28
+ choice. Never silently fall back to a key.
29
+
30
+ For the first application trace, check the installed Metergraph CLI's version
31
+ and help before naming commands. `metergraph-cli@0.1.0` has `doctor` and project
32
+ skill installation; a later installed version may offer `login`, `setup` and
33
+ `verify`. Use only commands present in that installation. Otherwise follow
34
+ https://www.metergraph.dev/docs/start/first-trace/ and its manual setup steps.
35
+ Do not infer that a server endpoint or source-only CLI command has shipped.
36
+
37
+ For customer-local MeterGraph, use `metergraph-local-bundle` to stand up the
38
+ released bundle, then continue here. Use its included README, signed manifest
39
+ and verification files. The human obtains the current
40
+ pull-only registry credential; registry access is not yet universally self-serve.
41
+ Use password-stdin and the local secret store for registry login. Verify the
42
+ archive before running its scripts. Configure the bundle's local settings,
43
+ start with `./bin/start`, and inspect `./bin/status` and the reported readiness.
44
+ Use the bundle's printed loopback URL. Record bundle version and image digests.
45
+ The human sets their local admin/database passwords and signs in. Reuse the
46
+ bundle's startup/recovery instructions; do not replace them with an ad-hoc
47
+ Compose stack. First boot may ingest a demo trace, which is synthetic evidence.
48
+
49
+ ## Connect the coding client
50
+
51
+ For hosted OAuth, the connection is the URL-only server added above. For a
52
+ keyed or customer-local connection, reuse the Keys page's client-specific
53
+ instructions, documented in
54
+ https://www.metergraph.dev/docs/guides/agent-access/. The endpoint is
55
+ `<origin>/v1/agent/mcp`; the stdio bridge is `metergraph-mcp` with
56
+ `METERGRAPH_URL` and `METERGRAPH_AGENT_TOKEN` in its environment. Verify
57
+ `tools/list`, then `metergraph_get_workspace_context` and
58
+ `metergraph_get_capabilities`. Record the selected origin separately: `managed` identifies the hosted profile
59
+ and does not distinguish staging from production. Match the returned workspace ID and deployment
60
+ to the user's selection before reading traces. Capabilities and required scopes
61
+ from this connection are authoritative. Do not infer write authority from
62
+ `agent:read` or provider authority from a healthy MCP connection.
63
+
64
+ Keep these credentials separate:
65
+
66
+ | Credential | Destination and purpose | Human step |
67
+ | --- | --- | --- |
68
+ | Registry pull credential | Image registry only | Obtain/refresh through the supported distribution channel |
69
+ | Ingest credential | Application SDK or supported import connector | Create an ingest key for the selected workspace |
70
+ | Agent Access credential | Coding client's supported OAuth/secret configuration | Sign in/consent or create a scoped connection |
71
+ | Model-provider credential | Deployment's supported runner/provider configuration | Set the provider key and approve paid execution |
72
+
73
+ Never ask for secrets in chat, commit them, put them in a shared MCP file or
74
+ save them in verification evidence. A read connection is sufficient for this
75
+ onboarding verification. Replay, eval previews and analyses make separate
76
+ authorization and cost decisions.
77
+
78
+ ## Instrument or import application traffic
79
+
80
+ Reuse the appropriate language/provider instructions and the repository prompt
81
+ at https://www.metergraph.dev/docs/agents/instrument-a-repo/#verify-one-application-call, or a supported
82
+ connector under https://www.metergraph.dev/docs/import/overview/. Inspect the real
83
+ application entry points first. Exclude tests, fixtures, mock libraries, demos
84
+ and validation jobs from production traffic claims. Keep provider base URLs,
85
+ gateway credentials and application behavior intact. Review instrumentation
86
+ changes with the user, including calls that could not be captured.
87
+
88
+ Use the selected deployment's ingestion destination and the ingest credential,
89
+ not its MCP URL or Agent Access key. Use that deployment's SDK/connector setup
90
+ instructions to choose the complete ingest URL. Set capture/retention as the
91
+ workspace owner intends; metadata-only telemetry does not prove retained
92
+ inputs/outputs needed for eval replay. Do not silently enable content capture.
93
+ Avoid treating received HTTP acceptance as worker processing completion.
94
+
95
+ With user authorization, trigger one normal application request/job and record
96
+ its application-owned trace ID and time. For an import, record the supported
97
+ connector, source trace identity and imported time window. Do not create an LLM
98
+ connection, paid eval rule or provider invocation just to manufacture proof.
99
+
100
+ ## Verify and hand off
101
+
102
+ 1. Read bounded `metergraph_get_ingestion_health` if available. Preserve pending,
103
+ processed and failed states and receive/process timestamps. Counts apply to
104
+ a workspace window; they do not identify the batch that contains one trace.
105
+ 2. Read bounded `metergraph_query_traces` in the same workspace/time window and
106
+ locate the exact application/import trace ID. Continue bounded pagination
107
+ when advertised. An empty page is a setup state, not proof of no ingestion.
108
+ 3. If the user permits a content-class read and `trace_content` is available,
109
+ inspect one `metergraph_get_trace` for retained usable input/output and capture
110
+ warnings. Record availability and omissions, never the content in setup logs.
111
+ 4. When advertised, use classified-workload and model-readiness tools to explain
112
+ available cohorts, capture eligibility, supported checks and missing provider
113
+ credentials. A configured credential is not proof of a successful provider
114
+ call. Missing capability is an explicit handoff blocker.
115
+ 5. Return workspace/profile, bundle/app/client revisions, source label, exact
116
+ trace ID/time window, ingestion and capture evidence, warnings, human steps
117
+ still needed and the next eval selection action. A synthetic demonstration
118
+ does not meet real application or hosted pilot acceptance.
119
+
120
+ Verify the named trace through the connected client's advertised MCP tools,
121
+ with an explicit source label and the same workspace/deployment before and
122
+ after any permitted content read. The older trace-debug contract has no
123
+ workspace envelope, so confirm its tenant-bound connection separately. Trace
124
+ discovery is not analysis readiness. Keep retained content out of setup reports.
125
+
126
+ ## Recovery
127
+
128
+ | State | Next action |
129
+ | --- | --- |
130
+ | Signup, sign-in or OAuth consent required | Hand the browser step to the human, then reconnect and verify workspace identity |
131
+ | Registry access expired/denied | Refresh the pull credential through the supported distribution channel, repeat registry login, retain local data |
132
+ | Local startup or bootstrap failure | Inspect `bin/status` and the bundle's named logs/readiness checks; follow its recovery instructions |
133
+ | Runner unavailable | Use the bundle's runner health instructions; do not claim MCP health proves analysis readiness |
134
+ | Empty data | Check application environment, correct ingest destination/key, flush/import window and worker status; exclude demos/mocks |
135
+ | Received but pending/failed | Inspect ingestion failure classes and the existing retry/recovery flow before changing instrumentation |
136
+ | Missing/retained content unavailable | Explain capture policy, retention and omission; collect a new permitted application trace |
137
+ | Agent credential revoked or insufficient scope | Reconnect/re-authorize through the workspace owner's supported path; do not use an ingest key |
138
+ | Provider key absent or candidate unsupported | Complete the runner/provider setup or choose an available candidate; defer paid execution |
139
+ | API capability unavailable | State the unsupported deployment boundary and the specific supported next step |
140
+
141
+ Do not run destructive `down --volumes` as recovery. The bundle warns that raw
142
+ payload persistence differs from database persistence; prefer its stop/start
143
+ procedure and preserve existing data. Refine and rerun evals only through the
144
+ separately authorized workflow. Packaging this skill belongs to the distribution
145
+ workflow; this source does not assert that plugin OAuth/configuration or hosted
146
+ onboarding has already shipped.
147
+
148
+ Use the public skill's current client/runtime/deployment table at
149
+ https://www.metergraph.dev/SKILL.md. An unimplemented cloud/OAuth client path
150
+ is blocked even if server code exists. Query an exact SDK trace ID, or resolve a
151
+ provider request ID only when that argument is advertised. Ambiguous request IDs
152
+ need confirmation from the application; never select the first match. Preserve
153
+ checkpoint source run, pattern and pattern-set identities in workload queries and
154
+ cursor continuation. Missing/fallback IDs and expired records leave incomplete
155
+ evidence. Read classification availability separately from failed analysis and
156
+ report availability. Discovery must not launch or spend on classification or analysis.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: metergraph-report
3
+ description: Summarize a finished Metergraph analysis in a few lines with the link to its report in the app, say whether the evidence supports a switch, no switch or more testing, and tell the person exactly what to inspect in the app. Use after an analysis completes, when someone asks "what did the analysis find", "should we switch", "give me the report link", or stage 5 of the metergraph-model-swap loop.
4
+ ---
5
+
6
+ # Summarize the report and hand off to the app
7
+
8
+ The coding agent gives the answer and the link. The app is where the person
9
+ reads the full report and inspects case-level evidence.
10
+
11
+ ## 0. Confirm the connection
12
+
13
+ If this conversation has not already confirmed the workspace, call
14
+ `metergraph_get_workspace_context`, then `metergraph_get_capabilities`, before
15
+ anything else. Say which workspace you are connected to in one line, and plan
16
+ only with the tools the capabilities advertise.
17
+
18
+ ## 1. Read it
19
+
20
+ 1. `metergraph_get_analysis_run` with the run ID. Continue only when the status
21
+ is final. If `report.available` is false on a completed run, the report is
22
+ missing: say so and give the run's page (below).
23
+ 2. `metergraph_get_report` with `report.analysis_id`. Read the `outcome`, each
24
+ workload's `state`, `sample_count` and `reason`, each `model_swap`
25
+ opportunity (`recommendation_status`, `quality_vs_reference`,
26
+ `safe_switch_rate`, `cost_ratio`, `quality_grade_counts`,
27
+ `evaluation_gate`, `savings_usd`), `spend_projection`,
28
+ `analysis_generation_cost` and every warning.
29
+ 3. Read evidence with `metergraph_get_report_evidence` only if the person asks
30
+ about specific cases and the key has the Debug role. It returns captured
31
+ content.
32
+
33
+ ## 2. Write the summary
34
+
35
+ At most eight lines, in this order:
36
+
37
+ ```
38
+ <Switch | Don't switch | Test more>: <workload> from <current model> to <candidate>.
39
+ Quality: <quality_vs_reference>, <safe_switch_rate> of cases safe to switch, graded on <n> samples against <eval revision or "default graders">.
40
+ Cost: <cost_ratio>× per call; about $<savings_usd>/month projected from <observed_days> days of traffic.
41
+ Runner-up: <candidate> — <one reason it lost>.
42
+ Caveats: <sample size, short window, partial coverage, failed candidates, no eval bound>.
43
+ Report: <link>
44
+ Inspect: <the one thing to look at in the app>
45
+ ```
46
+
47
+ Rules:
48
+
49
+ - Every number comes from the report. Say "projected" for `spend_projection`
50
+ figures; a projection from a few days of traffic is not observed savings.
51
+ - Keep quality failures, evaluator or execution failures and missing coverage
52
+ apart. A candidate with missing results has not passed.
53
+ - If no eval was bound, say the grades came from default graders, not from the
54
+ person's own quality bar, and point to `metergraph-evals`.
55
+ - `no_opportunities` is a real answer: the candidates did not beat the current
56
+ model within the bar. `no_eligible_workloads` with `insufficient_samples`
57
+ needs more traffic, not a rerun.
58
+ - If the person's code shows where this workload's model is set, name the file
59
+ and line and suggest a canary experiment. Do not change code without a
60
+ separate instruction.
61
+
62
+ ## 3. Links
63
+
64
+ Use a link a tool returned (`metergraph_links`, `links`) exactly as given.
65
+ Otherwise build it from the origin of the Metergraph server the agent is
66
+ connected to (hosted is `https://app.metergraph.dev`; the customer-local bundle
67
+ is normally `http://127.0.0.1:8080`) and these app routes:
68
+
69
+ | Page | Route |
70
+ | --- | --- |
71
+ | Report | `<origin>/#analysis?report=<analysis_id>` |
72
+ | One workload's results in a report | `<origin>/#analysis/<analysis_id>/workloads/<workload_id>/model_evaluation` |
73
+ | Workload evidence | `<origin>/#workloads?pattern=<workload_id>&report=<analysis_id>` |
74
+ | Eval definition used | `<origin>/#analysis/<analysis_id>/workloads/<workload_id>/eval_definition` |
75
+ | All analyses and runs | `<origin>/#opportunities` |
76
+
77
+ Say that a built link was built, not returned, in a few words. If the person
78
+ says it does not open, give the Opportunities page instead.
79
+
80
+ ## 4. What to inspect in the app
81
+
82
+ Name one or two concrete things, from what the report shows:
83
+
84
+ - cases where the candidate was graded worse than the current model, to judge
85
+ whether the grader or the candidate is wrong;
86
+ - a check with a surprising pass rate, which may be a bad check;
87
+ - the cost of the cases that drive most of the spend.
88
+
89
+ Then ask the person to come back with what they found: grades they disagree
90
+ with, cases that should be added, or a candidate to drop. That is the input to
91
+ `metergraph-iterate`.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: metergraph-workloads
3
+ description: Choose which Metergraph workload to analyze - list classified workloads, rank them by spend with the source of each number stated, flag anomalies and suspect data, check readiness, and hand off when classification has not run. Use for "what is my largest workload", "list my workloads", "which workload should we optimize", or stage 1 of the metergraph-model-swap loop.
4
+ ---
5
+
6
+ # Choose a workload
7
+
8
+ The goal is one classified workload, its exact `selection_reference`, and a
9
+ one-sentence reason it was chosen.
10
+
11
+ ## 0. Confirm the connection
12
+
13
+ If this conversation has not already confirmed the workspace, call
14
+ `metergraph_get_workspace_context`, then `metergraph_get_capabilities`, before
15
+ anything else. Say which workspace you are connected to in one line, and plan
16
+ only with the tools the capabilities advertise.
17
+
18
+ ## 1. Read what exists
19
+
20
+ 1. `metergraph_list_classified_workloads` with a small limit (10). Each item
21
+ has a `display_name`, `classified_sample_count` and a `selection_reference`
22
+ of `source_run_id`, `pattern_id` and `pattern_set_version`. Keep the
23
+ reference exactly as returned. The name is a label.
24
+ 2. `metergraph_list_routes` and `metergraph_get_usage` with `days: 30`. These
25
+ give cost, calls, tokens, latency and errors per route per UTC day, plus
26
+ each route's `constraints` and `replay_eligible_calls`.
27
+ 3. `metergraph_list_reports` with a small limit. If a recent report exists,
28
+ `metergraph_get_report` gives each workload's `spend_projection`.
29
+
30
+ ## 2. Rank by spend, and say where each number came from
31
+
32
+ There are two kinds of spend number. Do not mix them in one column.
33
+
34
+ | Number | Source | Meaning |
35
+ | --- | --- | --- |
36
+ | Observed cost | `metergraph_get_usage` | What was ingested in the window. Per route, not per workload. |
37
+ | Projected spend | a report's `spend_projection` (`daily_rate_usd`, `projected_monthly_cost_usd`, `population`) | An extrapolation the analysis made. Per workload. |
38
+
39
+ The app's pages can show projected numbers. If the person quotes figures from
40
+ the app that do not match what you read, say which source each one is before
41
+ ranking, and ask which one they mean. Never rank on classified sample counts:
42
+ they are the sample, not the traffic.
43
+
44
+ Rank on a typical day, not the total. Compute each route's median daily cost
45
+ over the window. If one day or one hour dominates a route's total, call it out
46
+ as a spike and investigate it with `metergraph_list_incidents` and
47
+ `metergraph_query_traces` (status and error counts) before treating that route
48
+ as large. A runaway loop or retry storm is a bug to fix, not a model to swap.
49
+
50
+ Present at most the top five, one line each: workload, current model(s), typical
51
+ daily cost, share of typical spend, and one reason it is or is not a good
52
+ candidate. Opus-class models on extraction, classification or short-output
53
+ work are the usual best candidates. Workloads already on small models, or
54
+ under about 5% of spend, rarely repay an analysis.
55
+
56
+ ## 3. Check the data is real
57
+
58
+ Say so before planning when any of these hold, with the numbers:
59
+
60
+ - the workspace or all its data is only hours old, or arrived in one ingest
61
+ batch (`metergraph_get_ingestion_health`);
62
+ - incidents or usage disagree with each other for the same day;
63
+ - traces are labeled synthetic or demo, or a route mixes staging and
64
+ production. Ask which environment the analysis should use.
65
+
66
+ An analysis of seeded data proves the pipeline works and says nothing about
67
+ real costs.
68
+
69
+ ## 4. Check readiness of the chosen workload
70
+
71
+ `metergraph_get_workload_readiness` with the selection reference and the
72
+ chosen environment. Report `blocking_reasons`, the counts and the
73
+ `retained_window` in one or two lines. An analysis needs at least **10
74
+ samples**. Below that it completes with `no_eligible_workloads`; the fix is
75
+ more traffic, not a rerun. `stale_checkpoint` means the classification is
76
+ older than the current window.
77
+
78
+ ## 5. When nothing is classified yet
79
+
80
+ An empty list with `classification_pending` or `no_traces` means classification
81
+ has not run. No agent tool starts classification on its own. Tell the person:
82
+
83
+ 1. Open the **Opportunities** page in the Metergraph app and choose **Find
84
+ opportunities**.
85
+ 2. Choose to pick a specific workload rather than the automatic option. That
86
+ run classifies the traffic and then waits for them to choose a workload,
87
+ before any candidate model is called.
88
+ 3. The automatic option classifies and then immediately analyzes the largest
89
+ workloads with the saved candidate models, which sends captured prompts to
90
+ those providers and spends money. Point this out before they click.
91
+
92
+ When they are back, list classified workloads again and continue. Do not click
93
+ through the app yourself unless the person asks you to and has approved what
94
+ the button will do.
package/bin/metergraph.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { handOff, handsOff } from "../src/approval-handoff.js";
2
3
  import { main } from "../src/cli.js";
3
4
 
4
5
  // A closed pipe on stdout (for example "metergraph --help | head -1") is not
5
6
  // a CLI failure.
6
7
  process.stdout.on("error", () => {});
7
8
 
8
- process.exitCode = await main(process.argv.slice(2), {
9
- stdout: process.stdout,
10
- stderr: process.stderr,
11
- });
9
+ const argv = process.argv.slice(2);
10
+ const io = { stdout: process.stdout, stderr: process.stderr };
11
+ process.exitCode = handsOff(argv) ? await handOff(argv, io) : await main(argv, io);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metergraph-cli",
3
- "version": "0.2.0-preview.4",
3
+ "version": "0.2.0-preview.6",
4
4
  "description": "Preview Metergraph command line tool with a read-only connection probe, a project skill installer, Metadata-only sign in and bounded workspace Metadata reads",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,8 @@
14
14
  "src/*.js",
15
15
  "assets/skill/SKILL.md",
16
16
  "assets/skill/manifest.json",
17
+ "assets/skills/manifest.json",
18
+ "assets/skills/*/SKILL.md",
17
19
  "LICENSE",
18
20
  "README.md"
19
21
  ],
@@ -0,0 +1,91 @@
1
+ import childProcess from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { parseArgs } from "./args.js";
5
+ import { envelope, toJsonLine } from "./output.js";
6
+
7
+ // login and setup with --json --no-browser may need a person to approve in a
8
+ // browser. The approval returns to a loopback listener, so something must
9
+ // keep listening after the agent has the URL. Agent shells usually show a
10
+ // command's output only after it exits, so that cannot be this process.
11
+ //
12
+ // This process runs the same command as a detached waiter and relays the
13
+ // waiter's one JSON line. When the line is action_required, it exits with it
14
+ // at once and leaves the waiter listening until approval or --timeout-ms; the
15
+ // waiter saves what the approval completes and prints nothing more. Any other
16
+ // line is the command's result, and this process waits for the waiter to
17
+ // finish. The arguments carry no credential, and the waiter's environment is
18
+ // this one plus a marker.
19
+ const BIN = fileURLToPath(new URL("../bin/metergraph.js", import.meta.url));
20
+ const WAITER = "METERGRAPH_APPROVAL_WAITER";
21
+
22
+ export function handsOff(argv, env = process.env) {
23
+ if (env[WAITER] === "1") return false;
24
+ const parsed = parseArgs(argv);
25
+ return parsed.ok && (parsed.command === "login" || parsed.command === "setup") && parsed.json && parsed.noBrowser;
26
+ }
27
+
28
+ export function handOff(argv, { stdout }) {
29
+ return new Promise((resolve) => {
30
+ let child;
31
+ try {
32
+ child = childProcess.spawn(process.execPath, [...process.execArgv, BIN, ...argv], {
33
+ env: { ...process.env, [WAITER]: "1" },
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ detached: true,
36
+ windowsHide: true,
37
+ });
38
+ } catch {
39
+ resolve(failed(argv, stdout));
40
+ return;
41
+ }
42
+ let text = "";
43
+ let settled = false;
44
+ // Ctrl+C before the line arrives cancels the waiter, which reports it.
45
+ const forward = (signal) => child.kill(signal);
46
+ const signals = ["SIGINT", "SIGTERM"];
47
+ for (const signal of signals) process.on(signal, forward);
48
+ const finish = (code) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ for (const signal of signals) process.removeListener(signal, forward);
52
+ resolve(code);
53
+ };
54
+ child.once("error", () => finish(failed(argv, stdout)));
55
+ child.stdout.setEncoding("utf8");
56
+ child.stdout.on("data", (chunk) => {
57
+ if (settled) return;
58
+ text += chunk;
59
+ const end = text.indexOf("\n");
60
+ if (end === -1) return;
61
+ let line;
62
+ try { line = JSON.parse(text.slice(0, end)); } catch { line = null; }
63
+ if (line?.outcome !== "action_required") return;
64
+ stdout.write(text.slice(0, end + 1));
65
+ child.stdout.destroy();
66
+ child.unref();
67
+ finish(line.exit_code);
68
+ });
69
+ child.once("close", (code) => {
70
+ if (settled) return;
71
+ if (text.endsWith("\n") && text.indexOf("\n") === text.length - 1) {
72
+ stdout.write(text);
73
+ finish(code ?? 1);
74
+ } else {
75
+ finish(failed(argv, stdout));
76
+ }
77
+ });
78
+ });
79
+ }
80
+
81
+ function failed(argv, stdout) {
82
+ const parsed = parseArgs(argv);
83
+ const result = envelope({
84
+ command: parsed.command ?? null,
85
+ outcome: "internal_error",
86
+ reason: "unexpected_failure",
87
+ message: "The CLI failed unexpectedly. No details are shown to avoid exposing sensitive input.",
88
+ });
89
+ stdout.write(toJsonLine(result));
90
+ return result.exit_code;
91
+ }
package/src/args.js CHANGED
@@ -26,13 +26,15 @@ import { parseOrigin } from "./origin.js";
26
26
  import { isCursor, isSafeFilter } from "./read-contract.js";
27
27
 
28
28
  export const READ_COMMANDS = Object.freeze(["status", "context", "capabilities", "usage", "routes", "traces"]);
29
- const COMMANDS = new Set(["doctor", "help", "skill", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
30
- const HELP_TOPICS = new Set(["doctor", "skill", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
29
+ const COMMANDS = new Set(["doctor", "help", "skill", "skills", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
30
+ const HELP_TOPICS = new Set(["doctor", "skill", "skills", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
31
31
  const SKILL_ACTIONS = new Set(["install", "update"]);
32
+ const SKILLS_ACTIONS = new Set(["install", "update", "list"]);
32
33
  const READ_BASE = ["--project", "--config-dir", "--timeout-ms"];
33
34
  const OPTIONS = {
34
35
  doctor: new Set(["--url", "--timeout-ms"]),
35
36
  skill: new Set(["--client", "--runtime", "--project"]),
37
+ skills: new Set(["--client", "--runtime", "--project"]),
36
38
  login: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--timeout-ms"]),
37
39
  setup: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--env-file", "--client", "--timeout-ms", "--deployment", "--agent-token-file", "--repository"]),
38
40
  logout: new Set(["--project", "--config-dir"]),
@@ -86,6 +88,8 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
86
88
  // { ok: true, command: "version", json }
87
89
  // { ok: true, command: "doctor", origin, timeoutMs, json }
88
90
  // { ok: true, command: "skill", action, client, runtime, project, json }
91
+ // { ok: true, command: "skills", action, client, runtime, project, json }
92
+ // (client and runtime are null for list)
89
93
  // { ok: true, command: "login", runtime, origin, workspace, project,
90
94
  // configDir, timeoutMs, signup, noBrowser, reconnect, json }
91
95
  // { ok: true, command: "logout", project, configDir, json }
@@ -113,7 +117,9 @@ export function parseArgs(argv) {
113
117
 
114
118
  const fail = (code, message, outcome = "invalid_input") => ({
115
119
  ok: false,
116
- command: command === "skill" && action !== null ? `skill ${action}` : command ?? (version ? "version" : null),
120
+ command: (command === "skill" || command === "skills") && action !== null
121
+ ? `${command} ${action}`
122
+ : command ?? (version ? "version" : null),
117
123
  json,
118
124
  code,
119
125
  message,
@@ -142,6 +148,10 @@ export function parseArgs(argv) {
142
148
  action = arg;
143
149
  continue;
144
150
  }
151
+ if (command === "skills" && action === null && SKILLS_ACTIONS.has(arg)) {
152
+ action = arg;
153
+ continue;
154
+ }
145
155
 
146
156
  if (READ_COMMANDS.includes(command)) {
147
157
  const equals = arg.indexOf("=");
@@ -192,6 +202,12 @@ export function parseArgs(argv) {
192
202
  `Unknown skill subcommand at argument ${position}. Use install or update.`,
193
203
  );
194
204
  }
205
+ if (command === "skills" && action === null && !arg.startsWith("-")) {
206
+ return fail(
207
+ "unknown_subcommand",
208
+ `Unknown skills subcommand at argument ${position}. Use install, update or list.`,
209
+ );
210
+ }
195
211
  return fail(
196
212
  "unknown_argument",
197
213
  `Unrecognized argument at position ${position}. Run "metergraph --help" for usage.`,
@@ -203,6 +219,7 @@ export function parseArgs(argv) {
203
219
  return { ok: true, command: "help", topic, json };
204
220
  }
205
221
  if (command === "skill") return parseSkill(action, values, json, fail);
222
+ if (command === "skills") return parseSkills(action, values, json, fail);
206
223
  if (command === "login") return parseLogin(values, flags, json, fail);
207
224
  if (command === "setup") return parseSetup(values, flags, json, fail);
208
225
  if (command === "verify") return parseVerify(values, flags, json, fail);
@@ -262,9 +279,10 @@ function parseVerify(values, flags, json, fail) {
262
279
  open: flags.has("--open"), noBrowser: flags.has("--no-browser"), json };
263
280
  }
264
281
 
265
- // Client and runtime are required, so a script states where the skill is
266
- // used instead of the CLI guessing. Values for clients and runtimes that
267
- // cannot use project skill files are accepted here and handed off later.
282
+ // The client is required, so a script states which client loads the skill.
283
+ // The runtime defaults to local, the runtime setup installs the skill for;
284
+ // pass --runtime cloud for a cloud checkout. Values for clients and runtimes
285
+ // that cannot use project skill files are accepted here and handed off later.
268
286
  function parseSkill(action, values, json, fail) {
269
287
  if (action === null) {
270
288
  return fail(
@@ -279,10 +297,7 @@ function parseSkill(action, values, json, fail) {
279
297
  if (!Object.hasOwn(SKILL_CLIENTS, client) && !Object.hasOwn(HANDOFF_SKILL_CLIENTS, client)) {
280
298
  return fail("invalid_client", "--client must be codex, claude or cursor.");
281
299
  }
282
- const runtime = values["--runtime"];
283
- if (runtime === undefined) {
284
- return fail("missing_runtime", "--runtime is required. Use local or cloud.");
285
- }
300
+ const runtime = values["--runtime"] ?? "local";
286
301
  if (!SKILL_RUNTIMES.includes(runtime) && !HANDOFF_SKILL_RUNTIMES.includes(runtime)) {
287
302
  return fail("invalid_runtime", "--runtime must be local or cloud.");
288
303
  }
@@ -293,6 +308,29 @@ function parseSkill(action, values, json, fail) {
293
308
  return { ok: true, command: "skill", action, client, runtime, project: project ?? null, json };
294
309
  }
295
310
 
311
+ // skills install and update take the same options as skill. list is read
312
+ // only and takes just --project.
313
+ function parseSkills(action, values, json, fail) {
314
+ if (action === null) {
315
+ return fail(
316
+ "missing_subcommand",
317
+ 'skills requires a subcommand: install, update or list. Run "metergraph help skills" for usage.',
318
+ );
319
+ }
320
+ if (action === "list") {
321
+ if (values["--client"] !== undefined || values["--runtime"] !== undefined) {
322
+ return fail("unexpected_option", "skills list takes only --project. It reports every client.");
323
+ }
324
+ const project = values["--project"];
325
+ if (project !== undefined && (project === "" || project.includes("\0"))) {
326
+ return fail("invalid_project", "--project must name an existing directory.");
327
+ }
328
+ return { ok: true, command: "skills", action, client: null, runtime: null, project: project ?? null, json };
329
+ }
330
+ const parsed = parseSkill(action, values, json, fail);
331
+ return parsed.ok ? { ...parsed, command: "skills" } : parsed;
332
+ }
333
+
296
334
  const INVALID_URL =
297
335
  "--url must be a bare https origin such as https://metergraph.example.com, " +
298
336
  "or an http origin on localhost, 127.0.0.1 or [::1]. " +
@@ -402,7 +440,8 @@ function parseSetup(values, flags, json, fail) {
402
440
  parseTimeout(rawTimeout, LOGIN_MIN_TIMEOUT_MS, LOGIN_MAX_TIMEOUT_MS);
403
441
  if (timeoutMs === null) return fail("invalid_timeout", `--timeout-ms must be a whole number from ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}.`);
404
442
  return { ok: true, command: "setup", runtime, origin, originExplicit: values["--url"] !== undefined,
405
- deployment, confirmPrerequisites: flags.has("--confirm-prerequisites"), agentTokenFile,
443
+ deployment, deploymentExplicit: values["--deployment"] !== undefined,
444
+ confirmPrerequisites: flags.has("--confirm-prerequisites"), agentTokenFile,
406
445
  workspace: rawWorkspace === undefined ? null : rawWorkspace.toLowerCase(),
407
446
  project: paths.project, configDir: paths.configDir, envFile, client, skipSkill: flags.has("--skip-skill"),
408
447
  timeoutMs, noBrowser: flags.has("--no-browser"), repair: flags.has("--repair"),