metergraph-cli 0.1.0 → 0.2.0-preview.1
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 +500 -33
- package/assets/skill/SKILL.md +17 -1
- package/assets/skill/manifest.json +3 -3
- package/package.json +2 -2
- package/src/args.js +323 -17
- package/src/auth-binding.js +202 -0
- package/src/auth-browser.js +74 -0
- package/src/auth-callback.js +177 -0
- package/src/auth-login.js +410 -0
- package/src/auth-oauth.js +462 -0
- package/src/auth-session.js +174 -0
- package/src/auth-store.js +397 -0
- package/src/cli.js +78 -2
- package/src/constants.js +99 -4
- package/src/deployment-credential.js +207 -0
- package/src/deployment-route.js +449 -0
- package/src/doctor.js +10 -0
- package/src/http.js +1 -1
- package/src/output.js +454 -2
- package/src/read-contract.js +609 -0
- package/src/read-output.js +216 -0
- package/src/read.js +325 -0
- package/src/setup-deployment.js +178 -0
- package/src/setup-env-acl.js +99 -0
- package/src/setup-env-git.js +103 -0
- package/src/setup-env-parse.js +169 -0
- package/src/setup-env.js +685 -0
- package/src/setup-state.js +141 -0
- package/src/setup.js +326 -0
- package/src/skill-bundle.js +1 -1
- package/src/trace-contract.js +129 -0
- package/src/trace-open.js +36 -0
- package/src/transport.js +171 -0
- package/src/verify-output.js +36 -0
- package/src/verify.js +120 -0
package/assets/skill/SKILL.md
CHANGED
|
@@ -12,22 +12,38 @@ Connection and troubleshooting: https://www.metergraph.dev/docs/guides/agent-acc
|
|
|
12
12
|
|
|
13
13
|
Ask for or confirm these non-secret choices. Do not guess from the agent brand:
|
|
14
14
|
|
|
15
|
-
1. Client: Claude Desktop, Claude Code, ChatGPT or
|
|
15
|
+
1. Client: Claude Desktop, Claude Code, ChatGPT, Codex or Cursor.
|
|
16
16
|
2. Execution runtime: customer machine or cloud. Codex local app/CLI/IDE is distinct from cloud execution. Claude Desktop remote connectors execute in Anthropic's cloud; Desktop local MCP is a separate mechanism. ChatGPT MCP apps execute in the cloud.
|
|
17
17
|
3. Deployment: Metergraph hosted cloud, the signed commercial customer-local bundle, a customer-owned AWS installation (`byoc-core`), or the open-source self-hosted server. These have different addresses, accounts and credentials. Staging is not a customer setup path.
|
|
18
18
|
4. Fresh workspace/installation or connecting an existing workspace. Ask for the intended workspace name and deployment origin, never a token.
|
|
19
19
|
|
|
20
20
|
Use the routing below with those four choices. Follow the matching credential and client configuration instructions in the connection guide.
|
|
21
21
|
|
|
22
|
+
For a first application trace, check the installed CLI's version and help before
|
|
23
|
+
using it. `metergraph-cli@0.1.0` supports `doctor` and project skill
|
|
24
|
+
installation; later versions may also offer `login`, `setup` and `verify`.
|
|
25
|
+
Use those commands only if the installed package lists them. Use the [first
|
|
26
|
+
trace guide](https://www.metergraph.dev/docs/start/first-trace/) for steps the
|
|
27
|
+
package does not support. A CLI login or health probe alone does not prove
|
|
28
|
+
application traffic. Verification needs an exact trace or request ID from an
|
|
29
|
+
application invocation, its time window and the intended workspace.
|
|
30
|
+
|
|
22
31
|
## Route honestly
|
|
23
32
|
|
|
24
33
|
| Client and runtime | Hosted | Customer-local bundle | Customer AWS | Open source self-hosted |
|
|
25
34
|
| --- | --- | --- | --- | --- |
|
|
26
35
|
| Claude Code or Codex on the customer machine | Keyed HTTP at `https://app.metergraph.dev/v1/agent/mcp` | Keyed HTTP at the installed local address, normally `http://127.0.0.1:8080/v1/agent/mcp` | Keyed HTTP at the customer's reachable installation address | Static `MG_AGENT_TOKENS` bearer at the OSS server address, normally `http://localhost:8787/v1/agent/mcp` |
|
|
36
|
+
| Cursor on the customer machine | Project skill supported; MCP connection not validated in this guide | Project skill supported; MCP connection not validated in this guide | Project skill supported; MCP connection not validated in this guide | Project skill supported; MCP connection not validated in this guide |
|
|
27
37
|
| Claude Desktop remote connector on an individual account | Custom connector with a scoped bearer key if Request headers are available for the account; verify tools before claiming success | Cannot reach localhost from Anthropic's cloud | Default private installation is unreachable from Anthropic's cloud; no validated public route in this guide | Cannot reach localhost from Anthropic's cloud |
|
|
28
38
|
| Claude Desktop local MCP | Separate local extension required; not supported by this guide | Docker does not install a host extension | Separate local extension required; not supported by this guide | Docker does not install a host extension |
|
|
29
39
|
| ChatGPT MCP app or cloud execution | Separate cloud client configuration; not supported by this guide | Cannot reach localhost | Default private installation is unreachable | Cannot reach localhost |
|
|
30
40
|
|
|
41
|
+
For Cursor, the published CLI can install a project skill with
|
|
42
|
+
`metergraph skill install --client cursor --runtime local` when installed.
|
|
43
|
+
Confirm Cursor discovers the skill. This does not configure MCP or sign in.
|
|
44
|
+
Use the first trace guide for instrumentation, and report the read connection
|
|
45
|
+
as unverified until a supported Cursor MCP route is tested.
|
|
46
|
+
|
|
31
47
|
For hosted Claude Desktop on an individual account, guide the human through Customize → Connectors → Add custom connector with `https://app.metergraph.dev/v1/agent/mcp`. They create a coding-agent key in the intended hosted workspace. If Request headers are available, choose No sign-in and enter a required header named `Authorization` with value `Bearer <key>` in the connector's private settings, never in chat. Request headers are in beta and may be unavailable for the account; do not claim the route works without them. They must enable the connector in a conversation. Team and Enterprise connectors may share fixed credentials across users, so do not put one person's workspace key in a shared connector. Do not present OAuth server code, a tool listing, local CLI installation or a saved configuration as connection success. Do not invent a released plugin, extension, tunnel or signup path. If a route is blocked, name the blocker and offer the customer-machine Claude Code/Codex keyed HTTP route or hosted deployment as appropriate. Do not expose localhost publicly or disable authentication as a workaround.
|
|
32
48
|
|
|
33
49
|
For a fresh hosted workspace, first ask whether the human already has a Metergraph account and workspace. Direct them to https://app.metergraph.dev/ and the signup, invitation or sign-in flow actually offered. The hosted UI can offer "Get a free API key" for self-service signup, but do not assume that button or a workspace invitation is available to everyone. If access is unavailable, direct them to their workspace owner or Metergraph. Confirm the workspace before creating a coding-agent key.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "metergraph",
|
|
4
4
|
"file": "SKILL.md",
|
|
5
5
|
"source_url": "https://www.metergraph.dev/SKILL.md",
|
|
6
|
-
"sha256": "
|
|
7
|
-
"size":
|
|
8
|
-
"revision": "sha256-
|
|
6
|
+
"sha256": "57b920677adf62759c7221629327192a2d16b7e6034f7948ffd96cee402d4891",
|
|
7
|
+
"size": 10426,
|
|
8
|
+
"revision": "sha256-57b920677adf"
|
|
9
9
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metergraph-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Preview Metergraph command line tool with a read-only connection probe
|
|
3
|
+
"version": "0.2.0-preview.1",
|
|
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": {
|
|
7
7
|
"metergraph": "bin/metergraph.js"
|
package/src/args.js
CHANGED
|
@@ -3,27 +3,96 @@ import {
|
|
|
3
3
|
DEFAULT_TIMEOUT_MS,
|
|
4
4
|
HANDOFF_SKILL_CLIENTS,
|
|
5
5
|
HANDOFF_SKILL_RUNTIMES,
|
|
6
|
+
HANDOFF_LOGIN_RUNTIMES,
|
|
7
|
+
LOGIN_DEFAULT_TIMEOUT_MS,
|
|
8
|
+
LOGIN_MAX_TIMEOUT_MS,
|
|
9
|
+
LOGIN_MIN_TIMEOUT_MS,
|
|
10
|
+
LOGIN_RUNTIMES,
|
|
6
11
|
MAX_TIMEOUT_MS,
|
|
7
12
|
MIN_TIMEOUT_MS,
|
|
13
|
+
READ_DEFAULT_DAYS,
|
|
14
|
+
READ_DEFAULT_LIMIT,
|
|
15
|
+
READ_DEFAULT_TIMEOUT_MS,
|
|
16
|
+
READ_MAX_DAYS,
|
|
17
|
+
READ_MAX_LIMIT,
|
|
18
|
+
READ_MAX_TIMEOUT_MS,
|
|
19
|
+
READ_MIN_TIMEOUT_MS,
|
|
8
20
|
SKILL_CLIENTS,
|
|
9
21
|
SKILL_RUNTIMES,
|
|
22
|
+
TRACES_DEFAULT_LIMIT,
|
|
10
23
|
} from "./constants.js";
|
|
11
24
|
import { parseOrigin } from "./origin.js";
|
|
25
|
+
import { isCursor, isSafeFilter } from "./read-contract.js";
|
|
12
26
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
27
|
+
export const READ_COMMANDS = Object.freeze(["status", "context", "capabilities", "usage", "routes", "traces"]);
|
|
28
|
+
const COMMANDS = new Set(["doctor", "help", "skill", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
|
|
29
|
+
const HELP_TOPICS = new Set(["doctor", "skill", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
|
|
15
30
|
const SKILL_ACTIONS = new Set(["install", "update"]);
|
|
31
|
+
const READ_BASE = ["--project", "--config-dir", "--timeout-ms"];
|
|
16
32
|
const OPTIONS = {
|
|
17
33
|
doctor: new Set(["--url", "--timeout-ms"]),
|
|
18
34
|
skill: new Set(["--client", "--runtime", "--project"]),
|
|
35
|
+
login: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--timeout-ms"]),
|
|
36
|
+
setup: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--env-file", "--client", "--timeout-ms", "--deployment", "--agent-token-file"]),
|
|
37
|
+
logout: new Set(["--project", "--config-dir"]),
|
|
38
|
+
status: new Set(READ_BASE),
|
|
39
|
+
context: new Set(READ_BASE),
|
|
40
|
+
capabilities: new Set(READ_BASE),
|
|
41
|
+
usage: new Set([...READ_BASE, "--days", "--limit"]),
|
|
42
|
+
routes: new Set([...READ_BASE, "--limit"]),
|
|
43
|
+
traces: new Set([...READ_BASE, "--days", "--limit", "--route", "--status", "--cursor"]),
|
|
44
|
+
verify: new Set([...READ_BASE, "--trace-id", "--request-id", "--since", "--until", "--source", "--days", "--poll-ms", "--max-attempts"]),
|
|
19
45
|
};
|
|
46
|
+
// Requests the read commands recognize and refuse, so a script gets an
|
|
47
|
+
// explicit unsupported result instead of an unknown argument or, worse, a
|
|
48
|
+
// broader query than it asked for. Nothing is sent for them.
|
|
49
|
+
const REFUSED = {
|
|
50
|
+
"--environment": { value: true, reason: "environment_selector_unsupported" },
|
|
51
|
+
"--workload": { value: true, reason: "workload_filter_unsupported" },
|
|
52
|
+
"--since": { value: true, reason: "time_range_unsupported" },
|
|
53
|
+
"--until": { value: true, reason: "time_range_unsupported" },
|
|
54
|
+
"--sql": { value: true, reason: "query_unsupported" },
|
|
55
|
+
"--query": { value: true, reason: "query_unsupported" },
|
|
56
|
+
"--content": { value: false, reason: "content_access_unsupported" },
|
|
57
|
+
"--include-content": { value: false, reason: "content_access_unsupported" },
|
|
58
|
+
"--debug": { value: false, reason: "content_access_unsupported" },
|
|
59
|
+
"--replay": { value: false, reason: "content_access_unsupported" },
|
|
60
|
+
};
|
|
61
|
+
const REFUSED_MESSAGES = {
|
|
62
|
+
environment_selector_unsupported:
|
|
63
|
+
"--environment is not supported: the service's agent access contract has no environment selector, " +
|
|
64
|
+
"so the CLI never sends one. Results cover the bound workspace. No request was made.",
|
|
65
|
+
workload_filter_unsupported:
|
|
66
|
+
"--workload is not supported by this CLI version: it cannot verify from the returned traces that a " +
|
|
67
|
+
"workload filter was applied, so it never sends one. No request was made.",
|
|
68
|
+
time_range_unsupported:
|
|
69
|
+
"--since and --until are not supported. Use --days N (1 to 90) for a window relative to now. No request was made.",
|
|
70
|
+
query_unsupported:
|
|
71
|
+
"Free-form queries are not supported. Use the fixed options of this command. No request was made.",
|
|
72
|
+
content_access_unsupported:
|
|
73
|
+
"Read commands return Metadata only. They never read retained content, debug data or replays. No request was made.",
|
|
74
|
+
};
|
|
75
|
+
// Options that take no value.
|
|
76
|
+
const FLAGS = {
|
|
77
|
+
login: new Set(["--signup", "--no-browser", "--reconnect"]),
|
|
78
|
+
setup: new Set(["--no-browser", "--repair", "--signup", "--reconnect", "--skip-skill", "--confirm-prerequisites"]),
|
|
79
|
+
verify: new Set(["--open", "--no-browser"]),
|
|
80
|
+
};
|
|
81
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
82
|
|
|
21
83
|
// Parses argv into one of:
|
|
22
84
|
// { ok: true, command: "help", topic, json }
|
|
23
85
|
// { ok: true, command: "version", json }
|
|
24
86
|
// { ok: true, command: "doctor", origin, timeoutMs, json }
|
|
25
87
|
// { ok: true, command: "skill", action, client, runtime, project, json }
|
|
26
|
-
// { ok:
|
|
88
|
+
// { ok: true, command: "login", runtime, origin, workspace, project,
|
|
89
|
+
// configDir, timeoutMs, signup, noBrowser, reconnect, json }
|
|
90
|
+
// { ok: true, command: "logout", project, configDir, json }
|
|
91
|
+
// { ok: true, command: one of READ_COMMANDS, project, configDir, timeoutMs,
|
|
92
|
+
// days, limit, route, status, cursor, json }
|
|
93
|
+
// { ok: false, command, json, code, message, outcome }
|
|
94
|
+
// outcome is invalid_input, or unsupported for a recognized request the read
|
|
95
|
+
// commands refuse (such as --environment).
|
|
27
96
|
// Error messages are fixed strings. They never contain an argument value,
|
|
28
97
|
// because a mistyped argument can be a credential.
|
|
29
98
|
export function parseArgs(argv) {
|
|
@@ -39,13 +108,15 @@ export function parseArgs(argv) {
|
|
|
39
108
|
let topic = null;
|
|
40
109
|
let version = false;
|
|
41
110
|
const values = {};
|
|
111
|
+
const flags = new Set();
|
|
42
112
|
|
|
43
|
-
const fail = (code, message) => ({
|
|
113
|
+
const fail = (code, message, outcome = "invalid_input") => ({
|
|
44
114
|
ok: false,
|
|
45
115
|
command: command === "skill" && action !== null ? `skill ${action}` : command ?? (version ? "version" : null),
|
|
46
116
|
json,
|
|
47
117
|
code,
|
|
48
118
|
message,
|
|
119
|
+
outcome,
|
|
49
120
|
});
|
|
50
121
|
|
|
51
122
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -71,9 +142,24 @@ export function parseArgs(argv) {
|
|
|
71
142
|
continue;
|
|
72
143
|
}
|
|
73
144
|
|
|
74
|
-
if (command
|
|
145
|
+
if (READ_COMMANDS.includes(command)) {
|
|
146
|
+
const equals = arg.indexOf("=");
|
|
147
|
+
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
148
|
+
if (Object.hasOwn(REFUSED, name)) {
|
|
149
|
+
const { reason } = REFUSED[name];
|
|
150
|
+
return fail(reason, REFUSED_MESSAGES[reason], "unsupported");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Object.hasOwn(OPTIONS, command ?? "")) {
|
|
75
155
|
const equals = arg.indexOf("=");
|
|
76
156
|
const name = equals === -1 ? arg : arg.slice(0, equals);
|
|
157
|
+
if (FLAGS[command]?.has(name)) {
|
|
158
|
+
if (equals !== -1) return fail("unexpected_value", `${name} does not take a value.`);
|
|
159
|
+
if (flags.has(name)) return fail("duplicate_option", `${name} may be given only once.`);
|
|
160
|
+
flags.add(name);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
77
163
|
if (OPTIONS[command].has(name)) {
|
|
78
164
|
let value;
|
|
79
165
|
if (equals === -1) {
|
|
@@ -116,18 +202,19 @@ export function parseArgs(argv) {
|
|
|
116
202
|
return { ok: true, command: "help", topic, json };
|
|
117
203
|
}
|
|
118
204
|
if (command === "skill") return parseSkill(action, values, json, fail);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"Credentials, paths, queries and fragments are not accepted.",
|
|
128
|
-
);
|
|
205
|
+
if (command === "login") return parseLogin(values, flags, json, fail);
|
|
206
|
+
if (command === "setup") return parseSetup(values, flags, json, fail);
|
|
207
|
+
if (command === "verify") return parseVerify(values, flags, json, fail);
|
|
208
|
+
if (READ_COMMANDS.includes(command)) return parseRead(command, values, json, fail);
|
|
209
|
+
if (command === "logout") {
|
|
210
|
+
const paths = parsePaths(values, fail);
|
|
211
|
+
if (!paths.ok) return paths;
|
|
212
|
+
return { ok: true, command: "logout", project: paths.project, configDir: paths.configDir, json };
|
|
129
213
|
}
|
|
130
214
|
|
|
215
|
+
const origin = parseUrlOption(values);
|
|
216
|
+
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
217
|
+
|
|
131
218
|
const rawTimeout = values["--timeout-ms"];
|
|
132
219
|
const timeoutMs =
|
|
133
220
|
rawTimeout === undefined ? DEFAULT_TIMEOUT_MS : parseTimeout(rawTimeout);
|
|
@@ -141,6 +228,39 @@ export function parseArgs(argv) {
|
|
|
141
228
|
return { ok: true, command: "doctor", origin, timeoutMs, json };
|
|
142
229
|
}
|
|
143
230
|
|
|
231
|
+
function parseVerify(values, flags, json, fail) {
|
|
232
|
+
const paths = parsePaths(values, fail);
|
|
233
|
+
if (!paths.ok) return paths;
|
|
234
|
+
const traceId = values["--trace-id"] ?? null;
|
|
235
|
+
const requestId = values["--request-id"] ?? null;
|
|
236
|
+
if ((traceId === null) === (requestId === null) ||
|
|
237
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(traceId ?? requestId)) {
|
|
238
|
+
return fail("invalid_trace_identity", "Provide exactly one --trace-id or --request-id (1 to 200 safe characters).");
|
|
239
|
+
}
|
|
240
|
+
const since = values["--since"];
|
|
241
|
+
const until = values["--until"];
|
|
242
|
+
const validTime = (value) => typeof value === "string" && value.length <= 40 &&
|
|
243
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
|
|
244
|
+
if (!validTime(since) || !validTime(until) || Date.parse(since) > Date.parse(until) || Date.parse(until) > Date.now()) {
|
|
245
|
+
return fail("invalid_invocation_window", "--since and --until must bound a past invocation using ISO 8601 timestamps.");
|
|
246
|
+
}
|
|
247
|
+
const source = values["--source"] ?? "unspecified";
|
|
248
|
+
if (!["application", "synthetic", "demo", "import", "unspecified"].includes(source)) {
|
|
249
|
+
return fail("invalid_source", "--source must be application, synthetic, demo, import or unspecified.");
|
|
250
|
+
}
|
|
251
|
+
const days = values["--days"] === undefined ? undefined : parseBounded(values["--days"], 1, 90);
|
|
252
|
+
if (days === null) return fail("invalid_days", "--days must be a whole number from 1 to 90.");
|
|
253
|
+
const timeoutMs = values["--timeout-ms"] === undefined ? 30000 : parseTimeout(values["--timeout-ms"], 100, 60000);
|
|
254
|
+
if (timeoutMs === null) return fail("invalid_timeout", "--timeout-ms must be a whole number from 100 to 60000.");
|
|
255
|
+
const pollIntervalMs = values["--poll-ms"] === undefined ? 1000 : parseTimeout(values["--poll-ms"], 100, 10000);
|
|
256
|
+
if (pollIntervalMs === null) return fail("invalid_poll_interval", "--poll-ms must be a whole number from 100 to 10000.");
|
|
257
|
+
const maxAttempts = values["--max-attempts"] === undefined ? 30 : parseBounded(values["--max-attempts"], 1, 60);
|
|
258
|
+
if (maxAttempts === null) return fail("invalid_max_attempts", "--max-attempts must be a whole number from 1 to 60.");
|
|
259
|
+
return { ok: true, command: "verify", project: paths.project, configDir: paths.configDir,
|
|
260
|
+
traceId, requestId, since, until, source, days, timeoutMs, pollIntervalMs, maxAttempts,
|
|
261
|
+
open: flags.has("--open"), noBrowser: flags.has("--no-browser"), json };
|
|
262
|
+
}
|
|
263
|
+
|
|
144
264
|
// Client and runtime are required, so a script states where the skill is
|
|
145
265
|
// used instead of the CLI guessing. Values for clients and runtimes that
|
|
146
266
|
// cannot use project skill files are accepted here and handed off later.
|
|
@@ -172,9 +292,195 @@ function parseSkill(action, values, json, fail) {
|
|
|
172
292
|
return { ok: true, command: "skill", action, client, runtime, project: project ?? null, json };
|
|
173
293
|
}
|
|
174
294
|
|
|
175
|
-
|
|
295
|
+
const INVALID_URL =
|
|
296
|
+
"--url must be a bare https origin such as https://metergraph.example.com, " +
|
|
297
|
+
"or an http origin on localhost, 127.0.0.1 or [::1]. " +
|
|
298
|
+
"Credentials, paths, queries and fragments are not accepted.";
|
|
299
|
+
|
|
300
|
+
function parseUrlOption(values) {
|
|
301
|
+
const raw = values["--url"];
|
|
302
|
+
return raw === undefined ? DEFAULT_ORIGIN : parseOrigin(raw);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The runtime is required so a script states where the browser is. Cloud
|
|
306
|
+
// runtimes are accepted here and handed off later, before any request.
|
|
307
|
+
function parseLogin(values, flags, json, fail) {
|
|
308
|
+
const runtime = values["--runtime"];
|
|
309
|
+
if (runtime === undefined) return fail("missing_runtime", "--runtime is required. Use local.");
|
|
310
|
+
if (!LOGIN_RUNTIMES.includes(runtime) && !HANDOFF_LOGIN_RUNTIMES.includes(runtime)) {
|
|
311
|
+
return fail("invalid_runtime", "--runtime must be local, cloud or cloud-no-shell.");
|
|
312
|
+
}
|
|
313
|
+
const origin = parseUrlOption(values);
|
|
314
|
+
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
315
|
+
const rawWorkspace = values["--workspace"];
|
|
316
|
+
if (rawWorkspace !== undefined && !UUID.test(rawWorkspace)) {
|
|
317
|
+
return fail("invalid_workspace", "--workspace must be a workspace ID in UUID form.");
|
|
318
|
+
}
|
|
319
|
+
const paths = parsePaths(values, fail);
|
|
320
|
+
if (!paths.ok) return paths;
|
|
321
|
+
const rawTimeout = values["--timeout-ms"];
|
|
322
|
+
const timeoutMs =
|
|
323
|
+
rawTimeout === undefined
|
|
324
|
+
? LOGIN_DEFAULT_TIMEOUT_MS
|
|
325
|
+
: parseTimeout(rawTimeout, LOGIN_MIN_TIMEOUT_MS, LOGIN_MAX_TIMEOUT_MS);
|
|
326
|
+
if (timeoutMs === null) {
|
|
327
|
+
return fail(
|
|
328
|
+
"invalid_timeout",
|
|
329
|
+
`--timeout-ms must be a whole number from ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
command: "login",
|
|
335
|
+
runtime,
|
|
336
|
+
origin,
|
|
337
|
+
workspace: rawWorkspace === undefined ? null : rawWorkspace.toLowerCase(),
|
|
338
|
+
project: paths.project,
|
|
339
|
+
configDir: paths.configDir,
|
|
340
|
+
timeoutMs,
|
|
341
|
+
signup: flags.has("--signup"),
|
|
342
|
+
noBrowser: flags.has("--no-browser"),
|
|
343
|
+
reconnect: flags.has("--reconnect"),
|
|
344
|
+
json,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseSetup(values, flags, json, fail) {
|
|
349
|
+
const runtime = values["--runtime"];
|
|
350
|
+
if (runtime === undefined) return fail("missing_runtime", "--runtime is required. Use local.");
|
|
351
|
+
if (!LOGIN_RUNTIMES.includes(runtime) && !HANDOFF_LOGIN_RUNTIMES.includes(runtime)) {
|
|
352
|
+
return fail("invalid_runtime", "--runtime must be local, cloud or cloud-no-shell.");
|
|
353
|
+
}
|
|
354
|
+
const paths = parsePaths(values, fail);
|
|
355
|
+
if (!paths.ok) return paths;
|
|
356
|
+
const origin = parseUrlOption(values);
|
|
357
|
+
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
358
|
+
const deployment = values["--deployment"] ?? "managed";
|
|
359
|
+
if (!["managed", "customer-local", "byoc", "oss"].includes(deployment)) {
|
|
360
|
+
return fail("invalid_deployment", "--deployment must be managed, customer-local, byoc or oss.");
|
|
361
|
+
}
|
|
362
|
+
if (deployment === "managed" && (flags.has("--confirm-prerequisites") || values["--agent-token-file"] !== undefined)) {
|
|
363
|
+
return fail("managed_route_conflict", "Operator prerequisites and agent token files are only for non-hosted deployments.");
|
|
364
|
+
}
|
|
365
|
+
if (deployment !== "managed" && values["--url"] === undefined) {
|
|
366
|
+
return fail("non_hosted_origin_required", "Non-hosted setup requires an explicit --url for the installed service.");
|
|
367
|
+
}
|
|
368
|
+
const rawWorkspace = values["--workspace"];
|
|
369
|
+
if (rawWorkspace !== undefined && !UUID.test(rawWorkspace)) {
|
|
370
|
+
return fail("invalid_workspace", "--workspace must be a workspace ID in UUID form.");
|
|
371
|
+
}
|
|
372
|
+
if (deployment !== "managed" && rawWorkspace === undefined) {
|
|
373
|
+
return fail("non_hosted_workspace_required", "Non-hosted setup requires an explicit --workspace UUID.");
|
|
374
|
+
}
|
|
375
|
+
const agentTokenFile = values["--agent-token-file"] ?? null;
|
|
376
|
+
if (agentTokenFile !== null && (agentTokenFile === "" || agentTokenFile.includes("\0"))) {
|
|
377
|
+
return fail("invalid_agent_token_file", "--agent-token-file must name a private absolute file.");
|
|
378
|
+
}
|
|
379
|
+
const client = values["--client"] ?? null;
|
|
380
|
+
if (client === null && !flags.has("--skip-skill")) {
|
|
381
|
+
return fail("client_required", "Choose --client codex, claude or cursor, or explicitly use --skip-skill.");
|
|
382
|
+
}
|
|
383
|
+
if (client !== null && !Object.hasOwn(SKILL_CLIENTS, client)) {
|
|
384
|
+
return fail("invalid_client", "--client must be codex, claude or cursor.");
|
|
385
|
+
}
|
|
386
|
+
if (client !== null && flags.has("--skip-skill")) {
|
|
387
|
+
return fail("client_conflict", "Use either --client or --skip-skill, not both.");
|
|
388
|
+
}
|
|
389
|
+
const envFile = values["--env-file"] ?? ".env";
|
|
390
|
+
if (envFile === "" || envFile.includes("\0")) return fail("invalid_env_file", "--env-file must name a project-relative env file.");
|
|
391
|
+
const rawTimeout = values["--timeout-ms"];
|
|
392
|
+
const timeoutMs = rawTimeout === undefined ? LOGIN_DEFAULT_TIMEOUT_MS :
|
|
393
|
+
parseTimeout(rawTimeout, LOGIN_MIN_TIMEOUT_MS, LOGIN_MAX_TIMEOUT_MS);
|
|
394
|
+
if (timeoutMs === null) return fail("invalid_timeout", `--timeout-ms must be a whole number from ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}.`);
|
|
395
|
+
return { ok: true, command: "setup", runtime, origin, originExplicit: values["--url"] !== undefined,
|
|
396
|
+
deployment, confirmPrerequisites: flags.has("--confirm-prerequisites"), agentTokenFile,
|
|
397
|
+
workspace: rawWorkspace === undefined ? null : rawWorkspace.toLowerCase(),
|
|
398
|
+
project: paths.project, configDir: paths.configDir, envFile, client, skipSkill: flags.has("--skip-skill"),
|
|
399
|
+
timeoutMs, noBrowser: flags.has("--no-browser"), repair: flags.has("--repair"),
|
|
400
|
+
signup: flags.has("--signup"), reconnect: flags.has("--reconnect"), json };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Read commands take the project and config directory, one total timeout and,
|
|
404
|
+
// where the endpoint supports them, bounded days, limit and trace filters.
|
|
405
|
+
// Out of range values fail here; nothing is clamped.
|
|
406
|
+
function parseRead(command, values, json, fail) {
|
|
407
|
+
const paths = parsePaths(values, fail);
|
|
408
|
+
if (!paths.ok) return paths;
|
|
409
|
+
const rawTimeout = values["--timeout-ms"];
|
|
410
|
+
const timeoutMs =
|
|
411
|
+
rawTimeout === undefined
|
|
412
|
+
? READ_DEFAULT_TIMEOUT_MS
|
|
413
|
+
: parseTimeout(rawTimeout, READ_MIN_TIMEOUT_MS, READ_MAX_TIMEOUT_MS);
|
|
414
|
+
if (timeoutMs === null) {
|
|
415
|
+
return fail(
|
|
416
|
+
"invalid_timeout",
|
|
417
|
+
`--timeout-ms must be a whole number from ${READ_MIN_TIMEOUT_MS} to ${READ_MAX_TIMEOUT_MS}.`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const takesDays = command === "usage" || command === "traces";
|
|
421
|
+
const takesLimit = takesDays || command === "routes";
|
|
422
|
+
let days = null;
|
|
423
|
+
if (takesDays) {
|
|
424
|
+
days = values["--days"] === undefined ? READ_DEFAULT_DAYS : parseBounded(values["--days"], 1, READ_MAX_DAYS);
|
|
425
|
+
if (days === null) return fail("invalid_days", `--days must be a whole number from 1 to ${READ_MAX_DAYS}.`);
|
|
426
|
+
}
|
|
427
|
+
let limit = null;
|
|
428
|
+
if (takesLimit) {
|
|
429
|
+
const fallback = command === "traces" ? TRACES_DEFAULT_LIMIT : READ_DEFAULT_LIMIT;
|
|
430
|
+
limit = values["--limit"] === undefined ? fallback : parseBounded(values["--limit"], 1, READ_MAX_LIMIT);
|
|
431
|
+
if (limit === null) return fail("invalid_limit", `--limit must be a whole number from 1 to ${READ_MAX_LIMIT}.`);
|
|
432
|
+
}
|
|
433
|
+
const route = values["--route"] ?? null;
|
|
434
|
+
if (route !== null && !isSafeFilter(route)) {
|
|
435
|
+
return fail("invalid_route", "--route must be 1 to 256 characters without control characters.");
|
|
436
|
+
}
|
|
437
|
+
const status = values["--status"] ?? null;
|
|
438
|
+
if (status !== null && status !== "success" && status !== "error") {
|
|
439
|
+
return fail("invalid_status", "--status must be success or error.");
|
|
440
|
+
}
|
|
441
|
+
const cursor = values["--cursor"] ?? null;
|
|
442
|
+
if (cursor !== null && !isCursor(cursor)) {
|
|
443
|
+
return fail(
|
|
444
|
+
"invalid_cursor",
|
|
445
|
+
"--cursor must be the next_cursor value from an earlier traces result, at most 512 printable characters.",
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
command,
|
|
451
|
+
project: paths.project,
|
|
452
|
+
configDir: paths.configDir,
|
|
453
|
+
timeoutMs,
|
|
454
|
+
days,
|
|
455
|
+
limit,
|
|
456
|
+
route,
|
|
457
|
+
status,
|
|
458
|
+
cursor,
|
|
459
|
+
json,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function parseBounded(raw, min, max) {
|
|
464
|
+
if (!/^[0-9]{1,3}$/.test(raw)) return null;
|
|
465
|
+
const value = Number(raw);
|
|
466
|
+
return value < min || value > max ? null : value;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parsePaths(values, fail) {
|
|
470
|
+
const project = values["--project"];
|
|
471
|
+
if (project !== undefined && (project === "" || project.includes("\0"))) {
|
|
472
|
+
return fail("invalid_project", "--project must name an existing directory.");
|
|
473
|
+
}
|
|
474
|
+
const configDir = values["--config-dir"];
|
|
475
|
+
if (configDir !== undefined && (configDir === "" || configDir.includes("\0"))) {
|
|
476
|
+
return fail("invalid_config_dir", "--config-dir must name a directory path.");
|
|
477
|
+
}
|
|
478
|
+
return { ok: true, project: project ?? null, configDir: configDir ?? null };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function parseTimeout(raw, min = MIN_TIMEOUT_MS, max = MAX_TIMEOUT_MS) {
|
|
176
482
|
if (!/^[0-9]{1,6}$/.test(raw)) return null;
|
|
177
483
|
const value = Number(raw);
|
|
178
|
-
if (value <
|
|
484
|
+
if (value < min || value > max) return null;
|
|
179
485
|
return value;
|
|
180
486
|
}
|