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.
@@ -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 Codex.
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": "90f7d8d78a5b0b7a57436f194222f0c73310b0b04201c297c8fbf0b00ad6bb3f",
7
- "size": 9149,
8
- "revision": "sha256-90f7d8d78a5b"
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.1.0",
4
- "description": "Preview Metergraph command line tool with a read-only connection probe and a project skill installer",
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 COMMANDS = new Set(["doctor", "help", "skill"]);
14
- const HELP_TOPICS = new Set(["doctor", "skill"]);
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: false, command, json, code, message }
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 === "doctor" || command === "skill") {
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
- const rawUrl = values["--url"];
121
- const origin = rawUrl === undefined ? DEFAULT_ORIGIN : parseOrigin(rawUrl);
122
- if (origin === null) {
123
- return fail(
124
- "invalid_url",
125
- "--url must be a bare https origin such as https://metergraph.example.com, " +
126
- "or an http origin on localhost, 127.0.0.1 or [::1]. " +
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
- function parseTimeout(raw) {
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 < MIN_TIMEOUT_MS || value > MAX_TIMEOUT_MS) return null;
484
+ if (value < min || value > max) return null;
179
485
  return value;
180
486
  }