metergraph-cli 0.2.2 → 0.2.4
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 +121 -13
- package/assets/skill/SKILL.md +124 -31
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +31 -31
- package/assets/skills/metergraph-analyze/SKILL.md +161 -58
- package/assets/skills/metergraph-candidates/SKILL.md +50 -8
- package/assets/skills/metergraph-evals/SKILL.md +83 -16
- package/assets/skills/metergraph-investigate/SKILL.md +90 -34
- package/assets/skills/metergraph-iterate/SKILL.md +82 -12
- package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
- package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
- package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
- package/assets/skills/metergraph-report/SKILL.md +60 -15
- package/assets/skills/metergraph-workloads/SKILL.md +97 -13
- package/bin/metergraph.js +1 -1
- package/native/README.md +85 -0
- package/native/anchored-fs.c +243 -0
- package/native/bin/anchored-fs-darwin-arm64 +0 -0
- package/native/bin/anchored-fs-darwin-x64 +0 -0
- package/native/bin/anchored-fs-linux-arm64 +0 -0
- package/native/bin/anchored-fs-linux-x64 +0 -0
- package/native/build.mjs +55 -0
- package/native/manifest.json +9 -0
- package/native/targets.mjs +24 -0
- package/package.json +10 -3
- package/src/action-help.js +130 -0
- package/src/agent-key.js +22 -12
- package/src/anchored-fs.js +163 -0
- package/src/args.js +335 -31
- package/src/auth-binding.js +12 -7
- package/src/auth-callback.js +10 -2
- package/src/auth-login.js +111 -28
- package/src/auth-oauth.js +133 -36
- package/src/auth-session.js +19 -12
- package/src/auth-store.js +28 -7
- package/src/cli.js +147 -15
- package/src/constants.js +70 -1
- package/src/experiments-contract.js +462 -0
- package/src/experiments-output.js +190 -0
- package/src/experiments.js +406 -0
- package/src/output.js +374 -68
- package/src/pricing-args.js +163 -0
- package/src/pricing-output.js +206 -0
- package/src/pricing-reference.js +195 -0
- package/src/pricing.js +307 -0
- package/src/read-contract.js +64 -10
- package/src/read-output.js +66 -13
- package/src/read.js +105 -16
- package/src/repository-identity.js +2 -2
- package/src/setup-deployment.js +7 -0
- package/src/setup-env-git.js +1 -1
- package/src/setup-env.js +16 -9
- package/src/setup-interactive.js +356 -0
- package/src/setup-refusal.js +2 -2
- package/src/setup-state.js +9 -4
- package/src/setup.js +15 -3
- package/src/skill-bundle.js +2 -2
- package/src/skill-trusted.js +56 -0
- package/src/skill.js +28 -9
- package/src/style.js +33 -7
- package/src/tools-output.js +73 -0
- package/src/tools.js +249 -0
- package/src/trace-contract.js +1 -1
- package/src/transport.js +26 -3
- package/src/verify-output.js +2 -2
- package/src/verify.js +5 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// A process-local directory capability service. The shipped helper owns all
|
|
9
|
+
// descriptors; only its inherited anonymous pipes carry requests and contents.
|
|
10
|
+
// An absent or unverified backend is an error, never a pathname-write fallback.
|
|
11
|
+
const MAX = 2 * 1024 * 1024;
|
|
12
|
+
const base = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../native");
|
|
13
|
+
const errnoNames = new Map(Object.entries(os.constants.errno).map(([name, number]) => [number, name]));
|
|
14
|
+
let service = null;
|
|
15
|
+
let rollbackDepth = 0;
|
|
16
|
+
const aliases = new Map(process.platform === "darwin" ? [["/var", "/private/var"], ["/tmp", "/private/tmp"]] : []);
|
|
17
|
+
function failure(code) { const error = new Error(`Secure filesystem operation failed (${code}).`); error.code = code; return error; }
|
|
18
|
+
function verifiedHelper() {
|
|
19
|
+
const name = `anchored-fs-${process.platform}-${process.arch}`;
|
|
20
|
+
let file;
|
|
21
|
+
try {
|
|
22
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(base, "manifest.json"), "utf8"));
|
|
23
|
+
file = path.join(base, "bin", name);
|
|
24
|
+
const bytes = fs.readFileSync(file);
|
|
25
|
+
if (manifest.schema !== 1 || typeof manifest.artifacts[name] !== "string" ||
|
|
26
|
+
createHash("sha256").update(bytes).digest("hex") !== manifest.artifacts[name]) throw failure("ENOTSUP");
|
|
27
|
+
} catch { throw failure("ENOTSUP"); }
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
// Test and diagnostic capability discovery verifies the same packaged bytes as
|
|
31
|
+
// start(). It never enables a platform merely because its name is recognised.
|
|
32
|
+
export function hasVerifiedHelper() {
|
|
33
|
+
try { verifiedHelper(); return true; } catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
function start() {
|
|
36
|
+
if (service !== null) return service;
|
|
37
|
+
const file = verifiedHelper();
|
|
38
|
+
const child = spawn(file, [], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true, env: {} });
|
|
39
|
+
child.on("error", () => { /* synchronous pipe operations report backend loss */ });
|
|
40
|
+
const input = child.stdin?._handle;
|
|
41
|
+
const output = child.stdout?._handle;
|
|
42
|
+
if (!input || !output || typeof input.setBlocking !== "function" || typeof output.setBlocking !== "function") {
|
|
43
|
+
child.kill("SIGKILL"); throw failure("ENOTSUP");
|
|
44
|
+
}
|
|
45
|
+
input.setBlocking(true); output.setBlocking(true);
|
|
46
|
+
service = { child, input: input.fd, output: output.fd };
|
|
47
|
+
child.unref(); child.stdin.unref(); child.stdout.unref();
|
|
48
|
+
return service;
|
|
49
|
+
}
|
|
50
|
+
function writeAll(fd, bytes) { let offset = 0; while (offset < bytes.length) { const count = fs.writeSync(fd, bytes, offset, bytes.length - offset); if (count === 0) throw failure("EIO"); offset += count; } }
|
|
51
|
+
function readAll(fd, bytes) { let offset = 0; while (offset < bytes.length) { const count = fs.readSync(fd, bytes, offset, bytes.length - offset, null); if (count === 0) throw failure("EIO"); offset += count; } }
|
|
52
|
+
function request(operation, args = [], payload = Buffer.alloc(0)) {
|
|
53
|
+
const encoded = args.map(value => Buffer.from(String(value)));
|
|
54
|
+
if (encoded.some(value => value.includes(0))) throw failure("EINVAL");
|
|
55
|
+
const size = 2 + encoded.reduce((n, value) => n + 4 + value.length, 0) + payload.length;
|
|
56
|
+
if (size > MAX) throw failure("EFBIG");
|
|
57
|
+
const body = Buffer.alloc(4 + size); body.writeUInt32BE(size); body[4] = operation; body[5] = args.length;
|
|
58
|
+
let offset = 6;
|
|
59
|
+
for (const value of encoded) { body.writeUInt32BE(value.length, offset); offset += 4; value.copy(body, offset); offset += value.length; }
|
|
60
|
+
payload.copy(body, offset);
|
|
61
|
+
const pipe = start(); writeAll(pipe.input, body);
|
|
62
|
+
const header = Buffer.alloc(8); readAll(pipe.output, header);
|
|
63
|
+
const error = header.readUInt32BE(); const length = header.readUInt32BE(4);
|
|
64
|
+
if (length > MAX) throw failure("EIO");
|
|
65
|
+
const result = Buffer.alloc(length); readAll(pipe.output, result);
|
|
66
|
+
if (error) throw failure(errnoNames.get(error) ?? "EIO");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function canonical(value) {
|
|
70
|
+
if (typeof value !== "string" || value.includes("\0")) throw failure("EINVAL");
|
|
71
|
+
let absolute = path.resolve(value);
|
|
72
|
+
const choices = [...aliases].sort((a,b) => b[0].length - a[0].length);
|
|
73
|
+
for (const [alias, target] of choices) if (absolute === alias || absolute.startsWith(`${alias}/`)) { absolute = target + absolute.slice(alias.length); break; }
|
|
74
|
+
return absolute;
|
|
75
|
+
}
|
|
76
|
+
function descriptor(value) { if (!Number.isSafeInteger(value) || value > -1001) throw failure("EBADF"); return -value - 1001; }
|
|
77
|
+
function statResult(bytes, options) {
|
|
78
|
+
if (bytes.length !== 88) throw failure("EIO");
|
|
79
|
+
const keys = ["dev", "ino", "mode", "nlink", "uid", "gid", "size"];
|
|
80
|
+
const result = {};
|
|
81
|
+
keys.forEach((key, index) => { const n = bytes.readBigUInt64BE(index*8); result[key] = options?.bigint ? n : Number(n); });
|
|
82
|
+
for (const [name, index] of [["mtime", 7], ["ctime", 9]]) {
|
|
83
|
+
const seconds = bytes.readBigInt64BE(index * 8);
|
|
84
|
+
const nanoseconds = bytes.readBigUInt64BE((index + 1) * 8);
|
|
85
|
+
if (nanoseconds >= 1000000000n) throw failure("EIO");
|
|
86
|
+
const ns = seconds * 1000000000n + nanoseconds;
|
|
87
|
+
result[`${name}Ms`] = options?.bigint ? ns / 1000000n : Number(seconds) * 1000 + Number(nanoseconds) / 1000000;
|
|
88
|
+
if (options?.bigint) result[`${name}Ns`] = ns;
|
|
89
|
+
result[name] = new Date(Number(ns / 1000000n));
|
|
90
|
+
}
|
|
91
|
+
const mode = Number(result.mode) & 0o170000;
|
|
92
|
+
return Object.assign(result, { isFile: () => mode === 0o100000, isDirectory: () => mode === 0o040000,
|
|
93
|
+
isSymbolicLink: () => mode === 0o120000, isFIFO: () => mode === 0o010000,
|
|
94
|
+
isSocket: () => mode === 0o140000, isBlockDevice: () => mode === 0o060000, isCharacterDevice: () => mode === 0o020000 });
|
|
95
|
+
}
|
|
96
|
+
export function validatePath(value) { request(13, [canonical(value)]); return true; }
|
|
97
|
+
export function withRollback(callback) {
|
|
98
|
+
if (rollbackDepth++ === 0) request(14, [1]);
|
|
99
|
+
try { return callback(); } finally { if (--rollbackDepth === 0) request(14, [0]); }
|
|
100
|
+
}
|
|
101
|
+
export function closeAnchoredFs() {
|
|
102
|
+
if (service !== null) { service.child.stdin.destroy(); service.child.stdout.destroy(); service.child.kill("SIGKILL"); service = null; }
|
|
103
|
+
}
|
|
104
|
+
const anchored = {
|
|
105
|
+
constants: fs.constants,
|
|
106
|
+
lstatSync(value, options) { return statResult(request(1, [canonical(value)]), options); },
|
|
107
|
+
statSync(value, options) { const result = this.lstatSync(value, options); if (result.isSymbolicLink()) throw failure("ELOOP"); return result; },
|
|
108
|
+
realpathSync(value) {
|
|
109
|
+
// Root selection is read-only. Pin the resolved identity immediately; every
|
|
110
|
+
// subsequent operation uses the helper's retained directory descriptors.
|
|
111
|
+
const absolute = path.resolve(value); const real = fs.realpathSync(absolute);
|
|
112
|
+
const prior = aliases.get(absolute);
|
|
113
|
+
if (prior !== undefined && prior !== real) throw failure("ESTALE");
|
|
114
|
+
// Freeze both the selected spelling and the capability spelling. A later
|
|
115
|
+
// root resolution cannot rebind an existing capability to another tree.
|
|
116
|
+
const pinned = aliases.get(real);
|
|
117
|
+
if (pinned !== undefined && pinned !== real) throw failure("ESTALE");
|
|
118
|
+
validatePath(real); aliases.set(real, real); aliases.set(absolute, real); return real;
|
|
119
|
+
},
|
|
120
|
+
existsSync(value) { try { this.lstatSync(value); return true; } catch (error) { if (["ENOENT", "ENOTDIR"].includes(error.code)) return false; throw error; } },
|
|
121
|
+
openSync(value, flags, mode = 0o666) {
|
|
122
|
+
let kind;
|
|
123
|
+
const extras = (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
|
|
124
|
+
const stripped = typeof flags === "number" ? flags & ~extras : flags;
|
|
125
|
+
if (flags === "r" || stripped === fs.constants.O_RDONLY) kind = 0;
|
|
126
|
+
else if (flags === "r+" || stripped === fs.constants.O_RDWR) kind = 1;
|
|
127
|
+
else if (flags === "wx" || flags === "wx+" || stripped === (fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL)) kind = 2;
|
|
128
|
+
else throw failure("ENOTSUP");
|
|
129
|
+
return -1001 - request(2, [canonical(value), kind, mode]).readUInt32BE();
|
|
130
|
+
},
|
|
131
|
+
fstatSync(fd, options) { return statResult(request(3, [descriptor(fd)]), options); },
|
|
132
|
+
readFileSync(value, options) {
|
|
133
|
+
const own = typeof value !== "number"; const fd = own ? this.openSync(value, "r") : value;
|
|
134
|
+
try { const bytes = request(4, [descriptor(fd)]); const encoding = typeof options === "string" ? options : options?.encoding; return encoding ? bytes.toString(encoding) : bytes; }
|
|
135
|
+
finally { if (own) this.closeSync(fd); }
|
|
136
|
+
},
|
|
137
|
+
readSync(fd, buffer, offset, length, position) {
|
|
138
|
+
const bytes = request(16, [descriptor(fd), length, position ?? 0]); bytes.copy(buffer, offset); return bytes.length;
|
|
139
|
+
},
|
|
140
|
+
writeFileSync(fd, value, options) {
|
|
141
|
+
if (typeof fd !== "number") throw failure("ENOTSUP");
|
|
142
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
143
|
+
request(5, [descriptor(fd)], Buffer.isBuffer(value) ? value : Buffer.from(value, encoding));
|
|
144
|
+
},
|
|
145
|
+
fchmodSync(fd, mode) { request(6, [descriptor(fd), mode]); },
|
|
146
|
+
chmodSync(value, mode) { const fd = this.openSync(value, "r"); try { this.fchmodSync(fd, mode); } finally { this.closeSync(fd); } },
|
|
147
|
+
fsyncSync(fd) { request(7, [descriptor(fd)]); },
|
|
148
|
+
closeSync(fd) { request(8, [descriptor(fd)]); },
|
|
149
|
+
mkdirSync(value, options = {}) {
|
|
150
|
+
const absolute = canonical(value); const mode = typeof options === "number" ? options : options.mode ?? 0o777;
|
|
151
|
+
try { request(9, [absolute, mode]); }
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!options.recursive) throw error;
|
|
154
|
+
if (error.code === "ENOENT") { this.mkdirSync(path.dirname(absolute), {recursive:true, mode}); request(9, [absolute, mode]); }
|
|
155
|
+
else if (error.code !== "EEXIST" || !this.statSync(absolute).isDirectory()) throw error;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
renameSync(from, to) { request(10, [canonical(from), canonical(to)]); },
|
|
159
|
+
unlinkSync(value) { request(11, [canonical(value)]); },
|
|
160
|
+
rmdirSync(value) { request(12, [canonical(value)]); },
|
|
161
|
+
linkSync(from, to) { request(15, [canonical(from), canonical(to)]); },
|
|
162
|
+
};
|
|
163
|
+
export default anchored;
|
package/src/args.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { parsePricing, PRICING_ACTIONS, PRICING_OPTIONS } from "./pricing-args.js";
|
|
1
2
|
import { parseRepository } from "./repository-identity.js";
|
|
2
3
|
import {
|
|
3
4
|
DEFAULT_ORIGIN,
|
|
4
5
|
DEFAULT_TIMEOUT_MS,
|
|
6
|
+
EXPERIMENT_LAST_N,
|
|
7
|
+
EXPERIMENT_LIST_DEFAULT_LIMIT,
|
|
8
|
+
EXPERIMENT_LIST_MAX_LIMIT,
|
|
9
|
+
EXPERIMENT_MAX_TOKENS,
|
|
10
|
+
EXPERIMENT_MAX_TRACKED_FIELDS,
|
|
11
|
+
EXPERIMENT_MAX_TREATMENTS,
|
|
12
|
+
EXPERIMENT_REASONING_EFFORTS,
|
|
13
|
+
EXPERIMENT_TEXT_LIMITS,
|
|
5
14
|
HANDOFF_SKILL_CLIENTS,
|
|
6
15
|
HANDOFF_SKILL_RUNTIMES,
|
|
7
16
|
HANDOFF_LOGIN_RUNTIMES,
|
|
@@ -30,13 +39,25 @@ import {
|
|
|
30
39
|
USAGE_DIMENSIONS,
|
|
31
40
|
USAGE_MAX_FILTER_VALUES,
|
|
32
41
|
USAGE_SORTS,
|
|
42
|
+
WORKFLOW_PERMISSIONS,
|
|
33
43
|
} from "./constants.js";
|
|
34
44
|
import { parseOrigin } from "./origin.js";
|
|
35
45
|
import { groupedRequest, hasUnsafeText, isCursor, isSafeFilter, readQuery } from "./read-contract.js";
|
|
46
|
+
import { TOOL_ARGS_MAX_BYTES, TOOL_NAME } from "./tools.js";
|
|
36
47
|
|
|
37
48
|
export const READ_COMMANDS = Object.freeze(["status", "context", "capabilities", "usage", "routes", "traces"]);
|
|
38
|
-
const COMMANDS = new Set(["doctor", "help", "skill", "skills", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
|
|
39
|
-
const HELP_TOPICS = new Set(["doctor", "skill", "skills", "login", "logout", "setup", "verify", ...READ_COMMANDS]);
|
|
49
|
+
const COMMANDS = new Set(["pricing", "doctor", "help", "skill", "skills", "login", "logout", "setup", "verify", "experiments", "tools", ...READ_COMMANDS]);
|
|
50
|
+
const HELP_TOPICS = new Set(["agents", "exit-codes", "pricing", "doctor", "skill", "skills", "login", "logout", "setup", "verify", "experiments", "tools", ...READ_COMMANDS]);
|
|
51
|
+
export const TOOLS_ACTIONS = Object.freeze(["list", "call"]);
|
|
52
|
+
// The roles login asks for: Metadata, or Debug, to which --allow-* adds the
|
|
53
|
+
// workflow permissions a workspace owner approves. Replay is never asked for.
|
|
54
|
+
export const LOGIN_ROLES = Object.freeze(["metadata", "debug"]);
|
|
55
|
+
// Each --allow-* switch and the scope it asks for, by the names bin/agent-key
|
|
56
|
+
// and the Keys page use.
|
|
57
|
+
const LOGIN_PERMISSIONS = Object.freeze(
|
|
58
|
+
WORKFLOW_PERMISSIONS.map(({ field, scope }) => [`--${field.replaceAll("_", "-")}`, scope]),
|
|
59
|
+
);
|
|
60
|
+
export const EXPERIMENT_ACTIONS = Object.freeze(["list", "estimate", "start", "get"]);
|
|
40
61
|
const SKILL_ACTIONS = new Set(["install", "update"]);
|
|
41
62
|
const SKILLS_ACTIONS = new Set(["install", "update", "list"]);
|
|
42
63
|
const READ_BASE = ["--project", "--config-dir", "--timeout-ms"];
|
|
@@ -45,10 +66,11 @@ const READ_BASE = ["--project", "--config-dir", "--timeout-ms"];
|
|
|
45
66
|
// never an argument.
|
|
46
67
|
const READ_KEY = ["--url", "--workspace", "--agent-token-file"];
|
|
47
68
|
const OPTIONS = {
|
|
69
|
+
pricing: new Set(PRICING_OPTIONS),
|
|
48
70
|
doctor: new Set(["--url", "--timeout-ms"]),
|
|
49
71
|
skill: new Set(["--client", "--runtime", "--project"]),
|
|
50
72
|
skills: new Set(["--client", "--runtime", "--project"]),
|
|
51
|
-
login: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--timeout-ms"]),
|
|
73
|
+
login: new Set(["--runtime", "--url", "--workspace", "--role", "--project", "--config-dir", "--timeout-ms"]),
|
|
52
74
|
setup: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--env-file", "--client", "--timeout-ms", "--deployment", "--agent-token-file", "--repository"]),
|
|
53
75
|
logout: new Set(["--project", "--config-dir"]),
|
|
54
76
|
status: new Set([...READ_BASE, ...READ_KEY]),
|
|
@@ -63,6 +85,17 @@ const OPTIONS = {
|
|
|
63
85
|
...READ_BASE, ...READ_KEY, "--days", "--limit", "--route", "--status", "--cursor", "--metadata", "--metadata-json",
|
|
64
86
|
]),
|
|
65
87
|
verify: new Set([...READ_BASE, "--trace-id", "--request-id", "--since", "--until", "--source", "--days", "--poll-ms", "--max-attempts"]),
|
|
88
|
+
experiments: new Set([
|
|
89
|
+
...READ_BASE, ...READ_KEY, "--workload", "--limit", "--treatment", "--last-n", "--sample-model", "--track", "--id",
|
|
90
|
+
]),
|
|
91
|
+
tools: new Set([...READ_BASE, ...READ_KEY, "--name", "--args", "--args-file"]),
|
|
92
|
+
};
|
|
93
|
+
// Which experiments options each action takes, beyond READ_BASE and READ_KEY.
|
|
94
|
+
const EXPERIMENT_OPTIONS = {
|
|
95
|
+
list: new Set(["--workload", "--limit"]),
|
|
96
|
+
estimate: new Set(["--workload", "--treatment", "--last-n", "--sample-model", "--track"]),
|
|
97
|
+
start: new Set(["--workload", "--treatment", "--last-n", "--sample-model", "--track"]),
|
|
98
|
+
get: new Set(["--id"]),
|
|
66
99
|
};
|
|
67
100
|
// Requests the read commands recognize and refuse, so a script gets an
|
|
68
101
|
// explicit unsupported result instead of an unknown argument or, worse, a
|
|
@@ -96,6 +129,7 @@ const REFUSED_MESSAGES = {
|
|
|
96
129
|
};
|
|
97
130
|
// Options that may be given more than once; their values are kept in order.
|
|
98
131
|
const REPEATABLE = new Set(["--metadata"]);
|
|
132
|
+
const EXPERIMENT_REPEATABLE = new Set(["--treatment", "--track"]);
|
|
99
133
|
// usage takes several values for each of its exact-match filters.
|
|
100
134
|
const USAGE_REPEATABLE = new Set(["--route", "--provider", "--model", "--environment"]);
|
|
101
135
|
// usage reads a time range and environments itself, through grouped usage.
|
|
@@ -106,10 +140,12 @@ const USAGE_GROUPED = [
|
|
|
106
140
|
];
|
|
107
141
|
// Options that take no value.
|
|
108
142
|
const FLAGS = {
|
|
109
|
-
|
|
143
|
+
pricing: new Set(["--batch"]),
|
|
144
|
+
login: new Set(["--signup", "--no-browser", "--reconnect", ...LOGIN_PERMISSIONS.map(([flag]) => flag)]),
|
|
110
145
|
setup: new Set(["--no-browser", "--repair", "--signup", "--reconnect", "--skip-skill", "--confirm-prerequisites", "--no-repository"]),
|
|
111
146
|
verify: new Set(["--open", "--no-browser"]),
|
|
112
147
|
usage: new Set(["--today"]),
|
|
148
|
+
tools: new Set(["--yes"]),
|
|
113
149
|
};
|
|
114
150
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
115
151
|
|
|
@@ -133,13 +169,24 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
133
169
|
export function parseArgs(argv) {
|
|
134
170
|
const json = argv.includes("--json");
|
|
135
171
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
const helpCommand = argv.find(arg => arg !== "--json") === "help";
|
|
173
|
+
if (argv.includes("--help") || argv.includes("-h") || helpCommand) {
|
|
174
|
+
const parts = argv.filter(arg => !["--json", "--help", "-h", "--advanced"].includes(arg));
|
|
175
|
+
if (parts[0] === "help") parts.shift();
|
|
176
|
+
const topic = HELP_TOPICS.has(parts[0]) ? parts[0] : null;
|
|
177
|
+
const actions = { pricing: PRICING_ACTIONS, experiments: EXPERIMENT_ACTIONS,
|
|
178
|
+
skill: [...SKILL_ACTIONS], skills: [...SKILLS_ACTIONS] };
|
|
179
|
+
const action = actions[topic]?.includes(parts[1]) ? parts[1] : null;
|
|
180
|
+
if (helpCommand && (parts.length > (action ? 2 : topic ? 1 : 0))) {
|
|
181
|
+
return { ok: false, command: "help", json, code: "unknown_help_topic", outcome: "invalid_input",
|
|
182
|
+
message: 'Unknown help topic. Run "metergraph --help" for commands.' };
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, command: "help", topic, ...(action ? { action } : {}), ...(argv.includes("--advanced") ? { advanced: true } : {}), json };
|
|
139
185
|
}
|
|
140
186
|
|
|
141
187
|
let command = null;
|
|
142
188
|
let action = null;
|
|
189
|
+
let toolName = null;
|
|
143
190
|
let topic = null;
|
|
144
191
|
let version = false;
|
|
145
192
|
const values = {};
|
|
@@ -147,7 +194,7 @@ export function parseArgs(argv) {
|
|
|
147
194
|
|
|
148
195
|
const fail = (code, message, outcome = "invalid_input") => ({
|
|
149
196
|
ok: false,
|
|
150
|
-
command: (command === "skill" || command === "skills") && action !== null
|
|
197
|
+
command: (command === "skill" || command === "skills" || command === "experiments" || command === "tools") && action !== null
|
|
151
198
|
? `${command} ${action}`
|
|
152
199
|
: command ?? (version ? "version" : null),
|
|
153
200
|
json,
|
|
@@ -174,6 +221,10 @@ export function parseArgs(argv) {
|
|
|
174
221
|
topic = arg;
|
|
175
222
|
continue;
|
|
176
223
|
}
|
|
224
|
+
if (command === "pricing" && action === null && PRICING_ACTIONS.includes(arg)) {
|
|
225
|
+
action = arg;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
177
228
|
if (command === "skill" && action === null && SKILL_ACTIONS.has(arg)) {
|
|
178
229
|
action = arg;
|
|
179
230
|
continue;
|
|
@@ -182,6 +233,18 @@ export function parseArgs(argv) {
|
|
|
182
233
|
action = arg;
|
|
183
234
|
continue;
|
|
184
235
|
}
|
|
236
|
+
if (command === "experiments" && action === null && EXPERIMENT_ACTIONS.includes(arg)) {
|
|
237
|
+
action = arg;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (command === "tools" && action === null && TOOLS_ACTIONS.includes(arg)) {
|
|
241
|
+
action = arg;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (command === "tools" && action === "call" && toolName === null && !arg.startsWith("-")) {
|
|
245
|
+
toolName = arg;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
185
248
|
|
|
186
249
|
if (READ_COMMANDS.includes(command)) {
|
|
187
250
|
const equals = arg.indexOf("=");
|
|
@@ -212,7 +275,8 @@ export function parseArgs(argv) {
|
|
|
212
275
|
if (value === undefined) {
|
|
213
276
|
return fail("missing_value", `${name} requires a value.`);
|
|
214
277
|
}
|
|
215
|
-
if (REPEATABLE.has(name) || (command === "usage" && USAGE_REPEATABLE.has(name))
|
|
278
|
+
if (REPEATABLE.has(name) || (command === "usage" && USAGE_REPEATABLE.has(name)) ||
|
|
279
|
+
(command === "experiments" && EXPERIMENT_REPEATABLE.has(name))) {
|
|
216
280
|
values[name] = [...(values[name] ?? []), value];
|
|
217
281
|
continue;
|
|
218
282
|
}
|
|
@@ -242,6 +306,15 @@ export function parseArgs(argv) {
|
|
|
242
306
|
`Unknown skills subcommand at argument ${position}. Use install, update or list.`,
|
|
243
307
|
);
|
|
244
308
|
}
|
|
309
|
+
if (command === "experiments" && action === null && !arg.startsWith("-")) {
|
|
310
|
+
return fail(
|
|
311
|
+
"unknown_subcommand",
|
|
312
|
+
`Unknown experiments subcommand at argument ${position}. Use list, estimate, start or get.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (command === "tools" && action === null && !arg.startsWith("-")) {
|
|
316
|
+
return fail("unknown_subcommand", `Unknown tools subcommand at argument ${position}. Use list or call.`);
|
|
317
|
+
}
|
|
245
318
|
return fail(
|
|
246
319
|
"unknown_argument",
|
|
247
320
|
`Unrecognized argument at position ${position}. Run "metergraph --help" for usage.`,
|
|
@@ -252,11 +325,14 @@ export function parseArgs(argv) {
|
|
|
252
325
|
if (command === null || command === "help") {
|
|
253
326
|
return { ok: true, command: "help", topic, json };
|
|
254
327
|
}
|
|
328
|
+
if (command === "pricing") return parsePricing(action, values, flags, json, fail);
|
|
255
329
|
if (command === "skill") return parseSkill(action, values, json, fail);
|
|
256
330
|
if (command === "skills") return parseSkills(action, values, json, fail);
|
|
257
331
|
if (command === "login") return parseLogin(values, flags, json, fail);
|
|
258
332
|
if (command === "setup") return parseSetup(values, flags, json, fail);
|
|
259
333
|
if (command === "verify") return parseVerify(values, flags, json, fail);
|
|
334
|
+
if (command === "experiments") return parseExperiments(action, values, json, fail);
|
|
335
|
+
if (command === "tools") return parseTools(action, toolName, values, flags, json, fail);
|
|
260
336
|
if (READ_COMMANDS.includes(command)) return parseRead(command, values, flags, json, fail);
|
|
261
337
|
if (command === "logout") {
|
|
262
338
|
const paths = parsePaths(values, fail);
|
|
@@ -280,6 +356,240 @@ export function parseArgs(argv) {
|
|
|
280
356
|
return { ok: true, command: "doctor", origin, timeoutMs, json };
|
|
281
357
|
}
|
|
282
358
|
|
|
359
|
+
// --url, --workspace and --agent-token-file, shared by the read commands and
|
|
360
|
+
// experiments: { ok: true, origin, workspace, agentTokenFile } or a failure.
|
|
361
|
+
// --workspace names a workspace by its slug (on its Settings page, and in
|
|
362
|
+
// "metergraph context"), by its name, or by its ID. Returns { workspace,
|
|
363
|
+
// workspaceText }: workspace is an ID, workspaceText a slug or name, which is
|
|
364
|
+
// matched against the workspace the service reports. null for a bad value.
|
|
365
|
+
function workspaceOption(raw) {
|
|
366
|
+
if (raw === undefined) return { workspace: null, workspaceText: null };
|
|
367
|
+
if (UUID.test(raw)) return { workspace: raw.toLowerCase(), workspaceText: null };
|
|
368
|
+
const text = raw.trim();
|
|
369
|
+
if (text.length === 0 || text.length > MAX_WORKSPACE_TEXT || hasUnsafeText(text)) return null;
|
|
370
|
+
return { workspace: null, workspaceText: text };
|
|
371
|
+
}
|
|
372
|
+
const MAX_WORKSPACE_TEXT = 200;
|
|
373
|
+
const INVALID_WORKSPACE =
|
|
374
|
+
"--workspace takes the workspace's slug, shown on its Settings page and by \"metergraph context\", or its name.";
|
|
375
|
+
|
|
376
|
+
function parseKeyTarget(values, fail) {
|
|
377
|
+
let origin = null;
|
|
378
|
+
if (values["--url"] !== undefined) {
|
|
379
|
+
origin = parseOrigin(values["--url"]);
|
|
380
|
+
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
381
|
+
}
|
|
382
|
+
const named = workspaceOption(values["--workspace"]);
|
|
383
|
+
if (named === null) return fail("invalid_workspace", INVALID_WORKSPACE);
|
|
384
|
+
const agentTokenFile = values["--agent-token-file"] ?? null;
|
|
385
|
+
if (agentTokenFile !== null && (agentTokenFile === "" || agentTokenFile.includes("\0"))) {
|
|
386
|
+
return fail("invalid_agent_token_file", "--agent-token-file must name a private absolute file.");
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
ok: true,
|
|
390
|
+
origin,
|
|
391
|
+
...named,
|
|
392
|
+
agentTokenFile,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// tools list [--name TOOL] | call TOOL [--args JSON | --args-file PATH] [--yes].
|
|
397
|
+
// The credential options are the read commands'. Arguments are checked for
|
|
398
|
+
// shape only; the service validates them against the tool's schema.
|
|
399
|
+
function parseTools(action, toolName, values, flags, json, fail) {
|
|
400
|
+
if (action === null) {
|
|
401
|
+
return fail("missing_subcommand", 'tools requires a subcommand: list or call. Run "metergraph help tools" for usage.');
|
|
402
|
+
}
|
|
403
|
+
const allowed = action === "list" ? ["--name"] : ["--args", "--args-file"];
|
|
404
|
+
for (const name of Object.keys(values)) {
|
|
405
|
+
if (!READ_BASE.includes(name) && !READ_KEY.includes(name) && !allowed.includes(name)) {
|
|
406
|
+
return fail("unknown_argument", `tools ${action} does not take ${name}. Run "metergraph help tools" for usage.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (action === "list" && flags.has("--yes")) {
|
|
410
|
+
return fail("unknown_argument", 'tools list does not take --yes. Run "metergraph help tools" for usage.');
|
|
411
|
+
}
|
|
412
|
+
const name = action === "list" ? values["--name"] ?? null : toolName;
|
|
413
|
+
if (action === "call" && name === null) {
|
|
414
|
+
return fail("missing_tool", 'tools call needs a tool name, such as metergraph_list_classified_workloads. Run "metergraph tools list" to see them.');
|
|
415
|
+
}
|
|
416
|
+
if (name !== null && !TOOL_NAME.test(name)) {
|
|
417
|
+
return fail("invalid_tool", "A tool name is metergraph_ followed by lower case letters, digits and underscores.");
|
|
418
|
+
}
|
|
419
|
+
if (values["--args"] !== undefined && values["--args-file"] !== undefined) {
|
|
420
|
+
return fail("conflicting_options", "Give --args or --args-file, not both.");
|
|
421
|
+
}
|
|
422
|
+
if (values["--args"] !== undefined && Buffer.byteLength(values["--args"]) > TOOL_ARGS_MAX_BYTES) {
|
|
423
|
+
return fail("args_too_large", "--args is larger than 1 MiB. Use --args-file.");
|
|
424
|
+
}
|
|
425
|
+
if (values["--args-file"] !== undefined && (values["--args-file"] === "" || values["--args-file"].includes("\0"))) {
|
|
426
|
+
return fail("invalid_args_file", "--args-file must name a file.");
|
|
427
|
+
}
|
|
428
|
+
const paths = parsePaths(values, fail);
|
|
429
|
+
if (!paths.ok) return paths;
|
|
430
|
+
const rawTimeout = values["--timeout-ms"];
|
|
431
|
+
const timeoutMs = rawTimeout === undefined
|
|
432
|
+
? READ_DEFAULT_TIMEOUT_MS
|
|
433
|
+
: parseTimeout(rawTimeout, READ_MIN_TIMEOUT_MS, READ_MAX_TIMEOUT_MS);
|
|
434
|
+
if (timeoutMs === null) {
|
|
435
|
+
return fail("invalid_timeout", `--timeout-ms must be a whole number from ${READ_MIN_TIMEOUT_MS} to ${READ_MAX_TIMEOUT_MS}.`);
|
|
436
|
+
}
|
|
437
|
+
const target = parseKeyTarget(values, fail);
|
|
438
|
+
if (!target.ok) return target;
|
|
439
|
+
delete target.ok;
|
|
440
|
+
return {
|
|
441
|
+
ok: true, command: "tools", action, ...target, project: paths.project, configDir: paths.configDir, timeoutMs, json,
|
|
442
|
+
tool: { name, args: values["--args"] ?? null, argsFile: values["--args-file"] ?? null, yes: flags.has("--yes") },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// experiments list | estimate | start | get. estimate and start take the same
|
|
447
|
+
// request, so an agent starts exactly what it priced: the workload, 1 to 3
|
|
448
|
+
// --treatment PROVIDER/MODEL[,label=..][,temperature=..][,max_tokens=..]
|
|
449
|
+
// [,reasoning_effort=..], and optionally --last-n, --sample-model and up to ten
|
|
450
|
+
// --track JSON paths. The service validates every request in full; these
|
|
451
|
+
// checks only refuse what it would refuse before anything is sent.
|
|
452
|
+
function parseExperiments(action, values, json, fail) {
|
|
453
|
+
if (action === null) {
|
|
454
|
+
return fail(
|
|
455
|
+
"missing_subcommand",
|
|
456
|
+
'experiments requires a subcommand: list, estimate, start or get. Run "metergraph help experiments" for usage.',
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
for (const name of Object.keys(values)) {
|
|
460
|
+
if (!READ_BASE.includes(name) && !READ_KEY.includes(name) && !EXPERIMENT_OPTIONS[action].has(name)) {
|
|
461
|
+
return fail("unknown_argument", `experiments ${action} does not take ${name}. Run "metergraph help experiments" for usage.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const paths = parsePaths(values, fail);
|
|
465
|
+
if (!paths.ok) return paths;
|
|
466
|
+
const rawTimeout = values["--timeout-ms"];
|
|
467
|
+
const timeoutMs = rawTimeout === undefined
|
|
468
|
+
? READ_DEFAULT_TIMEOUT_MS
|
|
469
|
+
: parseTimeout(rawTimeout, READ_MIN_TIMEOUT_MS, READ_MAX_TIMEOUT_MS);
|
|
470
|
+
if (timeoutMs === null) {
|
|
471
|
+
return fail("invalid_timeout", `--timeout-ms must be a whole number from ${READ_MIN_TIMEOUT_MS} to ${READ_MAX_TIMEOUT_MS}.`);
|
|
472
|
+
}
|
|
473
|
+
const target = parseKeyTarget(values, fail);
|
|
474
|
+
if (!target.ok) return target;
|
|
475
|
+
delete target.ok;
|
|
476
|
+
const parsed = {
|
|
477
|
+
ok: true, command: "experiments", action, ...target, project: paths.project, configDir: paths.configDir,
|
|
478
|
+
timeoutMs, json, experiment: { id: null, workloadId: null, limit: null, request: null },
|
|
479
|
+
};
|
|
480
|
+
const workload = values["--workload"];
|
|
481
|
+
if (workload !== undefined && !experimentText(workload, EXPERIMENT_TEXT_LIMITS.workload)) {
|
|
482
|
+
return fail("invalid_workload", `--workload must be 1 to ${EXPERIMENT_TEXT_LIMITS.workload} characters without control characters.`);
|
|
483
|
+
}
|
|
484
|
+
if (action === "get") {
|
|
485
|
+
const id = values["--id"];
|
|
486
|
+
if (id === undefined || !UUID.test(id)) return fail("invalid_experiment_id", "--id must be an experiment ID in UUID form.");
|
|
487
|
+
parsed.experiment.id = id.toLowerCase();
|
|
488
|
+
return parsed;
|
|
489
|
+
}
|
|
490
|
+
if (action === "list") {
|
|
491
|
+
const limit = values["--limit"] === undefined
|
|
492
|
+
? EXPERIMENT_LIST_DEFAULT_LIMIT
|
|
493
|
+
: parseBounded(values["--limit"], 1, EXPERIMENT_LIST_MAX_LIMIT);
|
|
494
|
+
if (limit === null) return fail("invalid_limit", `--limit must be a whole number from 1 to ${EXPERIMENT_LIST_MAX_LIMIT}.`);
|
|
495
|
+
// The exact query list sends, so an over-long one fails here and not after sign in.
|
|
496
|
+
const query = new URLSearchParams({ ...(workload === undefined ? {} : { workload_id: workload }), limit: String(limit) });
|
|
497
|
+
if (query.toString().length > MAX_QUERY_LENGTH) {
|
|
498
|
+
return fail("query_too_long", "--workload is too long to send in one request once encoded.");
|
|
499
|
+
}
|
|
500
|
+
parsed.experiment.workloadId = workload ?? null;
|
|
501
|
+
parsed.experiment.limit = limit;
|
|
502
|
+
return parsed;
|
|
503
|
+
}
|
|
504
|
+
if (workload === undefined) return fail("missing_workload", "--workload is required: the workload (route) whose calls to replay.");
|
|
505
|
+
const specs = values["--treatment"] ?? [];
|
|
506
|
+
if (specs.length < 1 || specs.length > EXPERIMENT_MAX_TREATMENTS) {
|
|
507
|
+
return fail("invalid_treatments", `Give 1 to ${EXPERIMENT_MAX_TREATMENTS} --treatment values.`);
|
|
508
|
+
}
|
|
509
|
+
const treatments = [];
|
|
510
|
+
for (const spec of specs) {
|
|
511
|
+
const treatment = parseTreatment(spec);
|
|
512
|
+
if (treatment === null) {
|
|
513
|
+
return fail(
|
|
514
|
+
"invalid_treatment",
|
|
515
|
+
"--treatment must be PROVIDER/MODEL, optionally followed by ,label=NAME ,temperature=0..2 " +
|
|
516
|
+
`,max_tokens=1..${EXPERIMENT_MAX_TOKENS} or ,reasoning_effort=${EXPERIMENT_REASONING_EFFORTS.join("|")}.`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
treatments.push(treatment);
|
|
520
|
+
}
|
|
521
|
+
const labels = treatments.map((treatment) => treatment.label).filter((label) => label !== undefined);
|
|
522
|
+
if (new Set(labels.map((label) => label.toLowerCase())).size !== labels.length ||
|
|
523
|
+
labels.some((label) => label.toLowerCase() === "control")) {
|
|
524
|
+
return fail("invalid_treatments", 'Treatment labels must be unique and cannot be "control".');
|
|
525
|
+
}
|
|
526
|
+
const sample = { last_n: EXPERIMENT_LAST_N.default };
|
|
527
|
+
if (values["--last-n"] !== undefined) {
|
|
528
|
+
sample.last_n = parseBounded(values["--last-n"], EXPERIMENT_LAST_N.min, EXPERIMENT_LAST_N.max);
|
|
529
|
+
if (sample.last_n === null) {
|
|
530
|
+
return fail("invalid_last_n", `--last-n must be a whole number from ${EXPERIMENT_LAST_N.min} to ${EXPERIMENT_LAST_N.max}.`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (values["--sample-model"] !== undefined) {
|
|
534
|
+
if (!experimentText(values["--sample-model"], EXPERIMENT_TEXT_LIMITS.model)) {
|
|
535
|
+
return fail("invalid_sample_model", `--sample-model must be 1 to ${EXPERIMENT_TEXT_LIMITS.model} characters without control characters.`);
|
|
536
|
+
}
|
|
537
|
+
sample.model = values["--sample-model"];
|
|
538
|
+
}
|
|
539
|
+
const tracked = values["--track"] ?? [];
|
|
540
|
+
if (tracked.length > EXPERIMENT_MAX_TRACKED_FIELDS ||
|
|
541
|
+
tracked.some((field) => !experimentText(field, EXPERIMENT_TEXT_LIMITS.path) || !field.startsWith("$"))) {
|
|
542
|
+
return fail("invalid_tracked_fields", `Give at most ${EXPERIMENT_MAX_TRACKED_FIELDS} --track JSON paths, each starting with $.`);
|
|
543
|
+
}
|
|
544
|
+
parsed.experiment.request = { workload_id: workload, sample, treatments, tracked_fields: [...new Set(tracked)] };
|
|
545
|
+
return parsed;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function experimentText(value, max) {
|
|
549
|
+
return typeof value === "string" && value.trim() === value && value.length > 0 && value.length <= max && !hasUnsafeText(value);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const TREATMENT_KEYS = new Set(["label", "temperature", "max_tokens", "reasoning_effort"]);
|
|
553
|
+
|
|
554
|
+
// PROVIDER/MODEL[,key=value...] as a treatment, or null. The model may itself
|
|
555
|
+
// contain "/" (a gateway ID such as anthropic/claude-haiku-4.5).
|
|
556
|
+
function parseTreatment(spec) {
|
|
557
|
+
const [head, ...options] = spec.split(",");
|
|
558
|
+
const slash = head.indexOf("/");
|
|
559
|
+
if (slash <= 0) return null;
|
|
560
|
+
const provider = head.slice(0, slash);
|
|
561
|
+
const model = head.slice(slash + 1);
|
|
562
|
+
if (!experimentText(provider, EXPERIMENT_TEXT_LIMITS.provider) || !experimentText(model, EXPERIMENT_TEXT_LIMITS.model)) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const treatment = { provider, model };
|
|
566
|
+
const settings = {};
|
|
567
|
+
for (const option of options) {
|
|
568
|
+
const equals = option.indexOf("=");
|
|
569
|
+
const key = option.slice(0, equals);
|
|
570
|
+
const value = option.slice(equals + 1);
|
|
571
|
+
if (equals <= 0 || !TREATMENT_KEYS.has(key) || Object.hasOwn(settings, key) || (key === "label" && treatment.label !== undefined)) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
if (key === "label") {
|
|
575
|
+
if (!experimentText(value, EXPERIMENT_TEXT_LIMITS.label)) return null;
|
|
576
|
+
treatment.label = value;
|
|
577
|
+
} else if (key === "temperature") {
|
|
578
|
+
if (!/^(?:[0-1](?:\.[0-9]{1,3})?|2(?:\.0{1,3})?)$/.test(value)) return null;
|
|
579
|
+
settings.temperature = Number(value);
|
|
580
|
+
} else if (key === "max_tokens") {
|
|
581
|
+
const tokens = /^[0-9]{1,5}$/.test(value) ? Number(value) : 0;
|
|
582
|
+
if (tokens < 1 || tokens > EXPERIMENT_MAX_TOKENS) return null;
|
|
583
|
+
settings.max_tokens = tokens;
|
|
584
|
+
} else {
|
|
585
|
+
if (!EXPERIMENT_REASONING_EFFORTS.includes(value)) return null;
|
|
586
|
+
settings.reasoning_effort = value;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (Object.keys(settings).length > 0) treatment.settings = settings;
|
|
590
|
+
return treatment;
|
|
591
|
+
}
|
|
592
|
+
|
|
283
593
|
function parseVerify(values, flags, json, fail) {
|
|
284
594
|
const paths = parsePaths(values, fail);
|
|
285
595
|
if (!paths.ok) return paths;
|
|
@@ -385,9 +695,16 @@ function parseLogin(values, flags, json, fail) {
|
|
|
385
695
|
}
|
|
386
696
|
const origin = parseUrlOption(values);
|
|
387
697
|
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
|
|
698
|
+
const named = workspaceOption(values["--workspace"]);
|
|
699
|
+
if (named === null) return fail("invalid_workspace", INVALID_WORKSPACE);
|
|
700
|
+
const role = values["--role"] ?? "metadata";
|
|
701
|
+
if (!LOGIN_ROLES.includes(role)) return fail("invalid_role", "--role must be metadata or debug.");
|
|
702
|
+
const permissions = LOGIN_PERMISSIONS.filter(([flag]) => flags.has(flag)).map(([, scope]) => scope);
|
|
703
|
+
if (permissions.length > 0 && role !== "debug") {
|
|
704
|
+
return fail(
|
|
705
|
+
"permission_needs_debug",
|
|
706
|
+
"--allow-evaluation-writes, --allow-evaluation-previews and --allow-analysis-starts need --role debug.",
|
|
707
|
+
);
|
|
391
708
|
}
|
|
392
709
|
const paths = parsePaths(values, fail);
|
|
393
710
|
if (!paths.ok) return paths;
|
|
@@ -407,7 +724,9 @@ function parseLogin(values, flags, json, fail) {
|
|
|
407
724
|
command: "login",
|
|
408
725
|
runtime,
|
|
409
726
|
origin,
|
|
410
|
-
|
|
727
|
+
...named,
|
|
728
|
+
role,
|
|
729
|
+
permissions,
|
|
411
730
|
project: paths.project,
|
|
412
731
|
configDir: paths.configDir,
|
|
413
732
|
timeoutMs,
|
|
@@ -515,24 +834,9 @@ function parseRead(command, values, flags, json, fail) {
|
|
|
515
834
|
}
|
|
516
835
|
// Where to read and with which key: the same for every read, grouped usage
|
|
517
836
|
// included.
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
522
|
-
}
|
|
523
|
-
const rawWorkspace = values["--workspace"];
|
|
524
|
-
if (rawWorkspace !== undefined && !UUID.test(rawWorkspace)) {
|
|
525
|
-
return fail("invalid_workspace", "--workspace must be a workspace ID in UUID form.");
|
|
526
|
-
}
|
|
527
|
-
const agentTokenFile = values["--agent-token-file"] ?? null;
|
|
528
|
-
if (agentTokenFile !== null && (agentTokenFile === "" || agentTokenFile.includes("\0"))) {
|
|
529
|
-
return fail("invalid_agent_token_file", "--agent-token-file must name a private absolute file.");
|
|
530
|
-
}
|
|
531
|
-
const target = {
|
|
532
|
-
origin,
|
|
533
|
-
workspace: rawWorkspace === undefined ? null : rawWorkspace.toLowerCase(),
|
|
534
|
-
agentTokenFile,
|
|
535
|
-
};
|
|
837
|
+
const target = parseKeyTarget(values, fail);
|
|
838
|
+
if (!target.ok) return target;
|
|
839
|
+
delete target.ok;
|
|
536
840
|
if (command === "usage" && (USAGE_GROUPED.some((name) => values[name] !== undefined) || flags.has("--today"))) {
|
|
537
841
|
return parseGroupedUsage(values, flags, json, fail, { paths, timeoutMs, limit, target });
|
|
538
842
|
}
|