@metrio-ai/cli 1.0.0 → 1.1.0

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 CHANGED
@@ -25,14 +25,26 @@ metrio prompt create-version -p 7 -f system.md
25
25
  ```bash
26
26
  metrio login # browser sign-in; token stored in ~/.config/metrio/credentials.json
27
27
  metrio whoami
28
- metrio tools # every console tool your login can call
29
- metrio call list_prompts --args '{"project_slug":"my-project"}'
30
- metrio call create_workflow_version -f args.json
31
- metrio logout
28
+ export METRIO_PROJECT=my-project # or --project, or "project: my-project" in metrio.yaml
29
+
30
+ metrio prompt list
31
+ metrio version get 34 -o system.md # the system prompt as a file
32
+ metrio version create 12 -f system.md --model-name gpt-5
33
+ metrio version get 34 -o v.yaml && metrio version update 34 -f v.yaml # round-trip
34
+ metrio skill get 5 -o skill.md # frontmatter (name, description, references) + body
35
+ metrio skill update 5 -f skill.md
36
+ metrio workflow version create intake -f definition.json --activate
37
+ metrio version create-batch -f versions.jsonl
38
+ metrio prompt delete 7 --yes
39
+ metrio run workflow attachment <task-id> <key> -o original.jpg
32
40
  ```
33
41
 
42
+ Every command lists its options with `--help` (`metrio version --help`, `metrio version create --help`). Any input field can be passed as `--<field-name>` (kebab-case); text fields accept `@file` (start with `@@` for a literal `@`), object/array fields accept JSON or `@file`. `-f` takes `.json`, `.yaml`, `.jsonl`, `.md`/`.txt` (text field) or `-` (stdin). Input is validated locally before anything is sent. Output is a table or YAML by default, `--json` for scripts, `-o file` to save (`.json`, `.yaml`, `.md`).
43
+
44
+ `metrio tools` lists every console tool your login can call; `metrio call <tool> -f args.json` calls any of them directly.
45
+
34
46
  For CI set `METRIO_TOKEN` (and `METRIO_CONSOLE_URL` if not `https://app.metrio.ai`). It must be an OAuth access token, valid for about 1 hour (e.g. `access_token` from `~/.config/metrio/credentials.json`), so it suits short jobs. For long-running headless use, run `metrio login` once on that machine; the stored login refreshes itself.
35
47
 
36
- Exit codes: `0` ok, `1` failure, `2` usage or validation error, `3` not logged in / not allowed. Codes 2 and 3 apply to the account/management commands (`login`, `logout`, `whoami`, `tools`, `call`); the run commands keep their original behavior (errors exit 1).
48
+ Exit codes: `0` ok, `1` failure, `2` usage or validation error, `3` not logged in / not allowed. Codes 2 and 3 apply to the account and resource-management commands (`login`, `logout`, `whoami`, `tools`, `call`, and every `metrio <group> ...` command); the run commands (`chat`, `workflow run` / `run-batch`, `prompt run-batch` / `create-version`) keep their original behavior (errors exit 1). A workflow DAG the console rejects exits 2; a partially failed `version create-batch` exits 1.
37
49
 
38
50
  Migrating from `@metrio-ai/client`'s bundled CLI: if the old CLI was installed globally, run `npm rm -g @metrio-ai/client` before `npm i -g @metrio-ai/cli` (both install a `metrio` bin). Every command and flag is unchanged. Bare `metrio --messages …` still works but is deprecated in favor of `metrio chat --messages …`.
@@ -63,14 +63,32 @@ async function resolveConsoleAuth() {
63
63
  }
64
64
  class ConsoleClient {
65
65
  auth;
66
- constructor(auth) {
66
+ retryDelayMs;
67
+ constructor(auth, retryDelayMs = 300) {
67
68
  this.auth = auth;
69
+ this.retryDelayMs = retryDelayMs;
68
70
  }
69
71
  async listTools() {
70
72
  return (await this.request('GET', '/api/v1/tools'));
71
73
  }
72
- async callTool(name, args) {
73
- return this.request('POST', `/api/v1/tools/${encodeURIComponent(name)}`, args ?? {});
74
+ /**
75
+ * POST /api/v1/tools/{name}. With `retry` (read-scope tools only), network errors and 5xx are
76
+ * retried up to 3 attempts with exponential backoff; writes are never retried because the first
77
+ * attempt may have succeeded server-side.
78
+ */
79
+ async callTool(name, args, opts = {}) {
80
+ const attempts = opts.retry ? 3 : 1;
81
+ for (let attempt = 0;; attempt += 1) {
82
+ try {
83
+ return await this.request('POST', `/api/v1/tools/${encodeURIComponent(name)}`, args ?? {});
84
+ }
85
+ catch (err) {
86
+ const retryable = err instanceof errors_1.ConsoleApiError ? err.status >= 500 : !(err instanceof errors_1.CliError);
87
+ if (!retryable || attempt + 1 >= attempts)
88
+ throw err;
89
+ await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs * 3 ** attempt));
90
+ }
91
+ }
74
92
  }
75
93
  /** GET a console endpoint that answers with a bare JSON body (e.g. /api/oauth/userinfo). */
76
94
  async getJson(path) {
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ const chat_1 = require("./commands/chat");
18
18
  const prompt_1 = require("./commands/prompt");
19
19
  const workflow_1 = require("./commands/workflow");
20
20
  const tools_1 = require("./commands/tools");
21
+ const run_1 = require("./manage/run");
22
+ const registry_1 = require("./manage/registry");
21
23
  exports.HELP = `metrio - MetrioAI CLI
22
24
 
23
25
  Usage: metrio <command> [options]
@@ -34,13 +36,33 @@ Account (console OAuth):
34
36
  logout Revoke and delete the stored login
35
37
  whoami Show the signed-in user and scopes
36
38
 
37
- Manage (console OAuth):
39
+ Manage (console OAuth; run "metrio <group> --help"):
40
+ project | prompt | version | skill | workflow | schedule | mcp | run | admin | model-tiers
41
+ e.g. metrio version create 12 -f system.md
42
+ metrio skill get 5 -o skill.md
38
43
  tools List the console tools your login can call
39
- call <tool> Call a console tool with JSON args (-f file | --args json)
44
+ call <tool> Call any console tool with JSON args (-f file | --args json)
40
45
 
41
46
  -h, --help Show this help
42
47
  -v, --version Show the CLI version
43
48
  `;
49
+ /** Actions of `prompt` / `workflow` that belong to the original execution commands. */
50
+ const EXECUTION_ACTIONS = {
51
+ prompt: ['run-batch', 'create-version'],
52
+ workflow: ['run', 'run-batch'],
53
+ };
54
+ async function runMixedGroup(group, argv) {
55
+ const action = argv[1];
56
+ const runExecution = group === 'prompt' ? prompt_1.runPromptCommand : workflow_1.runWorkflowCommand;
57
+ if (action !== undefined && EXECUTION_ACTIONS[group].includes(action))
58
+ return runExecution(argv.slice(1));
59
+ if (action === undefined || action === '-h' || action === '--help') {
60
+ await runExecution(['--help']);
61
+ process.stdout.write(`\n${(0, registry_1.groupHelp)(group)}`);
62
+ return;
63
+ }
64
+ return (0, run_1.runManageCommand)(argv);
65
+ }
44
66
  async function main(argv) {
45
67
  const [command] = argv;
46
68
  switch (command) {
@@ -56,9 +78,9 @@ async function main(argv) {
56
78
  case 'chat':
57
79
  return (0, chat_1.runChatCommand)(argv.slice(1));
58
80
  case 'workflow':
59
- return (0, workflow_1.runWorkflowCommand)(argv.slice(1));
81
+ return runMixedGroup('workflow', argv);
60
82
  case 'prompt':
61
- return (0, prompt_1.runPromptCommand)(argv.slice(1));
83
+ return runMixedGroup('prompt', argv);
62
84
  case 'login':
63
85
  return (0, auth_1.runLoginCommand)(argv.slice(1));
64
86
  case 'logout':
@@ -70,6 +92,8 @@ async function main(argv) {
70
92
  case 'call':
71
93
  return (0, tools_1.runCallCommand)(argv.slice(1));
72
94
  default:
95
+ if (registry_1.MANAGE_GROUPS.includes(command))
96
+ return (0, run_1.runManageCommand)(argv);
73
97
  if (command.startsWith('-')) {
74
98
  process.stderr.write('metrio: running chat without a subcommand is deprecated; use "metrio chat".\n');
75
99
  return (0, chat_1.runChatCommand)(argv);
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readText = readText;
37
+ exports.loadInputFile = loadInputFile;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const yaml_1 = require("yaml");
41
+ const errors_1 = require("../errors");
42
+ const stdin = () => fs.readFileSync(0, 'utf8');
43
+ /** Reads a file (or stdin for "-") as raw text, turning I/O errors into usage errors. */
44
+ function readText(file, readStdin = stdin) {
45
+ if (file === '-')
46
+ return readStdin().replace(/^\uFEFF/, '');
47
+ try {
48
+ return fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
49
+ }
50
+ catch (err) {
51
+ throw new errors_1.CliError(`Cannot read ${file}: ${err.message}`);
52
+ }
53
+ }
54
+ function isPlainObject(v) {
55
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
56
+ }
57
+ function structured(value, file) {
58
+ if (Array.isArray(value))
59
+ return { kind: 'array', value };
60
+ if (isPlainObject(value))
61
+ return { kind: 'object', value };
62
+ throw new errors_1.CliError(`${file} must contain an object or an array`);
63
+ }
64
+ function parseJson(raw, file) {
65
+ try {
66
+ return JSON.parse(raw);
67
+ }
68
+ catch (err) {
69
+ throw new errors_1.CliError(`${file} is not valid JSON: ${err.message}`);
70
+ }
71
+ }
72
+ /** Markdown/text, with optional leading `---` YAML frontmatter. Never parsed as YAML otherwise. */
73
+ function parseText(raw, file) {
74
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
75
+ if (!match)
76
+ return { kind: 'text', value: raw };
77
+ let front;
78
+ try {
79
+ front = (0, yaml_1.parse)(match[1]);
80
+ }
81
+ catch (err) {
82
+ throw new errors_1.CliError(`${file}: frontmatter is not valid YAML: ${err.message}`);
83
+ }
84
+ if (!isPlainObject(front))
85
+ throw new errors_1.CliError(`${file}: frontmatter must be a YAML mapping`);
86
+ return { kind: 'text', value: match[2].replace(/^\r?\n/, ''), frontmatter: front };
87
+ }
88
+ /**
89
+ * Loads a `-f` input. By extension: .json/.yaml/.yml → object or array, .jsonl → array,
90
+ * .md/.markdown/.txt → text (leading `---` frontmatter split off unless `frontmatter: false`). "-" reads stdin: JSON when it starts with { or [, otherwise text; always text with `stdinText: true`.
91
+ */
92
+ function loadInputFile(file, readStdin = stdin, opts = {}) {
93
+ const text = (raw, name) => (opts.frontmatter === false ? { kind: 'text', value: raw } : parseText(raw, name));
94
+ const raw = readText(file, readStdin);
95
+ if (file === '-') {
96
+ const trimmed = raw.trimStart();
97
+ return !opts.stdinText && (trimmed.startsWith('{') || trimmed.startsWith('['))
98
+ ? structured(parseJson(raw, 'stdin'), 'stdin')
99
+ : text(raw, 'stdin');
100
+ }
101
+ const ext = path.extname(file).toLowerCase();
102
+ switch (ext) {
103
+ case '.json':
104
+ return structured(parseJson(raw, file), file);
105
+ case '.yaml':
106
+ case '.yml': {
107
+ let parsed;
108
+ try {
109
+ parsed = (0, yaml_1.parse)(raw);
110
+ }
111
+ catch (err) {
112
+ throw new errors_1.CliError(`${file} is not valid YAML: ${err.message}`);
113
+ }
114
+ return structured(parsed, file);
115
+ }
116
+ case '.jsonl':
117
+ return {
118
+ kind: 'array',
119
+ value: raw.split('\n').flatMap((line, i) => {
120
+ if (!line.trim())
121
+ return [];
122
+ try {
123
+ return [JSON.parse(line)];
124
+ }
125
+ catch (err) {
126
+ throw new errors_1.CliError(`${file} line ${i + 1}: ${err.message}`);
127
+ }
128
+ }),
129
+ };
130
+ case '.md':
131
+ case '.markdown':
132
+ case '.txt':
133
+ return text(raw, file);
134
+ default:
135
+ throw new errors_1.CliError(`Unsupported file type ${ext || '(none)'} for ${file}; use .json, .yaml, .yml, .jsonl, .md, .txt or - for stdin`);
136
+ }
137
+ }
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.renderTable = renderTable;
37
+ exports.writeOutput = writeOutput;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const yaml_1 = require("yaml");
41
+ const errors_1 = require("../errors");
42
+ const PREFERRED = [
43
+ 'id', 'external_id', 'prompt_external_id', 'skill_id', 'task_id', 'workflow_id', 'version',
44
+ 'serial', 'slug', 'name', 'status', 'enabled', 'is_active', 'scope', 'created_at',
45
+ ];
46
+ const MAX_CELL = 60;
47
+ const isScalar = (v) => v === null || ['string', 'number', 'boolean'].includes(typeof v);
48
+ function cell(value) {
49
+ const s = value === undefined || value === null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
50
+ const flat = s.replace(/\s+/g, ' ');
51
+ return flat.length > MAX_CELL ? `${flat.slice(0, MAX_CELL - 3)}...` : flat;
52
+ }
53
+ function renderTable(rows) {
54
+ const first = rows[0] ?? {};
55
+ let columns = PREFERRED.filter((c) => c in first).slice(0, 6);
56
+ if (columns.length === 0)
57
+ columns = Object.keys(first).filter((k) => isScalar(first[k])).slice(0, 4);
58
+ const cells = rows.map((r) => columns.map((c) => cell(r[c])));
59
+ const widths = columns.map((c, i) => Math.max(c.length, ...cells.map((r) => r[i].length)));
60
+ const line = (vals) => vals.map((v, i) => (i === vals.length - 1 ? v : v.padEnd(widths[i]))).join(' ');
61
+ return [line(columns), ...cells.map(line)].join('\n');
62
+ }
63
+ function asRecord(data) {
64
+ return typeof data === 'object' && data !== null && !Array.isArray(data) ? data : {};
65
+ }
66
+ async function writeFile(data, opts, file) {
67
+ const record = asRecord(data);
68
+ if (opts.downloadField) {
69
+ const url = record[opts.downloadField];
70
+ if (typeof url !== 'string')
71
+ throw new errors_1.CliError(`The result has no ${opts.downloadField} to download`, 1);
72
+ const res = await fetch(url);
73
+ if (!res.ok)
74
+ throw new errors_1.CliError(`Download failed: HTTP ${res.status}`, 1);
75
+ const bytes = Buffer.from(await res.arrayBuffer());
76
+ fs.writeFileSync(file, bytes);
77
+ process.stderr.write(`Saved ${bytes.length} bytes to ${file}\n`);
78
+ return;
79
+ }
80
+ const ext = path.extname(file).toLowerCase();
81
+ let body;
82
+ if (ext === '.json')
83
+ body = `${JSON.stringify(data, null, 2)}\n`;
84
+ else if (ext === '.yaml' || ext === '.yml')
85
+ body = (0, yaml_1.stringify)(data);
86
+ else if (ext === '.md' || ext === '.markdown' || ext === '.txt') {
87
+ const text = opts.textField ? record[opts.textField] : undefined;
88
+ if (typeof text !== 'string')
89
+ throw new errors_1.CliError(`-o ${file}: this result has no text body; use .json or .yaml`);
90
+ const front = Object.fromEntries((opts.frontmatterKeys ?? []).filter((k) => record[k] !== undefined && record[k] !== null).map((k) => [k, record[k]]));
91
+ const head = Object.keys(front).length ? `---\n${(0, yaml_1.stringify)(front)}---\n\n` : '';
92
+ body = `${head}${text}`;
93
+ }
94
+ else {
95
+ throw new errors_1.CliError(`Unsupported output type ${ext || '(none)'} for ${file}; use .json, .yaml, .md or .txt`);
96
+ }
97
+ fs.writeFileSync(file, body);
98
+ process.stderr.write(`Wrote ${file}\n`);
99
+ }
100
+ /** Renders a tool result: -o file, --json, or a human table/YAML/text on stdout. */
101
+ async function writeOutput(data, opts) {
102
+ if (opts.outFile)
103
+ return writeFile(data, opts, opts.outFile);
104
+ if (opts.json) {
105
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
106
+ return;
107
+ }
108
+ const record = asRecord(data);
109
+ if (Object.keys(record).length === 1 && typeof record.text === 'string') {
110
+ process.stdout.write(record.text.endsWith('\n') ? record.text : `${record.text}\n`);
111
+ return;
112
+ }
113
+ const rows = Array.isArray(data) ? data : Array.isArray(record.items) ? record.items : undefined;
114
+ if (rows) {
115
+ process.stdout.write(rows.length ? `${renderTable(rows)}\n` : '(none)\n');
116
+ const p = asRecord(record.pagination);
117
+ if (p.page !== undefined)
118
+ process.stderr.write(`page ${p.page}/${p.total_pages} (total ${p.total})\n`);
119
+ return;
120
+ }
121
+ process.stdout.write((0, yaml_1.stringify)(data));
122
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveProject = resolveProject;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const yaml_1 = require("yaml");
40
+ const errors_1 = require("../errors");
41
+ /** --project, then METRIO_PROJECT, then `project:` in the nearest metrio.yaml walking up from cwd. */
42
+ function resolveProject(flag, cwd = process.cwd()) {
43
+ if (flag)
44
+ return flag;
45
+ if (process.env.METRIO_PROJECT)
46
+ return process.env.METRIO_PROJECT;
47
+ let dir = path.resolve(cwd);
48
+ for (;;) {
49
+ const file = path.join(dir, 'metrio.yaml');
50
+ if (fs.existsSync(file)) {
51
+ let raw;
52
+ try {
53
+ raw = fs.readFileSync(file, 'utf8');
54
+ }
55
+ catch (err) {
56
+ throw new errors_1.CliError(`Cannot read ${file}: ${err.message}`);
57
+ }
58
+ let doc;
59
+ try {
60
+ doc = (0, yaml_1.parse)(raw);
61
+ }
62
+ catch (err) {
63
+ throw new errors_1.CliError(`${file} is not valid YAML: ${err.message}`);
64
+ }
65
+ if (doc && typeof doc.project === 'string' && doc.project)
66
+ return doc.project;
67
+ throw new errors_1.CliError(`${file} has no "project:" entry`);
68
+ }
69
+ const parent = path.dirname(dir);
70
+ if (parent === dir)
71
+ return undefined;
72
+ dir = parent;
73
+ }
74
+ }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MANAGE_GROUPS = exports.COMMANDS = void 0;
4
+ exports.findCommand = findCommand;
5
+ exports.groupHelp = groupHelp;
6
+ exports.commandHelp = commandHelp;
7
+ const P = (prop, label) => ({ prop, label });
8
+ const PROMPT = P('prompt_external_id', '<prompt-id>');
9
+ const VERSION_ID = P('version_id', '<version-id>');
10
+ const SKILL = P('skill_id', '<skill-id>');
11
+ const WF = P('workflow_id', '<workflow-id>');
12
+ const WF_VERSION = P('version', '<version>');
13
+ const SCHEDULE = P('schedule_id', '<schedule-id>');
14
+ const INSTANCE = P('mcp_instance_id', '<instance-id>');
15
+ const TASK = P('task_id', '<task-id>');
16
+ exports.COMMANDS = [
17
+ { path: ['project', 'list'], tool: 'list_projects', summary: 'List your projects' },
18
+ { path: ['project', 'create'], tool: 'create_project', summary: 'Create a project (-f or --team-id/--name)' },
19
+ { path: ['project', 'update'], tool: 'update_project', summary: 'Rename or describe the current project' },
20
+ { path: ['prompt', 'list'], tool: 'list_prompts', summary: 'List prompts' },
21
+ { path: ['prompt', 'create'], tool: 'create_prompt', summary: 'Create a prompt (--name)' },
22
+ { path: ['prompt', 'update'], tool: 'update_prompt', summary: 'Update a prompt', args: [PROMPT] },
23
+ { path: ['prompt', 'delete'], tool: 'delete_prompt', summary: 'Delete a prompt and all its versions', args: [PROMPT], destructive: true },
24
+ { path: ['prompt', 'skills'], tool: 'list_prompt_skills', summary: 'List skills attached to a prompt', args: [PROMPT] },
25
+ { path: ['prompt', 'skills', 'add'], tool: 'add_prompt_skill', summary: 'Attach a skill', args: [PROMPT, SKILL] },
26
+ { path: ['prompt', 'skills', 'remove'], tool: 'remove_prompt_skill', summary: 'Detach a skill', args: [PROMPT, SKILL] },
27
+ { path: ['prompt', 'mcps'], tool: 'list_prompt_mcps', summary: 'List MCP instances attached to a prompt', args: [PROMPT] },
28
+ { path: ['prompt', 'set-mcp-tools'], tool: 'set_prompt_mcp_tools', summary: 'Set the MCP tool allowlist (-f tools.yaml)', args: [PROMPT] },
29
+ { path: ['prompt', 'diff'], tool: 'diff_prompt', summary: 'Diff two versions', args: [PROMPT, P('from_serial', '<from>'), P('to_serial', '<to>')] },
30
+ { path: ['version', 'list'], tool: 'list_versions', summary: 'List versions of a prompt', args: [PROMPT] },
31
+ { path: ['version', 'get'], tool: 'get_version', summary: 'Get a version (-o v.yaml | system.md)', args: [VERSION_ID], textField: 'system_prompt' },
32
+ { path: ['version', 'create'], tool: 'create_version', summary: 'Create a version (-f system.md | v.yaml)', args: [PROMPT], textField: 'system_prompt' },
33
+ { path: ['version', 'update'], tool: 'update_version', summary: 'Update a version (-f system.md | v.yaml)', args: [VERSION_ID], textField: 'system_prompt' },
34
+ { path: ['version', 'create-batch'], tool: 'create_versions', summary: 'Create versions for several prompts (-f versions.jsonl)', wrap: 'versions' },
35
+ { path: ['skill', 'list'], tool: 'list_skills', summary: 'List skills' },
36
+ { path: ['skill', 'get'], tool: 'get_skill', summary: 'Get a skill (-o skill.md)', args: [SKILL], textField: 'content', frontmatterKeys: ['name', 'description', 'references'] },
37
+ { path: ['skill', 'create'], tool: 'create_skill', summary: 'Create a skill (-f skill.md)', textField: 'content', frontmatterKeys: ['name', 'description', 'references'] },
38
+ { path: ['skill', 'update'], tool: 'update_skill', summary: 'Update a skill (-f skill.md)', args: [SKILL], textField: 'content', frontmatterKeys: ['name', 'description', 'references'] },
39
+ { path: ['skill', 'delete'], tool: 'delete_skill', summary: 'Delete a skill', args: [SKILL], destructive: true },
40
+ { path: ['workflow', 'list'], tool: 'list_workflows', summary: 'List workflows' },
41
+ { path: ['workflow', 'get'], tool: 'get_workflow', summary: 'Get a workflow with its definition', args: [WF] },
42
+ { path: ['workflow', 'create'], tool: 'create_workflow', summary: 'Create a workflow (-f definition.json)', wrap: 'definition' },
43
+ { path: ['workflow', 'update'], tool: 'update_workflow', summary: 'Edit the live version (-f definition.json) or --enabled', args: [WF], wrap: 'definition' },
44
+ { path: ['workflow', 'delete'], tool: 'delete_workflow', summary: 'Delete a workflow', args: [WF], destructive: true },
45
+ { path: ['workflow', 'bump-pins'], tool: 'bump_prompt_pins', summary: 'Repoint prompt pins (-f pins.yaml)', args: [WF], wrap: 'pins' },
46
+ { path: ['workflow', 'version', 'list'], tool: 'list_workflow_versions', summary: 'List workflow versions', args: [WF] },
47
+ { path: ['workflow', 'version', 'get'], tool: 'get_workflow_version', summary: 'Get one workflow version', args: [WF, WF_VERSION] },
48
+ { path: ['workflow', 'version', 'create'], tool: 'create_workflow_version', summary: 'Create a version (-f definition.json)', args: [WF], wrap: 'definition' },
49
+ { path: ['workflow', 'version', 'diff'], tool: 'diff_workflow_version', summary: 'Diff two workflow versions', args: [WF, P('from_version', '<from>'), P('to_version', '<to>')] },
50
+ { path: ['workflow', 'version', 'publish'], tool: 'publish_workflow_version', summary: 'Publish a draft (--activate to go live)', args: [WF, WF_VERSION] },
51
+ { path: ['workflow', 'version', 'activate'], tool: 'set_active_workflow_version', summary: 'Make a published version live (rollback too)', args: [WF, WF_VERSION] },
52
+ { path: ['schedule', 'list'], tool: 'list_schedules', summary: 'List schedules' },
53
+ { path: ['schedule', 'create'], tool: 'create_schedule', summary: 'Create a schedule (-f schedule.yaml)' },
54
+ { path: ['schedule', 'update'], tool: 'update_schedule', summary: 'Update a schedule (-f schedule.yaml)', args: [SCHEDULE] },
55
+ { path: ['schedule', 'delete'], tool: 'delete_schedule', summary: 'Delete a schedule', args: [SCHEDULE], destructive: true },
56
+ { path: ['mcp', 'list'], tool: 'list_mcp_instances', summary: 'List MCP instances' },
57
+ { path: ['mcp', 'tools'], tool: 'list_mcp_instance_tools', summary: 'List tools of an MCP instance', args: [INSTANCE] },
58
+ { path: ['mcp', 'create'], tool: 'create_mcp_instance', summary: 'Create an MCP instance (-f instance.yaml)' },
59
+ { path: ['mcp', 'update'], tool: 'update_mcp_instance', summary: 'Update an MCP instance (-f instance.yaml)', args: [INSTANCE] },
60
+ { path: ['run', 'list'], tool: 'list_runs', summary: 'List execution runs' },
61
+ { path: ['run', 'trace'], tool: 'get_task_trace', summary: 'Call trace for one task', args: [TASK] },
62
+ { path: ['run', 'workflow', 'list'], tool: 'list_workflow_runs', summary: 'List workflow runs' },
63
+ { path: ['run', 'workflow', 'get'], tool: 'get_workflow_run', summary: 'Get one workflow run', args: [TASK] },
64
+ { path: ['run', 'workflow', 'attachment'], tool: 'get_workflow_run_attachment', summary: 'Download a run attachment (-o file)', args: [TASK, P('key', '<key>')], downloadField: 'url' },
65
+ { path: ['admin', 'model', 'list'], tool: 'list_models', summary: 'List models (admin)' },
66
+ { path: ['admin', 'model', 'create'], tool: 'create_model', summary: 'Create a model (admin, -f model.yaml)' },
67
+ { path: ['admin', 'model', 'update'], tool: 'update_model', summary: 'Update a model (admin)', args: [P('model_id', '<model-id>')] },
68
+ { path: ['admin', 'mcp-def', 'list'], tool: 'list_mcp_server_definitions', summary: 'List MCP server definitions (admin)' },
69
+ { path: ['admin', 'mcp-def', 'create'], tool: 'create_mcp_server_definition', summary: 'Register an MCP server (admin, -f def.yaml)' },
70
+ { path: ['model-tiers'], tool: 'list_model_tiers', summary: 'List model tiers' },
71
+ ];
72
+ exports.MANAGE_GROUPS = Array.from(new Set(exports.COMMANDS.map((c) => c.path[0])));
73
+ /** Longest command path that prefixes argv. */
74
+ function findCommand(argv) {
75
+ let best;
76
+ for (const spec of exports.COMMANDS) {
77
+ if (spec.path.every((word, i) => argv[i] === word) && (!best || spec.path.length > best.path.length))
78
+ best = spec;
79
+ }
80
+ return best ? { spec: best, rest: argv.slice(best.path.length) } : undefined;
81
+ }
82
+ const usage = (spec) => ['metrio', ...spec.path, ...(spec.args ?? []).map((a) => a.label)].join(' ');
83
+ function groupHelp(group) {
84
+ const specs = exports.COMMANDS.filter((c) => c.path[0] === group);
85
+ const width = Math.max(...specs.map((s) => usage(s).length));
86
+ return [
87
+ `metrio ${group} - manage ${group} resources (console login)`,
88
+ '',
89
+ ...specs.map((s) => ` ${usage(s).padEnd(width)} ${s.summary}`),
90
+ '',
91
+ 'Run "metrio <command> --help" for its options.',
92
+ '',
93
+ ].join('\n');
94
+ }
95
+ function commandHelp(spec) {
96
+ const lines = [
97
+ `${usage(spec)} - ${spec.summary}`,
98
+ '',
99
+ `Calls the console tool ${spec.tool}. Every input field of that tool can be passed as --<field-name>`,
100
+ '(kebab-case; see "metrio tools --json"). Text fields take @file (start with @@ for a literal @); object/array fields take JSON or @file.',
101
+ '',
102
+ 'Options:',
103
+ ' -f, --file <path> Input: .json/.yaml/.yml (fields), .jsonl (list), .md/.txt (text), - (stdin)',
104
+ ' -o, --output <path> Write the result: .json, .yaml, .md/.txt',
105
+ ' --json Print the raw result as JSON',
106
+ ' --project <slug> Project (default: METRIO_PROJECT or metrio.yaml)',
107
+ ];
108
+ if (spec.destructive)
109
+ lines.push(' --yes Confirm the delete');
110
+ if (spec.textField)
111
+ lines.push('', `A .md/.txt file fills ${spec.textField}.`);
112
+ if (spec.frontmatterKeys)
113
+ lines.push(`Its YAML frontmatter may set: ${spec.frontmatterKeys.join(', ')}.`);
114
+ if (spec.wrap)
115
+ lines.push('', `A file without a "${spec.wrap}" key is sent as ${spec.wrap}.`);
116
+ if (spec.downloadField)
117
+ lines.push('', '-o downloads the file itself.');
118
+ return `${lines.join('\n')}\n`;
119
+ }
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runManageCommand = runManageCommand;
4
+ const client_1 = require("../console/client");
5
+ const errors_1 = require("../errors");
6
+ const input_1 = require("./input");
7
+ const output_1 = require("./output");
8
+ const project_1 = require("./project");
9
+ const registry_1 = require("./registry");
10
+ const schema_1 = require("./schema");
11
+ function parse(rest) {
12
+ const parsed = { tokens: [], json: false, yes: false };
13
+ const take = (i, name) => {
14
+ const v = rest[i + 1];
15
+ if (v === undefined)
16
+ throw new errors_1.CliError(`${name} needs a value`);
17
+ return v;
18
+ };
19
+ for (let i = 0; i < rest.length; i += 1) {
20
+ const arg = rest[i];
21
+ const opt = /^--(file|output|project)=(.*)$/s.exec(arg);
22
+ if (opt) {
23
+ if (opt[1] === 'file')
24
+ parsed.file = opt[2];
25
+ else if (opt[1] === 'output')
26
+ parsed.outFile = opt[2];
27
+ else
28
+ parsed.project = opt[2];
29
+ }
30
+ else if (arg === '-f' || arg === '--file')
31
+ parsed.file = take(i++, arg);
32
+ else if (arg === '-o' || arg === '--output')
33
+ parsed.outFile = take(i++, arg);
34
+ else if (arg === '--project')
35
+ parsed.project = take(i++, arg);
36
+ else if (arg === '--json')
37
+ parsed.json = true;
38
+ else if (arg === '--yes')
39
+ parsed.yes = true;
40
+ else if (arg.startsWith('--')) {
41
+ const eq = arg.indexOf('=');
42
+ parsed.tokens.push(eq === -1 ? { flag: arg.slice(2) } : { flag: arg.slice(2, eq), value: arg.slice(eq + 1) });
43
+ }
44
+ else
45
+ parsed.tokens.push({ positional: arg });
46
+ }
47
+ return parsed;
48
+ }
49
+ function fromFile(spec, file, readStdin) {
50
+ const loaded = (0, input_1.loadInputFile)(file, readStdin, { frontmatter: !!spec.frontmatterKeys, stdinText: !!spec.textField });
51
+ const cmd = `metrio ${spec.path.join(' ')}`;
52
+ if (loaded.kind === 'text') {
53
+ if (!spec.textField)
54
+ throw new errors_1.CliError(`${cmd} does not take a text file; use .json or .yaml`);
55
+ return { ...(loaded.frontmatter ?? {}), [spec.textField]: loaded.value };
56
+ }
57
+ if (loaded.kind === 'array') {
58
+ if (!spec.wrap)
59
+ throw new errors_1.CliError(`${file} holds a list; ${cmd} takes an object`);
60
+ return { [spec.wrap]: loaded.value };
61
+ }
62
+ return spec.wrap && !(spec.wrap in loaded.value) ? { [spec.wrap]: loaded.value } : loaded.value;
63
+ }
64
+ /** True when the non-option words are a proper prefix of some command path, e.g. `workflow version`. */
65
+ function isCommandPrefix(argv) {
66
+ const words = argv.filter((a) => !a.startsWith('-'));
67
+ return registry_1.COMMANDS.some((c) => words.length < c.path.length && words.every((w, i) => c.path[i] === w));
68
+ }
69
+ /** Runs one registry command; argv starts with the group, e.g. ['version', 'create', '12', '-f', 's.md']. */
70
+ async function runManageCommand(argv, readStdin) {
71
+ const match = (0, registry_1.findCommand)(argv);
72
+ const wantsHelp = argv.includes('-h') || argv.includes('--help');
73
+ if (!match || wantsHelp) {
74
+ if (match)
75
+ process.stdout.write((0, registry_1.commandHelp)(match.spec));
76
+ else if (wantsHelp || argv.length === 1 || isCommandPrefix(argv))
77
+ process.stdout.write((0, registry_1.groupHelp)(argv[0]));
78
+ else
79
+ throw new errors_1.CliError(`Unknown command: metrio ${argv.slice(0, 2).join(' ')}. Run "metrio ${argv[0]} --help".`);
80
+ return;
81
+ }
82
+ const { spec, rest } = match;
83
+ const opts = parse(rest);
84
+ const client = new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)());
85
+ const tool = (await client.listTools()).find((t) => t.name === spec.tool);
86
+ if (!tool) {
87
+ throw new errors_1.CliError(`Your login cannot use ${spec.tool} (it needs a scope your login does not have, or the console is older than this CLI). Try "metrio login" again.`, 3);
88
+ }
89
+ const schema = tool.inputSchema;
90
+ const props = schema.properties ?? {};
91
+ const { values, positionals } = (0, schema_1.resolveFlags)(opts.tokens, schema, readStdin);
92
+ const expected = spec.args ?? [];
93
+ if (positionals.length !== expected.length) {
94
+ const labels = expected.map((a) => a.label).join(' ') || 'no arguments';
95
+ throw new errors_1.CliError(`metrio ${spec.path.join(' ')} expects ${labels}, got ${positionals.length ? positionals.join(' ') : 'none'}`);
96
+ }
97
+ const args = {};
98
+ const sources = {};
99
+ const put = (key, value, source) => {
100
+ args[key] = value;
101
+ sources[key] = source;
102
+ };
103
+ if (opts.file)
104
+ for (const [k, v] of Object.entries(fromFile(spec, opts.file, readStdin)))
105
+ put(k, v, `file ${opts.file}`);
106
+ if (opts.project && 'project_slug' in props)
107
+ put('project_slug', opts.project, 'project');
108
+ for (const [k, v] of Object.entries(values))
109
+ put(k, v, `--${(0, schema_1.kebab)(k)}`);
110
+ expected.forEach((arg, i) => put(arg.prop, (0, schema_1.coercePositional)(positionals[i], props[arg.prop], arg.label), arg.label));
111
+ if ('project_slug' in props && !opts.project && args.project_slug === undefined) {
112
+ try {
113
+ const project = (0, project_1.resolveProject)(undefined);
114
+ if (project)
115
+ put('project_slug', project, 'project');
116
+ }
117
+ catch (err) {
118
+ if ((schema.required ?? []).includes('project_slug') || !(err instanceof errors_1.CliError))
119
+ throw err;
120
+ }
121
+ }
122
+ if ((schema.required ?? []).includes('project_slug') && args.project_slug === undefined) {
123
+ throw new errors_1.CliError('No project. Pass --project <slug>, set METRIO_PROJECT, or add "project: <slug>" to metrio.yaml.');
124
+ }
125
+ const { args: accepted, dropped } = (0, schema_1.dropUnaccepted)(args, schema);
126
+ if (dropped.length)
127
+ process.stderr.write(`metrio: ignoring fields ${spec.tool} does not accept: ${dropped.join(', ')}\n`);
128
+ (0, schema_1.validateArgs)(spec.tool, accepted, schema, sources);
129
+ if (spec.destructive && !opts.yes) {
130
+ process.stderr.write(`About to run ${spec.tool} with ${JSON.stringify(accepted)}.\n`);
131
+ throw new errors_1.CliError('Re-run with --yes to confirm.', 2);
132
+ }
133
+ const data = await client.callTool(spec.tool, accepted, { retry: tool.scope === 'read' });
134
+ await (0, output_1.writeOutput)(data, {
135
+ json: opts.json,
136
+ outFile: opts.outFile,
137
+ textField: spec.textField,
138
+ frontmatterKeys: spec.frontmatterKeys,
139
+ downloadField: spec.downloadField,
140
+ });
141
+ if (isRecord(data) && data.valid === false) {
142
+ const errors = Array.isArray(data.errors) ? data.errors : [];
143
+ const first = errors.length ? (typeof errors[0] === 'string' ? errors[0] : JSON.stringify(errors[0])) : 'no details';
144
+ const detail = errors.length > 1 ? `${first} (+${errors.length - 1} more)` : first;
145
+ throw new errors_1.CliError(`metrio: ${spec.tool} reported invalid input: ${detail}`, 2);
146
+ }
147
+ if (isRecord(data) && data.all_succeeded === false) {
148
+ const failed = Array.isArray(data.failed) ? data.failed.length : 0;
149
+ const total = failed + (Array.isArray(data.created) ? data.created.length : 0);
150
+ throw new errors_1.CliError(`metrio: ${spec.tool}: ${failed} of ${total} items failed`, 1);
151
+ }
152
+ }
153
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.propFromFlag = exports.kebab = void 0;
7
+ exports.propertyTypes = propertyTypes;
8
+ exports.coercePositional = coercePositional;
9
+ exports.resolveFlags = resolveFlags;
10
+ exports.dropUnaccepted = dropUnaccepted;
11
+ exports.validateArgs = validateArgs;
12
+ const _2020_1 = __importDefault(require("ajv/dist/2020"));
13
+ const errors_1 = require("../errors");
14
+ const input_1 = require("./input");
15
+ const kebab = (prop) => prop.replace(/_/g, '-');
16
+ exports.kebab = kebab;
17
+ const propFromFlag = (flag) => flag.replace(/-/g, '_');
18
+ exports.propFromFlag = propFromFlag;
19
+ /** The JSON types a property accepts, flattening `anyOf`. Empty = unknown (treated as string). */
20
+ function propertyTypes(schema) {
21
+ if (Array.isArray(schema.type))
22
+ return schema.type;
23
+ if (typeof schema.type === 'string')
24
+ return [schema.type];
25
+ return (schema.anyOf ?? []).flatMap(propertyTypes);
26
+ }
27
+ const isBooleanOnly = (types) => types.length > 0 && types.every((t) => t === 'boolean');
28
+ function coerce(name, raw, prop, readStdin) {
29
+ const types = propertyTypes(prop);
30
+ if (raw === 'null' && types.includes('null'))
31
+ return null;
32
+ if (types.includes('boolean') && (raw === 'true' || raw === 'false'))
33
+ return raw === 'true';
34
+ if (types.includes('number') || types.includes('integer')) {
35
+ const n = Number(raw);
36
+ if (raw.trim() === '' || Number.isNaN(n))
37
+ throw new errors_1.CliError(`${name} must be a number, got "${raw}"`);
38
+ return n;
39
+ }
40
+ if (types.includes('object') || types.includes('array')) {
41
+ if (raw.startsWith('@')) {
42
+ const loaded = (0, input_1.loadInputFile)(raw.slice(1), readStdin);
43
+ if (loaded.kind === 'text')
44
+ throw new errors_1.CliError(`${name}: ${raw.slice(1)} must be .json or .yaml`);
45
+ return loaded.value;
46
+ }
47
+ try {
48
+ return JSON.parse(raw);
49
+ }
50
+ catch (err) {
51
+ throw new errors_1.CliError(`${name} must be valid JSON (or @file): ${err.message}`);
52
+ }
53
+ }
54
+ if (types.includes('boolean'))
55
+ throw new errors_1.CliError(`${name} must be true or false, got "${raw}"`);
56
+ if (raw.startsWith('@@'))
57
+ return raw.slice(1);
58
+ return raw.startsWith('@') ? (0, input_1.readText)(raw.slice(1), readStdin) : raw;
59
+ }
60
+ function coercePositional(raw, prop, label) {
61
+ if (!prop || (raw.startsWith('@') && !propertyTypes(prop).some((t) => ['number', 'integer', 'object', 'array'].includes(t))))
62
+ return raw;
63
+ return coerce(label, raw, prop);
64
+ }
65
+ /**
66
+ * Turns `--field value` tokens into tool arguments typed by the schema. A bare boolean flag
67
+ * never consumes the next token unless it is literally true/false.
68
+ */
69
+ function resolveFlags(tokens, schema, readStdin) {
70
+ const props = schema.properties ?? {};
71
+ const values = {};
72
+ const positionals = [];
73
+ const accepted = () => Object.keys(props).map((p) => `--${(0, exports.kebab)(p)}`).join(', ') || '(none)';
74
+ for (let i = 0; i < tokens.length; i += 1) {
75
+ const token = tokens[i];
76
+ if (token.flag === undefined) {
77
+ positionals.push(token.positional);
78
+ continue;
79
+ }
80
+ let prop = (0, exports.propFromFlag)(token.flag);
81
+ let negated = false;
82
+ if (!(prop in props) && token.flag.startsWith('no-') && (0, exports.propFromFlag)(token.flag.slice(3)) in props) {
83
+ prop = (0, exports.propFromFlag)(token.flag.slice(3));
84
+ negated = true;
85
+ }
86
+ const propSchema = props[prop];
87
+ if (!propSchema)
88
+ throw new errors_1.CliError(`Unknown option --${token.flag}. Accepted fields: ${accepted()}`);
89
+ const name = `--${(0, exports.kebab)(prop)}`;
90
+ const types = propertyTypes(propSchema);
91
+ if (negated) {
92
+ if (!types.includes('boolean'))
93
+ throw new errors_1.CliError(`--${token.flag}: ${name} is not a boolean`);
94
+ values[prop] = false;
95
+ continue;
96
+ }
97
+ let raw = token.value;
98
+ if (raw === undefined) {
99
+ const next = tokens[i + 1];
100
+ const nextIsValue = next !== undefined && next.flag === undefined;
101
+ if (isBooleanOnly(types) && !(nextIsValue && (next.positional === 'true' || next.positional === 'false'))) {
102
+ values[prop] = true;
103
+ continue;
104
+ }
105
+ if (!nextIsValue)
106
+ throw new errors_1.CliError(`${name} needs a value`);
107
+ raw = next.positional;
108
+ i += 1;
109
+ }
110
+ values[prop] = coerce(name, raw, propSchema, readStdin);
111
+ }
112
+ return { values, positionals };
113
+ }
114
+ /** Removes keys the tool does not declare and null values for properties that do not accept null. */
115
+ function dropUnaccepted(args, schema) {
116
+ const props = schema.properties ?? {};
117
+ const kept = {};
118
+ const dropped = [];
119
+ for (const [key, value] of Object.entries(args)) {
120
+ const prop = props[key];
121
+ const types = prop ? propertyTypes(prop) : [];
122
+ if (!prop || (value === null && types.length > 0 && !types.includes('null')))
123
+ dropped.push(key);
124
+ else
125
+ kept[key] = value;
126
+ }
127
+ return { args: kept, dropped };
128
+ }
129
+ const ajv = new _2020_1.default({ allErrors: true, strict: false });
130
+ const validators = new Map();
131
+ function describeError(error, sources) {
132
+ if (error.keyword === 'required') {
133
+ return ` ${error.params.missingProperty} (missing): is required`;
134
+ }
135
+ const field = error.instancePath.replace(/^\//, '').replace(/\//g, '.') || '(root)';
136
+ const top = field.split('.')[0];
137
+ const source = sources[top] ?? 'input';
138
+ return ` ${field} (${source}): ${error.message ?? 'is invalid'}`;
139
+ }
140
+ /** Validates against the tool's JSON Schema; errors list every field with where it came from. */
141
+ function validateArgs(tool, args, schema, sources) {
142
+ const key = `${tool}:${JSON.stringify(schema)}`;
143
+ let validate = validators.get(key);
144
+ if (!validate) {
145
+ validate = ajv.compile(schema);
146
+ validators.set(key, validate);
147
+ }
148
+ if (validate(args))
149
+ return;
150
+ const lines = Array.from(new Set((validate.errors ?? []).map((e) => describeError(e, sources))));
151
+ throw new errors_1.CliError(`Invalid input for ${tool}:\n${lines.join('\n')}`, 2);
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metrio-ai/cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "packageManager": "yarn@4.14.1",
5
5
  "description": "Metrio AI command-line interface: run prompts and workflows, manage Metrio resources",
6
6
  "bin": {
@@ -22,14 +22,16 @@
22
22
  "license": "BSD-3-Clause",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "https://github.com/JidouAI/metrioai-backend.git",
25
+ "url": "git+https://github.com/JidouAI/metrioai-backend.git",
26
26
  "directory": "packages/cli"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@metrio-ai/client": "^1.5.2"
32
+ "@metrio-ai/client": "^1.5.2",
33
+ "ajv": "^8.17.1",
34
+ "yaml": "^2.8.3"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/jest": "^29.5.14",