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.
- package/README.md +36 -12
- package/assets/skill/SKILL.md +20 -1
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +67 -0
- package/assets/skills/metergraph-analyze/SKILL.md +159 -0
- package/assets/skills/metergraph-candidates/SKILL.md +84 -0
- package/assets/skills/metergraph-evals/SKILL.md +99 -0
- package/assets/skills/metergraph-investigate/SKILL.md +105 -0
- package/assets/skills/metergraph-iterate/SKILL.md +73 -0
- package/assets/skills/metergraph-local-bundle/SKILL.md +283 -0
- package/assets/skills/metergraph-model-swap/SKILL.md +81 -0
- package/assets/skills/metergraph-onboarding/SKILL.md +156 -0
- package/assets/skills/metergraph-report/SKILL.md +91 -0
- package/assets/skills/metergraph-workloads/SKILL.md +94 -0
- package/bin/metergraph.js +4 -4
- package/package.json +3 -1
- package/src/approval-handoff.js +91 -0
- package/src/args.js +50 -11
- package/src/auth-login.js +27 -13
- package/src/cli.js +70 -7
- package/src/constants.js +9 -0
- package/src/deployment-route.js +1 -1
- package/src/output.js +138 -9
- package/src/setup-deployment.js +103 -7
- package/src/setup.js +33 -16
- package/src/skill-bundle.js +68 -1
- package/src/skill.js +175 -14
package/src/auth-login.js
CHANGED
|
@@ -39,6 +39,11 @@ import { deadline, trapSignals } from "./transport.js";
|
|
|
39
39
|
// server-verified workspace UUID. Token values, the authorization code, the
|
|
40
40
|
// PKCE verifier, absolute paths and server text never leave this module.
|
|
41
41
|
// progress(text) prints human progress lines; it is a no-op with --json.
|
|
42
|
+
// options.announce(data), given only with --json --no-browser, receives the
|
|
43
|
+
// login data with an open_url next action as soon as the authorization URL
|
|
44
|
+
// exists, while the listener keeps waiting. The caller prints it as the one
|
|
45
|
+
// JSON line, so an agent can hand the URL to a person; approval-handoff.js
|
|
46
|
+
// lets the agent's command exit then while this process keeps waiting.
|
|
42
47
|
|
|
43
48
|
export async function runLogin(options, progress) {
|
|
44
49
|
const ctx = {
|
|
@@ -58,21 +63,12 @@ export async function runLogin(options, progress) {
|
|
|
58
63
|
if (options.runtime !== "local") return end("unsupported", "runtime_not_supported", GUIDE);
|
|
59
64
|
const remote = detectRemoteSession();
|
|
60
65
|
if (remote !== null) return end("unsupported", remote, GUIDE);
|
|
61
|
-
// --json cannot show a person the --no-browser URL. A rerun with a saved,
|
|
62
|
-
// working grant needs no URL, so only refuse once approval is required.
|
|
63
|
-
const noBrowserHandoff = () => end("unsupported", "no_browser_requires_terminal", {
|
|
64
|
-
kind: "run_in_terminal",
|
|
65
|
-
message: "Run the same login command in a terminal without --json to see the sign in URL.",
|
|
66
|
-
});
|
|
67
|
-
const urlUnshowable = options.json && options.noBrowser;
|
|
68
|
-
|
|
69
66
|
const trap = trapSignals();
|
|
70
67
|
try {
|
|
71
68
|
const root = resolveProject(options.project);
|
|
72
69
|
const configDir = resolveConfigDir(options.configDir);
|
|
73
70
|
const existing = readBinding(root);
|
|
74
71
|
const bound = existing?.binding ?? null;
|
|
75
|
-
if (urlUnshowable && (bound === null || options.reconnect)) return noBrowserHandoff();
|
|
76
72
|
// Setup pins a deployment model before sign in. A stale binding for a
|
|
77
73
|
// different profile must never be silently reused or reconnected.
|
|
78
74
|
if (options.expectedProfile && bound !== null && !options.reconnect &&
|
|
@@ -102,9 +98,11 @@ export async function runLogin(options, progress) {
|
|
|
102
98
|
}
|
|
103
99
|
if (session.outcome !== "login_required") return end(session.outcome, session.reason);
|
|
104
100
|
}
|
|
105
|
-
if (urlUnshowable) return noBrowserHandoff();
|
|
106
101
|
|
|
107
|
-
const
|
|
102
|
+
const announce = options.announce
|
|
103
|
+
? (next) => options.announce(loginData({ ...ctx, status: "approval_pending" }, next))
|
|
104
|
+
: null;
|
|
105
|
+
const flow = await authorize(options, ctx, trap.signal, progress, announce);
|
|
108
106
|
if (!flow.ok) return end(flow.outcome, flow.reason, flow.next ?? null);
|
|
109
107
|
const { endpoints, clientId, grant } = flow;
|
|
110
108
|
|
|
@@ -169,7 +167,7 @@ export async function runLogin(options, progress) {
|
|
|
169
167
|
// written here. A validated grant that is not accepted is sent for revocation
|
|
170
168
|
// before returning; that is best effort, and a token response that fails
|
|
171
169
|
// validation cannot be revoked.
|
|
172
|
-
async function authorize(options, ctx, cancel, progress) {
|
|
170
|
+
async function authorize(options, ctx, cancel, progress, announce) {
|
|
173
171
|
const preflight = await runDoctor({ origin: ctx.origin, timeoutMs: AUTH_HTTP_TIMEOUT_MS });
|
|
174
172
|
if (cancel.aborted) return fail("authorization_failed", "cancelled");
|
|
175
173
|
if (preflight.outcome !== "authentication_required") return fail(preflight.outcome, preflight.reason);
|
|
@@ -217,7 +215,9 @@ async function authorize(options, ctx, cancel, progress) {
|
|
|
217
215
|
const seconds = Math.round(options.timeoutMs / 1000);
|
|
218
216
|
// A wait armed after Ctrl+C has already settled; do not show the URL.
|
|
219
217
|
if (cancel.aborted) return fail("authorization_failed", "cancelled");
|
|
220
|
-
if (options.noBrowser) {
|
|
218
|
+
if (options.noBrowser && announce) {
|
|
219
|
+
announce(openUrl(url, seconds, "sign in with Metadata access"));
|
|
220
|
+
} else if (options.noBrowser) {
|
|
221
221
|
progress(
|
|
222
222
|
"Open this URL in a browser on this machine to sign in:\n" +
|
|
223
223
|
`${url}\n` +
|
|
@@ -375,6 +375,20 @@ function fail(outcome, reason, next = null) {
|
|
|
375
375
|
return { ok: false, outcome, reason, next };
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// The next action a --json --no-browser run prints while it waits. The URL is
|
|
379
|
+
// the authorization request: public client ID, loopback redirect, state and
|
|
380
|
+
// PKCE challenge. It carries no token, code or verifier.
|
|
381
|
+
export function openUrl(url, seconds, purpose) {
|
|
382
|
+
return {
|
|
383
|
+
kind: "open_url",
|
|
384
|
+
url,
|
|
385
|
+
timeout_seconds: seconds,
|
|
386
|
+
message: `Show this URL to the person now and ask them to open it in a browser on this machine to ` +
|
|
387
|
+
`${purpose}. A background process waits up to ${seconds} seconds for their approval. ` +
|
|
388
|
+
"After they approve, run the same command again to continue.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
378
392
|
const GUIDE = { kind: "connection_guide", url: CONNECTION_GUIDE_URL };
|
|
379
393
|
const RECONNECT = {
|
|
380
394
|
kind: "reconnect",
|
package/src/cli.js
CHANGED
|
@@ -10,14 +10,15 @@ import {
|
|
|
10
10
|
helpData,
|
|
11
11
|
helpText,
|
|
12
12
|
skillText,
|
|
13
|
+
skillsText,
|
|
13
14
|
toJsonLine,
|
|
14
15
|
versionData,
|
|
15
16
|
} from "./output.js";
|
|
16
17
|
import { runRead } from "./read.js";
|
|
17
18
|
import { readMessage, readText } from "./read-output.js";
|
|
18
|
-
import { runSkill } from "./skill.js";
|
|
19
|
+
import { listSkillPack, runSkill, runSkillPack } from "./skill.js";
|
|
19
20
|
import { runSetup } from "./setup.js";
|
|
20
|
-
import { containsKnownCredential, preflightNonHostedSetup } from "./setup-deployment.js";
|
|
21
|
+
import { containsKnownCredential, preflightNonHostedSetup, prerequisiteGuide, resumeNonHostedRoute } from "./setup-deployment.js";
|
|
21
22
|
import { VERSION } from "./constants.js";
|
|
22
23
|
import { runVerify } from "./verify.js";
|
|
23
24
|
import { verifyMessage, verifyText } from "./verify-output.js";
|
|
@@ -74,8 +75,10 @@ async function run(argv, { stdout, stderr }) {
|
|
|
74
75
|
// Human progress goes to stderr, never with --json, so stdout carries
|
|
75
76
|
// only the final result.
|
|
76
77
|
const progress = parsed.json ? () => {} : (text) => stderr.write(`${text}\n`);
|
|
78
|
+
const pending = announcer(parsed, stdout, (data) => data);
|
|
77
79
|
const { outcome, reason, data } =
|
|
78
|
-
parsed.command === "login" ? await runLogin(parsed, progress) : await runLogout(parsed);
|
|
80
|
+
parsed.command === "login" ? await runLogin({ ...parsed, announce: pending.announce }, progress) : await runLogout(parsed);
|
|
81
|
+
if (pending.result) return pending.result;
|
|
79
82
|
const message = authMessage(outcome, reason);
|
|
80
83
|
const result = envelope({
|
|
81
84
|
command: parsed.command,
|
|
@@ -89,14 +92,18 @@ async function run(argv, { stdout, stderr }) {
|
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
if (parsed.command === "setup") {
|
|
92
|
-
const
|
|
95
|
+
const options = resumeNonHostedRoute(parsed);
|
|
96
|
+
const routed = options.deployment === "managed"
|
|
93
97
|
? { proceed: true, profile: "managed" }
|
|
94
|
-
: await preflightNonHostedSetup(
|
|
98
|
+
: await preflightNonHostedSetup(options);
|
|
95
99
|
const progress = parsed.json ? () => {} : (line) =>
|
|
96
100
|
stderr.write(containsKnownCredential(line, routed) ? "Setup progress.\n" : `${line}\n`);
|
|
101
|
+
// The announced line is checked for known credentials like the final one.
|
|
102
|
+
const pending = announcer(parsed, stdout, (data) => containsKnownCredential(data, routed) ? null : data);
|
|
97
103
|
const response = routed.proceed
|
|
98
|
-
? await runSetup({ ...
|
|
104
|
+
? await runSetup({ ...options, expectedProfile: routed.profile ?? null, announce: pending.announce }, progress)
|
|
99
105
|
: routed;
|
|
106
|
+
if (pending.result) return pending.result;
|
|
100
107
|
const { outcome, reason, data } = response;
|
|
101
108
|
let result = envelope({ command: "setup", outcome, reason, data,
|
|
102
109
|
message: outcome === "ok" ? null : "Setup did not complete. Review the reason and retry safely." });
|
|
@@ -106,7 +113,8 @@ async function run(argv, { stdout, stderr }) {
|
|
|
106
113
|
const receipt = result.data?.receipt;
|
|
107
114
|
const summary = receipt ? `Workspace: ${receipt.workspace_id}\nDeployment: ${receipt.deployment_profile} at ${receipt.origin}\nCompleted: ${receipt.completed_steps.join(", ")}\nPending: ${receipt.pending_steps.join(", ")}\n` : "";
|
|
108
115
|
const identity = repositoryLine(result.data?.repository);
|
|
109
|
-
const
|
|
116
|
+
const guide = handoffLines(result.data);
|
|
117
|
+
const human = `Metergraph setup: ${result.data?.status ?? "not_ready"}\n${summary}${identity}${guide}Application traffic verified: no\n${result.ok ? "Next: instrument your application and verify an exact trace.\n" : `Result: ${result.outcome} (${result.error.reason})\n`}`;
|
|
110
118
|
if (containsKnownCredential(human, routed)) {
|
|
111
119
|
result = envelope({ command: "setup", outcome: "unsupported", reason: "credential_echo" });
|
|
112
120
|
stdout.write(parsed.json ? toJsonLine(result) : "Setup stopped.\n");
|
|
@@ -151,6 +159,20 @@ async function run(argv, { stdout, stderr }) {
|
|
|
151
159
|
return result;
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
if (parsed.command === "skills") {
|
|
163
|
+
const { outcome, reason, message, data } =
|
|
164
|
+
parsed.action === "list" ? listSkillPack(parsed) : runSkillPack(parsed);
|
|
165
|
+
const result = envelope({
|
|
166
|
+
command: `skills ${parsed.action}`,
|
|
167
|
+
outcome,
|
|
168
|
+
reason,
|
|
169
|
+
data,
|
|
170
|
+
message: outcome === "ok" ? null : message,
|
|
171
|
+
});
|
|
172
|
+
stdout.write(parsed.json ? toJsonLine(result) : skillsText(result, message));
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
const { outcome, reason, report } = await runDoctor({
|
|
155
177
|
origin: parsed.origin,
|
|
156
178
|
timeoutMs: parsed.timeoutMs,
|
|
@@ -166,6 +188,47 @@ async function run(argv, { stdout, stderr }) {
|
|
|
166
188
|
return result;
|
|
167
189
|
}
|
|
168
190
|
|
|
191
|
+
// With --json --no-browser, login and setup print their one JSON line as
|
|
192
|
+
// soon as a person must open an approval URL, then keep waiting for the
|
|
193
|
+
// callback; bin/metergraph.js has already moved this process to the
|
|
194
|
+
// background (approval-handoff.js). Whatever the run does afterwards, that
|
|
195
|
+
// line is its result; the agent reruns the command to continue. screen(data) returns
|
|
196
|
+
// the data to print, or null to print a credential_echo refusal instead.
|
|
197
|
+
function announcer(parsed, stdout, screen) {
|
|
198
|
+
const pending = { announce: null, result: null };
|
|
199
|
+
if (!parsed.json || !parsed.noBrowser) return pending;
|
|
200
|
+
pending.announce = (data) => {
|
|
201
|
+
if (pending.result) return;
|
|
202
|
+
const safe = screen(data);
|
|
203
|
+
pending.result = safe === null
|
|
204
|
+
? envelope({ command: parsed.command, outcome: "unsupported", reason: "credential_echo" })
|
|
205
|
+
: envelope({ command: parsed.command, outcome: "action_required", reason: "browser_approval_required",
|
|
206
|
+
data: safe, message: authMessage("action_required", "browser_approval_required") });
|
|
207
|
+
stdout.write(toJsonLine(pending.result));
|
|
208
|
+
};
|
|
209
|
+
return pending;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// What an operator handoff is waiting for, in fixed text: the route, each
|
|
213
|
+
// unconfirmed prerequisite with its guidance, and the next action.
|
|
214
|
+
function handoffLines(data) {
|
|
215
|
+
if (data?.status !== "operator_handoff") return "";
|
|
216
|
+
let text = data.origin ? `Deployment: ${data.deployment_profile} at ${data.origin}\n` : "";
|
|
217
|
+
if (data.workspace_id) text += `Workspace: ${data.workspace_id}\n`;
|
|
218
|
+
if (data.pending_prerequisites.length > 0) {
|
|
219
|
+
text += "Confirm each prerequisite, then rerun with --confirm-prerequisites:\n";
|
|
220
|
+
for (const name of data.pending_prerequisites) {
|
|
221
|
+
text += ` - ${name}: ${prerequisiteGuide(name, data.deployment_profile) ?? "see the guide"}\n`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const next = data.next_action;
|
|
225
|
+
if (next?.message && !(next.kind === "complete_prerequisite" && data.pending_prerequisites.includes(next.prerequisite))) {
|
|
226
|
+
text += `Next: ${next.message}\n`;
|
|
227
|
+
}
|
|
228
|
+
if (next?.url) text += `Guide: ${next.url}\n`;
|
|
229
|
+
return text;
|
|
230
|
+
}
|
|
231
|
+
|
|
169
232
|
// One line on the repository identity setup found or recorded. The value is
|
|
170
233
|
// owner/name only; a remote URL is never shown.
|
|
171
234
|
function repositoryLine(repository) {
|
package/src/constants.js
CHANGED
|
@@ -39,6 +39,10 @@ export const SKILL_CLIENTS = Object.freeze({
|
|
|
39
39
|
});
|
|
40
40
|
export const SKILL_RUNTIMES = Object.freeze(["local", "cloud"]);
|
|
41
41
|
|
|
42
|
+
// Where every Metergraph skill is published. Claude Desktop installs the
|
|
43
|
+
// skills from here as a plugin marketplace instead of from project files.
|
|
44
|
+
export const SKILLS_REPOSITORY_URL = "https://github.com/metergraph/skills";
|
|
45
|
+
|
|
42
46
|
// Recognized values that cannot load project skill files. They get a pointer
|
|
43
47
|
// to the connection guide instead of a usage error, and nothing is written.
|
|
44
48
|
export const HANDOFF_SKILL_CLIENTS = Object.freeze({
|
|
@@ -137,6 +141,7 @@ export const EXIT_CODES = Object.freeze({
|
|
|
137
141
|
permission_denied: 15,
|
|
138
142
|
rate_limited: 16,
|
|
139
143
|
cancelled: 17,
|
|
144
|
+
action_required: 18,
|
|
140
145
|
});
|
|
141
146
|
|
|
142
147
|
export const EXIT_CODE_MEANINGS = Object.freeze({
|
|
@@ -173,4 +178,8 @@ export const EXIT_CODE_MEANINGS = Object.freeze({
|
|
|
173
178
|
rate_limited: "The service asked the CLI to slow down. Nothing was retried. Try again later.",
|
|
174
179
|
cancelled:
|
|
175
180
|
"A read command was interrupted before it finished. Read commands never change workspace configuration or telemetry.",
|
|
181
|
+
action_required:
|
|
182
|
+
"A person must act before this command can finish; data.next_action says what to do. " +
|
|
183
|
+
"With --json --no-browser the command exits as soon as the approval URL exists, and a background " +
|
|
184
|
+
"process waits for approval until --timeout-ms. After approval, run the same command again to continue.",
|
|
176
185
|
});
|
package/src/deployment-route.js
CHANGED
|
@@ -117,7 +117,7 @@ const DEPLOYMENT_PATH = "/v1/deployment";
|
|
|
117
117
|
const MIN_TIMEOUT_MS = 100;
|
|
118
118
|
const MAX_TIMEOUT_MS = 60000;
|
|
119
119
|
|
|
120
|
-
function action(kind, prerequisite = null) {
|
|
120
|
+
export function action(kind, prerequisite = null) {
|
|
121
121
|
// Kinds are constants from this module, so this is a programming error.
|
|
122
122
|
if (!ACTION_KINDS.includes(kind)) throw new Error("unknown next action kind");
|
|
123
123
|
return { kind, prerequisite, url: CONNECTION_GUIDE_URL };
|
package/src/output.js
CHANGED
|
@@ -73,15 +73,46 @@ const SKILL_OPTIONS = [
|
|
|
73
73
|
{
|
|
74
74
|
name: "--runtime",
|
|
75
75
|
value: "RUNTIME",
|
|
76
|
-
summary: "
|
|
76
|
+
summary: "local (default) or cloud: where the client runs. Recorded, not detected.",
|
|
77
77
|
},
|
|
78
78
|
{ name: "--project", value: "DIR", summary: "Existing project directory. Default: the current directory." },
|
|
79
79
|
{ name: "--json", value: null, summary: "Print one JSON line on stdout." },
|
|
80
80
|
];
|
|
81
81
|
|
|
82
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]",
|
|
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 SKILLS_USAGE = [
|
|
88
|
+
"metergraph skills install --client CLIENT [--runtime RUNTIME] [--project DIR] [--json]",
|
|
89
|
+
"metergraph skills update --client CLIENT [--runtime RUNTIME] [--project DIR] [--json]",
|
|
90
|
+
"metergraph skills list [--project DIR] [--json]",
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const SKILLS_LIST_OPTIONS = [
|
|
94
|
+
{ name: "--project", value: "DIR", summary: "Existing project directory. Default: the current directory." },
|
|
95
|
+
{ name: "--json", value: null, summary: "Print one JSON line on stdout." },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const SKILLS_STATE_LABELS = {
|
|
99
|
+
not_installed: "not installed",
|
|
100
|
+
installed: "installed",
|
|
101
|
+
outdated: "older revision; run skills update",
|
|
102
|
+
modified: "changed since install; left alone",
|
|
103
|
+
missing: "receipt only; run skills install",
|
|
104
|
+
not_owned: "not installed by this CLI; left alone",
|
|
105
|
+
receipt_invalid: "receipt not valid",
|
|
106
|
+
unsafe_path: "unsafe path",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// How a coding agent gets an approval URL from login or setup. Many agent
|
|
110
|
+
// shells show output only after the command exits, which is too late.
|
|
111
|
+
const AGENT_APPROVAL_HELP = [
|
|
112
|
+
"From a coding agent: with --json --no-browser, a step that needs approval exits at once",
|
|
113
|
+
"with exit 18 action_required and the URL in data.next_action.url, while a background",
|
|
114
|
+
"process keeps waiting for the approval until --timeout-ms. Show the URL to the person,",
|
|
115
|
+
"and after they approve, run the same command again to continue.",
|
|
85
116
|
];
|
|
86
117
|
|
|
87
118
|
const CONFIG_DIR_OPTION = {
|
|
@@ -112,7 +143,7 @@ const LOGIN_OPTIONS = [
|
|
|
112
143
|
summary: `Time to wait for the browser, ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}. Default ${LOGIN_DEFAULT_TIMEOUT_MS}.`,
|
|
113
144
|
},
|
|
114
145
|
{ 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. With --json,
|
|
146
|
+
{ name: "--no-browser", value: null, summary: "Print the sign in URL on stderr instead of opening a browser. With --json, exit 18 with it in data.next_action (open_url)." },
|
|
116
147
|
{ name: "--reconnect", value: null, summary: "Allow switching a bound project to another origin or workspace." },
|
|
117
148
|
{ name: "--json", value: null, summary: "Print one JSON line on stdout." },
|
|
118
149
|
];
|
|
@@ -257,6 +288,7 @@ export function helpData(topic) {
|
|
|
257
288
|
"metergraph --version [--json]",
|
|
258
289
|
"metergraph doctor [--url ORIGIN] [--timeout-ms N] [--json]",
|
|
259
290
|
...SKILL_USAGE,
|
|
291
|
+
...SKILLS_USAGE,
|
|
260
292
|
LOGIN_USAGE,
|
|
261
293
|
LOGOUT_USAGE,
|
|
262
294
|
SETUP_USAGE,
|
|
@@ -283,6 +315,25 @@ export function helpData(topic) {
|
|
|
283
315
|
"Replace a skill this CLI installed, and that is unchanged since, with the bundled revision.",
|
|
284
316
|
options: SKILL_OPTIONS,
|
|
285
317
|
},
|
|
318
|
+
{
|
|
319
|
+
name: "skills install",
|
|
320
|
+
summary:
|
|
321
|
+
"Copy the Metergraph workflow skills bundled with this CLI, such as the model-swap loop, into one " +
|
|
322
|
+
"client's project skill directory. Never replaces a skill it did not install. No network requests, no sign in.",
|
|
323
|
+
options: SKILL_OPTIONS,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "skills update",
|
|
327
|
+
summary:
|
|
328
|
+
"Replace workflow skills this CLI installed, and that are unchanged since, with the bundled revisions, " +
|
|
329
|
+
"and install skills added since.",
|
|
330
|
+
options: SKILL_OPTIONS,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "skills list",
|
|
334
|
+
summary: "List the bundled workflow skills and whether each is installed for each client. Writes nothing.",
|
|
335
|
+
options: SKILLS_LIST_OPTIONS,
|
|
336
|
+
},
|
|
286
337
|
{
|
|
287
338
|
name: "login",
|
|
288
339
|
summary:
|
|
@@ -311,7 +362,7 @@ export function helpData(topic) {
|
|
|
311
362
|
{ name: "--timeout-ms", value: "N", summary: "Time to wait for browser approval." },
|
|
312
363
|
{ name: "--signup", value: null, summary: "Start at hosted sign up when the project needs login." },
|
|
313
364
|
{ name: "--reconnect", value: null, summary: "Permit switching an existing project binding." },
|
|
314
|
-
{ name: "--no-browser", value: null, summary: "Print approval URL on stderr. With --json,
|
|
365
|
+
{ name: "--no-browser", value: null, summary: "Print approval URL on stderr. With --json, exit 18 with it in data.next_action (open_url); rerun after approval." },
|
|
315
366
|
{ name: "--repair", value: null, summary: "Explicitly approve replacement of an acknowledged key that no longer verifies." },
|
|
316
367
|
{ name: "--deployment", value: "MODEL", summary: "managed, customer-local, byoc or oss. Default managed." },
|
|
317
368
|
{ name: "--confirm-prerequisites", value: null, summary: "Attest deployment prerequisites are met; it does not verify bundle publication." },
|
|
@@ -375,6 +426,7 @@ export function helpText(topic) {
|
|
|
375
426
|
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
376
427
|
lines.push(` ${flag.padEnd(25)}${option.summary}`);
|
|
377
428
|
}
|
|
429
|
+
lines.push("", ...AGENT_APPROVAL_HELP);
|
|
378
430
|
} else if (topic === "login" || topic === "logout") {
|
|
379
431
|
const login = topic === "login";
|
|
380
432
|
lines.push(`Usage: ${login ? LOGIN_USAGE : LOGOUT_USAGE}`, "");
|
|
@@ -407,6 +459,7 @@ export function helpText(topic) {
|
|
|
407
459
|
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
408
460
|
lines.push(` ${flag.padEnd(18)}${option.summary}`);
|
|
409
461
|
}
|
|
462
|
+
if (login) lines.push("", ...AGENT_APPROVAL_HELP);
|
|
410
463
|
} else if (topic === "verify") {
|
|
411
464
|
lines.push(`Usage: ${VERIFY_USAGE}`, "", "Uses the saved Metadata grant to poll for one exact, processed trace.",
|
|
412
465
|
"The source label is supplied by the caller. A Metadata match alone does not prove application traffic.",
|
|
@@ -463,6 +516,40 @@ export function helpText(topic) {
|
|
|
463
516
|
"",
|
|
464
517
|
"Discovery stays pending until the client itself loads the skill.",
|
|
465
518
|
);
|
|
519
|
+
} else if (topic === "skills") {
|
|
520
|
+
lines.push(
|
|
521
|
+
"Usage:",
|
|
522
|
+
...SKILLS_USAGE.map((usage) => ` ${usage}`),
|
|
523
|
+
"",
|
|
524
|
+
"Copies the Metergraph workflow skills bundled with this CLI into one client's project",
|
|
525
|
+
"skill directory, one folder per skill, and records ownership of each in",
|
|
526
|
+
".metergraph/skills/NAME.json. The skills come from https://github.com/metergraph/skills",
|
|
527
|
+
"at the commit this release pins. They cover investigation and the model-swap loop:",
|
|
528
|
+
"choose a workload, choose traces and models, define the eval, run the analysis,",
|
|
529
|
+
"summarize the report and rerun. The setup skill is installed by \"metergraph skill\".",
|
|
530
|
+
"",
|
|
531
|
+
"Writes nothing else. Makes no network requests, does not sign in and does not",
|
|
532
|
+
"configure MCP, client settings, AGENTS.md or CLAUDE.md.",
|
|
533
|
+
"",
|
|
534
|
+
"Options:",
|
|
535
|
+
);
|
|
536
|
+
for (const option of SKILL_OPTIONS) {
|
|
537
|
+
const flag = option.value ? `${option.name} ${option.value}` : option.name;
|
|
538
|
+
lines.push(` ${flag.padEnd(18)}${option.summary}`);
|
|
539
|
+
}
|
|
540
|
+
lines.push(
|
|
541
|
+
"",
|
|
542
|
+
"install never replaces an existing skill. update replaces only skills this CLI",
|
|
543
|
+
"installed and that are unchanged since, and installs skills added since. A conflict",
|
|
544
|
+
"on one skill leaves the others in place. There is no force option.",
|
|
545
|
+
"list takes only --project and reports every client. It never writes.",
|
|
546
|
+
"",
|
|
547
|
+
"Claude Desktop (--client claude-desktop) cannot load project skill files. It exits",
|
|
548
|
+
"with code 6, writes nothing and points to the metergraph/skills plugin marketplace.",
|
|
549
|
+
"ChatGPT and cloud runtimes without a shell get the connection guide instead.",
|
|
550
|
+
"",
|
|
551
|
+
"Discovery stays pending until the client itself loads the skills.",
|
|
552
|
+
);
|
|
466
553
|
} else {
|
|
467
554
|
lines.push(
|
|
468
555
|
`metergraph ${VERSION} (preview)`,
|
|
@@ -472,6 +559,7 @@ export function helpText(topic) {
|
|
|
472
559
|
" metergraph --version [--json] Show the CLI version",
|
|
473
560
|
" metergraph doctor [options] Check a Metergraph service, read only",
|
|
474
561
|
" metergraph skill install|update Install or update the agent skill in a project",
|
|
562
|
+
" metergraph skills install|update|list Workflow skills, such as the model-swap loop",
|
|
475
563
|
" metergraph login [options] Sign in and bind a project to a workspace",
|
|
476
564
|
" metergraph logout [options] Revoke and remove a project's sign in",
|
|
477
565
|
" metergraph setup [options] Approve and write a private ingest key",
|
|
@@ -485,6 +573,7 @@ export function helpText(topic) {
|
|
|
485
573
|
"",
|
|
486
574
|
'Run "metergraph help doctor" for doctor options.',
|
|
487
575
|
'Run "metergraph help skill" for skill options.',
|
|
576
|
+
'Run "metergraph help skills" for workflow skill options.',
|
|
488
577
|
'Run "metergraph help login" or "metergraph help logout" for sign in options.',
|
|
489
578
|
'Run "metergraph help verify" for exact-trace verification options.',
|
|
490
579
|
'Run "metergraph help COMMAND" for status, context, capabilities, usage, routes or traces.',
|
|
@@ -521,6 +610,46 @@ export function skillText(result, message) {
|
|
|
521
610
|
return `${lines.join("\n")}\n`;
|
|
522
611
|
}
|
|
523
612
|
|
|
613
|
+
export function skillsText(result, message) {
|
|
614
|
+
const report = result.data;
|
|
615
|
+
const lines = [];
|
|
616
|
+
if (result.command === "skills list") {
|
|
617
|
+
lines.push("Metergraph skills bundled with this CLI");
|
|
618
|
+
if (report.source) lines.push(`Source: ${report.source.repository} at ${report.source.commit.slice(0, 12)}`);
|
|
619
|
+
if (!result.ok) {
|
|
620
|
+
lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`, message);
|
|
621
|
+
return `${lines.join("\n")}\n`;
|
|
622
|
+
}
|
|
623
|
+
for (const skill of report.skills) {
|
|
624
|
+
lines.push("", `${skill.name} (${skill.revision})`);
|
|
625
|
+
for (const [client, state] of Object.entries(skill.installs)) {
|
|
626
|
+
lines.push(` ${SKILL_CLIENTS[client].label.padEnd(12)}${SKILLS_STATE_LABELS[state] ?? state}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return `${lines.join("\n")}\n`;
|
|
630
|
+
}
|
|
631
|
+
const label = SKILL_CLIENTS[report.client]?.label ?? HANDOFF_SKILL_CLIENTS[report.client] ?? report.client;
|
|
632
|
+
lines.push(`Metergraph ${result.command}: ${label}, ${report.runtime} runtime`);
|
|
633
|
+
if (report.source) lines.push(`Source: ${report.source.repository} at ${report.source.commit.slice(0, 12)}`);
|
|
634
|
+
for (const skill of report.skills) {
|
|
635
|
+
const status = skill.status === "failed" ? `failed (${skill.reason})` : skill.status;
|
|
636
|
+
lines.push(` ${skill.name.padEnd(24)}${status.padEnd(12)}${skill.path}`);
|
|
637
|
+
}
|
|
638
|
+
if (result.ok) {
|
|
639
|
+
lines.push(
|
|
640
|
+
`Discovery: pending until ${label} loads the skills`,
|
|
641
|
+
"Authenticated: no",
|
|
642
|
+
"",
|
|
643
|
+
`Next: ${report.next_action.message}`,
|
|
644
|
+
);
|
|
645
|
+
} else {
|
|
646
|
+
lines.push("", `Result: ${result.outcome} (exit ${result.exit_code})`, message);
|
|
647
|
+
if (report.next_action?.kind === "plugin_marketplace") lines.push(`Next: ${report.next_action.message}`);
|
|
648
|
+
if (report.next_action?.kind === "connection_guide") lines.push(`Connection guide: ${report.next_action.url}`);
|
|
649
|
+
}
|
|
650
|
+
return `${lines.join("\n")}\n`;
|
|
651
|
+
}
|
|
652
|
+
|
|
524
653
|
export function versionData() {
|
|
525
654
|
return { name: PACKAGE_NAME, version: VERSION };
|
|
526
655
|
}
|
|
@@ -601,9 +730,9 @@ const AUTH_MESSAGES = {
|
|
|
601
730
|
"so sign in cannot finish here. Follow the connection guide instead. Nothing was written.",
|
|
602
731
|
ci_environment:
|
|
603
732
|
"This is a CI environment, where no person can approve sign in in a browser. Nothing was written.",
|
|
604
|
-
|
|
605
|
-
"
|
|
606
|
-
"
|
|
733
|
+
browser_approval_required:
|
|
734
|
+
"A person must approve in a browser. Open data.next_action.url on this machine; a background process " +
|
|
735
|
+
"waits for the approval until the timeout. After approval, run the same command again to continue.",
|
|
607
736
|
bound_to_other_origin:
|
|
608
737
|
"This project is bound to a different origin. Nothing was changed. Use --reconnect to switch it.",
|
|
609
738
|
bound_to_other_workspace:
|
|
@@ -619,7 +748,7 @@ const AUTH_MESSAGES = {
|
|
|
619
748
|
timeout: "The operation did not finish within the time limit. Nothing was saved.",
|
|
620
749
|
cancelled: "Sign in was cancelled. Nothing was saved.",
|
|
621
750
|
browser_unavailable:
|
|
622
|
-
"The browser could not be opened. Run login again with --no-browser
|
|
751
|
+
"The browser could not be opened. Run login again with --no-browser and open the URL it prints.",
|
|
623
752
|
oauth_metadata_missing: "This service does not support CLI sign in yet. Nothing was written.",
|
|
624
753
|
metadata_scope_unsupported:
|
|
625
754
|
"This service does not offer Metadata-only access for the CLI yet. The CLI does not fall back to broader access.",
|