@metrio-ai/cli 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/commands/sync.js +104 -19
- package/dist/commands/verify.js +67 -22
- package/dist/index.js +2 -2
- package/dist/manage/registry.js +13 -0
- package/dist/manage/run.js +19 -12
- package/dist/sync/push-agents.js +380 -0
- package/dist/sync/push.js +16 -4
- package/dist/sync/remote.js +52 -4
- package/dist/sync/render.js +119 -0
- package/dist/sync/status.js +35 -1
- package/dist/sync/workspace.js +3 -2
- package/dist/verify/assert.js +19 -0
- package/dist/verify/cases.js +15 -6
- package/dist/verify/execute.js +56 -3
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -70,7 +70,9 @@ metrio version list pick-products # prompt directories work wherever a promp
|
|
|
70
70
|
- Renaming a prompt or skill directory is detected by content (status `renamed`); pull/push move the state entry. Workflow directories are named by workflow id and cannot be renamed locally; push refuses it.
|
|
71
71
|
- Known limitation: renaming AND editing a prompt/skill directory in one go looks like a delete plus a new resource (push would create a second one; `--prune --yes` would delete the old one). Rename first, sync, then edit.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Metrio agents sync too: `agents/<dir>/agent.yaml` (entry prompt and version, sub-agents with version pins, tool policies), `--only agents`, with a sub-agent pin bumped automatically when its entry prompt gets a new head version (`push --activate`, or a new prompt's first version; `--no-bump-pins` to opt out). Manage them with `metrio agent ...`. This needs a console that has the agent tools (metrioai-console IMP-3). Details: `docs/cli.md` in the repository.
|
|
74
|
+
|
|
75
|
+
## Verify prompts, workflows and agents
|
|
74
76
|
|
|
75
77
|
Put a `cases.yaml` next to a synced resource (`prompts/<name>/cases.yaml`, `workflows/<id>/cases.yaml`) and run it with the project API key (`METRIOAI_API_KEY` in `.env`, or `--api-key`; it must belong to the workspace's project):
|
|
76
78
|
|
|
@@ -95,4 +97,6 @@ metrio push --activate # make it live once the cases pass
|
|
|
95
97
|
metrio verify -p 12 --cases cases.yaml --junit report.xml # outside a workspace
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
Agents work the same way (`agents/<dir>/cases.yaml` or `-a <id|dir>`), with the extra assertion `stop_reason` (`final`, `terminal_tool`, `max_turns`, `interrupted`, `cancelled`); they always run live.
|
|
101
|
+
|
|
98
102
|
Every `cases.yaml` is validated before the first request (a typo in an assertion is exit 2). A failed case prints each failing assertion with expected vs actual and exits 1. `--json` prints machine-readable results, `--junit <file>` writes JUnit XML, `--fail-fast` stops starting new cases after the first failure. Run `metrio verify --help` for the full format.
|
package/dist/commands/sync.js
CHANGED
|
@@ -47,14 +47,15 @@ const diff_1 = require("diff");
|
|
|
47
47
|
const client_1 = require("../console/client");
|
|
48
48
|
const errors_1 = require("../errors");
|
|
49
49
|
const push_1 = require("../sync/push");
|
|
50
|
+
const push_agents_1 = require("../sync/push-agents");
|
|
50
51
|
const remote_1 = require("../sync/remote");
|
|
51
52
|
const status_1 = require("../sync/status");
|
|
52
53
|
const workspace_1 = require("../sync/workspace");
|
|
53
|
-
const ONLY = { prompts: 'prompt', skills: 'skill', workflows: 'workflow' };
|
|
54
|
+
const ONLY = { prompts: 'prompt', skills: 'skill', workflows: 'workflow', agents: 'agent' };
|
|
54
55
|
function parseSyncArgs(argv) {
|
|
55
56
|
const o = {
|
|
56
|
-
paths: [], kinds: ['prompt', 'skill', 'workflow'], force: false, activate: false, publish: false,
|
|
57
|
-
prune: false, yes: false, dryRun: false, json: false,
|
|
57
|
+
paths: [], kinds: ['prompt', 'skill', 'workflow', 'agent'], force: false, activate: false, publish: false,
|
|
58
|
+
prune: false, yes: false, bumpPins: true, dryRun: false, json: false,
|
|
58
59
|
};
|
|
59
60
|
for (let i = 0; i < argv.length; i += 1) {
|
|
60
61
|
const arg = argv[i];
|
|
@@ -68,7 +69,7 @@ function parseSyncArgs(argv) {
|
|
|
68
69
|
if (arg === '--only') {
|
|
69
70
|
const kind = ONLY[value()];
|
|
70
71
|
if (!kind)
|
|
71
|
-
throw new errors_1.CliError('--only must be prompts, skills or
|
|
72
|
+
throw new errors_1.CliError('--only must be prompts, skills, workflows or agents');
|
|
72
73
|
o.kinds = [kind];
|
|
73
74
|
}
|
|
74
75
|
else if (arg === '-m' || arg === '--message')
|
|
@@ -85,6 +86,8 @@ function parseSyncArgs(argv) {
|
|
|
85
86
|
o.prune = true;
|
|
86
87
|
else if (arg === '--yes')
|
|
87
88
|
o.yes = true;
|
|
89
|
+
else if (arg === '--no-bump-pins')
|
|
90
|
+
o.bumpPins = false;
|
|
88
91
|
else if (arg === '--dry-run')
|
|
89
92
|
o.dryRun = true;
|
|
90
93
|
else if (arg === '--json')
|
|
@@ -123,25 +126,37 @@ function filterEntries(entries, root, paths) {
|
|
|
123
126
|
async function loadEntries(ctx, opts) {
|
|
124
127
|
const state = (0, workspace_1.readState)(ctx.root, ctx.project);
|
|
125
128
|
const remote = await (0, remote_1.fetchRemote)(ctx.client, ctx.project, opts.kinds);
|
|
126
|
-
|
|
129
|
+
if (remote.agentsUnavailable) {
|
|
130
|
+
process.stderr.write('agents skipped: this console has no agent tools (list_agents)\n');
|
|
131
|
+
opts.kinds = opts.kinds.filter((k) => k !== 'agent'); // later re-reads in this run leave agents out too
|
|
132
|
+
}
|
|
133
|
+
for (const a of remote.skippedAgents) {
|
|
134
|
+
process.stderr.write(safe(`agent ${a.id} "${a.name}" skipped: its entry prompt was deleted (fix it with "metrio agent update")\n`));
|
|
135
|
+
}
|
|
136
|
+
// A tracked agent left out is kept as it is, not shown as deleted in the console.
|
|
137
|
+
const skipped = new Set(remote.skippedAgents.map((a) => a.id));
|
|
138
|
+
const entries = (0, status_1.computeEntries)(ctx.root, state, remote.resources, opts.kinds).filter((e) => !(e.kind === 'agent' && e.state && skipped.has(e.state.id)));
|
|
139
|
+
return filterEntries(entries, ctx.root, opts.paths);
|
|
127
140
|
}
|
|
128
141
|
/** Console-sourced text must not carry terminal control sequences. */
|
|
129
142
|
function safe(text) {
|
|
130
143
|
return text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
131
144
|
}
|
|
132
|
-
exports.SYNC_HELP = `metrio pull | diff | push - sync prompts, skills and
|
|
145
|
+
exports.SYNC_HELP = `metrio pull | diff | push - sync prompts, skills, workflows and agents with local files
|
|
133
146
|
|
|
134
147
|
metrio pull --project <slug> First pull: creates metrio.yaml here and writes:
|
|
135
148
|
prompts/<name>/prompt.yaml + system.md
|
|
136
149
|
skills/<name>.md
|
|
137
150
|
workflows/<id>/workflow.yaml + definition.json
|
|
151
|
+
agents/<name>/agent.yaml
|
|
138
152
|
metrio pull [paths…] Update files from the console (keeps local edits unless --force)
|
|
139
153
|
metrio diff [paths…] Show what differs: modified, remote-changed, conflict, new, …
|
|
140
154
|
metrio push [paths…] Send local edits: new prompt version (not live unless --activate),
|
|
141
|
-
skill update, workflow version (draft; --publish / --activate)
|
|
155
|
+
skill update, workflow version (draft; --publish / --activate),
|
|
156
|
+
agent create/update (sub-agents before their parents)
|
|
142
157
|
|
|
143
158
|
Options:
|
|
144
|
-
--only prompts|skills|workflows Limit to one kind
|
|
159
|
+
--only prompts|skills|workflows|agents Limit to one kind
|
|
145
160
|
--force pull: overwrite local edits; push: push over console changes
|
|
146
161
|
(local wins — bindings and MCP list are replaced), allow
|
|
147
162
|
pruning resources changed in the console, and allow a prompt
|
|
@@ -150,6 +165,9 @@ Options:
|
|
|
150
165
|
--activate push: make new prompt versions head / workflow versions live
|
|
151
166
|
--publish push: publish new workflow versions without activating
|
|
152
167
|
--prune --yes push: delete console resources whose local files were removed
|
|
168
|
+
--no-bump-pins push: keep sub-agent pins; by default a pin moves to the head
|
|
169
|
+
version this push creates for the sub-agent's entry prompt
|
|
170
|
+
(--activate, or a new prompt's first version)
|
|
153
171
|
--dry-run Print the plan without changing anything
|
|
154
172
|
--json diff: machine-readable statuses
|
|
155
173
|
|
|
@@ -312,7 +330,7 @@ function remoteContent(r) {
|
|
|
312
330
|
}
|
|
313
331
|
}
|
|
314
332
|
const remoteMoved = (e) => (0, workspace_1.hashFiles)(e.remote.files) !== e.state.remote;
|
|
315
|
-
function planPush(entries, opts, options) {
|
|
333
|
+
function planPush(entries, state, opts, options) {
|
|
316
334
|
const actions = [];
|
|
317
335
|
for (const entry of entries) {
|
|
318
336
|
switch (entry.status) {
|
|
@@ -369,14 +387,47 @@ function planPush(entries, opts, options) {
|
|
|
369
387
|
break;
|
|
370
388
|
}
|
|
371
389
|
}
|
|
372
|
-
|
|
390
|
+
const withOp = (op) => actions.flatMap((a) => ('op' in a && a.op === op ? [a.entry] : []));
|
|
391
|
+
(0, push_agents_1.checkAgentRefs)(entries.filter((e) => e.kind === 'agent' && e.local), new Set([...withOp('create'), ...withOp('rename')].map((e) => e.key)), withOp('delete'), withOp('rename'), entries.filter((e) => e.status === 'remote-deleted'), state);
|
|
392
|
+
return sortForExecution(actions, entries);
|
|
373
393
|
}
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
394
|
+
const isAgentWrite = (a) => a.entry.kind === 'agent' && 'op' in a && (a.op === 'create' || a.op === 'update');
|
|
395
|
+
const isAgentDelete = (a) => a.entry.kind === 'agent' && 'op' in a && a.op === 'delete';
|
|
396
|
+
const KIND_RANK = { skill: 0, prompt: 1, workflow: 2, agent: 3 };
|
|
397
|
+
const DELETE_RANK = { agent: 4, prompt: 5, workflow: 5, skill: 6 };
|
|
398
|
+
/**
|
|
399
|
+
* Skills first (prompts may name new ones), then prompts, workflows, and agents (sub-agents before
|
|
400
|
+
* their parents; a local cycle fails before any write). Deletes come after every create/update:
|
|
401
|
+
* agents first (parents before children, so a parent drops a link before its child goes), then
|
|
402
|
+
* prompts and workflows, skills after what uses them.
|
|
403
|
+
*/
|
|
404
|
+
function sortForExecution(actions, entries) {
|
|
378
405
|
const rank = (a) => ('op' in a && a.op === 'delete' ? DELETE_RANK : KIND_RANK)[a.entry.kind];
|
|
379
|
-
|
|
406
|
+
const sorted = [...actions].sort((a, b) => rank(a) - rank(b));
|
|
407
|
+
const written = new Set(sorted.filter(isAgentWrite).map((a) => a.entry.key));
|
|
408
|
+
const writeOrder = (0, push_agents_1.agentWriteOrder)(entries, written);
|
|
409
|
+
const deleteOrder = (0, push_agents_1.agentDeleteOrder)(sorted.filter(isAgentDelete).map((a) => a.entry), entries);
|
|
410
|
+
const reorder = (list, pick, order) => {
|
|
411
|
+
const picked = list.filter(pick).sort((a, b) => order.indexOf(a.entry.key) - order.indexOf(b.entry.key));
|
|
412
|
+
let i = 0;
|
|
413
|
+
return list.map((a) => (pick(a) ? picked[i++] : a));
|
|
414
|
+
};
|
|
415
|
+
return reorder(reorder(sorted, isAgentWrite, writeOrder), isAgentDelete, deleteOrder);
|
|
416
|
+
}
|
|
417
|
+
/** The keys and ids state will hold once `done` is recorded (what agent references render against). */
|
|
418
|
+
function stateAfter(state, done) {
|
|
419
|
+
const resources = { ...state.resources };
|
|
420
|
+
for (const d of done) {
|
|
421
|
+
if (d.op === 'delete')
|
|
422
|
+
delete resources[d.entry.key];
|
|
423
|
+
else if (d.op === 'rename') {
|
|
424
|
+
resources[d.entry.key] = resources[d.entry.from];
|
|
425
|
+
delete resources[d.entry.from];
|
|
426
|
+
}
|
|
427
|
+
else
|
|
428
|
+
resources[d.entry.key] = { ...(resources[d.entry.key] ?? {}), kind: d.entry.kind, id: d.result.id };
|
|
429
|
+
}
|
|
430
|
+
return { ...state, resources };
|
|
380
431
|
}
|
|
381
432
|
const PAST = { create: 'created', update: 'updated', delete: 'deleted', rename: 'renamed' };
|
|
382
433
|
const describeOp = (op, e) => (op === 'rename' ? `${e.from} -> ${e.key}` : e.key);
|
|
@@ -392,14 +443,21 @@ async function runPushCommand(argv) {
|
|
|
392
443
|
const state = (0, workspace_1.readState)(ctx.root, ctx.project);
|
|
393
444
|
const options = { activate: opts.activate, publish: opts.publish, message: opts.message, force: opts.force };
|
|
394
445
|
const entries = await loadEntries(ctx, opts);
|
|
395
|
-
const actions = planPush(entries, opts, options);
|
|
446
|
+
const actions = planPush(entries, state, opts, options);
|
|
396
447
|
if (opts.dryRun) {
|
|
397
448
|
for (const a of actions) {
|
|
398
449
|
process.stdout.write(safe('op' in a ? `would ${a.op} ${describeOp(a.op, a.entry)}\n` : `skip ${a.entry.key}: ${a.skip}\n`));
|
|
399
450
|
}
|
|
400
451
|
return;
|
|
401
452
|
}
|
|
402
|
-
const
|
|
453
|
+
const agents = {
|
|
454
|
+
root: ctx.root,
|
|
455
|
+
ids: new Map(Object.entries(state.resources).map(([key, s]) => [key, s.id])),
|
|
456
|
+
promptSerials: new Map(),
|
|
457
|
+
entries,
|
|
458
|
+
bumpPins: opts.bumpPins,
|
|
459
|
+
};
|
|
460
|
+
const pushCtx = { client: ctx.client, project: ctx.project, options, agents };
|
|
403
461
|
const done = [];
|
|
404
462
|
let failed = 0;
|
|
405
463
|
for (const a of actions) {
|
|
@@ -418,6 +476,15 @@ async function runPushCommand(argv) {
|
|
|
418
476
|
await (0, push_1.deleteEntry)(pushCtx, a.entry);
|
|
419
477
|
else if (a.op !== 'rename')
|
|
420
478
|
result = await (0, push_1.pushEntry)(pushCtx, a.entry);
|
|
479
|
+
// Later agents resolve references to what this push created or renamed.
|
|
480
|
+
if (result)
|
|
481
|
+
agents.ids.set(a.entry.key, result.id);
|
|
482
|
+
if (a.op === 'rename' && state.resources[a.entry.from])
|
|
483
|
+
agents.ids.set(a.entry.key, state.resources[a.entry.from].id);
|
|
484
|
+
// Only a version that went live moves pins: a non-head one is still to be verified.
|
|
485
|
+
if (a.entry.kind === 'prompt' && result?.pushed_head && result.pushed_serial !== undefined) {
|
|
486
|
+
agents.promptSerials.set(result.id, result.pushed_serial);
|
|
487
|
+
}
|
|
421
488
|
done.push({ entry: a.entry, op: a.op, result });
|
|
422
489
|
process.stdout.write(safe(`${PAST[a.op]} ${describeOp(a.op, a.entry)}${result ? ` (${result.summary})` : ''}\n`));
|
|
423
490
|
}
|
|
@@ -426,6 +493,21 @@ async function runPushCommand(argv) {
|
|
|
426
493
|
process.stderr.write(safe(`failed ${a.entry.key}: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
427
494
|
}
|
|
428
495
|
}
|
|
496
|
+
// Unchanged agents whose sub-agent's entry prompt got a new version: move their pins.
|
|
497
|
+
try {
|
|
498
|
+
await (0, push_agents_1.bumpUnchangedPins)(pushCtx, entries.filter((e) => !done.some((d) => d.entry === e)), (entry, result) => {
|
|
499
|
+
done.push({ entry, op: 'update', result });
|
|
500
|
+
process.stdout.write(safe(`updated ${entry.key} (${result.summary})\n`));
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
failed += 1;
|
|
505
|
+
process.stderr.write(safe(`failed: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
506
|
+
}
|
|
507
|
+
const unbumped = (0, push_agents_1.parentsOutOfScope)(agents);
|
|
508
|
+
if (unbumped.length) {
|
|
509
|
+
process.stderr.write(safe(`sub-agent pins not bumped for ${unbumped.join(', ')}: not in this push (run "metrio push" or "metrio agent bump-pins")\n`));
|
|
510
|
+
}
|
|
429
511
|
// Converged resources (same content on both sides, stale state): record them as synced.
|
|
430
512
|
const converged = entries.filter((e) => e.refresh && !done.some((d) => d.entry === e));
|
|
431
513
|
for (const e of converged) {
|
|
@@ -439,8 +521,11 @@ async function runPushCommand(argv) {
|
|
|
439
521
|
const pushed = done.filter((d) => d.result);
|
|
440
522
|
let refreshed;
|
|
441
523
|
if (pushed.length) {
|
|
524
|
+
// Agent files reference other resources by directory: re-read every loaded kind to render them as diff will.
|
|
525
|
+
const kinds = pushed.some((d) => d.entry.kind === 'agent') ? opts.kinds : [...new Set(pushed.map((d) => d.entry.kind))];
|
|
442
526
|
try {
|
|
443
|
-
refreshed = await (0, remote_1.fetchRemote)(ctx.client, ctx.project,
|
|
527
|
+
refreshed = (await (0, remote_1.fetchRemote)(ctx.client, ctx.project, kinds)).resources;
|
|
528
|
+
(0, status_1.renderRemoteAgents)(stateAfter(state, done), refreshed, opts.kinds);
|
|
444
529
|
}
|
|
445
530
|
catch (error) {
|
|
446
531
|
refetchFailed = true;
|
|
@@ -464,7 +549,7 @@ async function runPushCommand(argv) {
|
|
|
464
549
|
...(previous ?? {}),
|
|
465
550
|
kind: d.entry.kind,
|
|
466
551
|
id: r.id,
|
|
467
|
-
base: (0, workspace_1.hashFiles)(d.entry.local),
|
|
552
|
+
base: (0, workspace_1.hashFiles)(r.files ?? d.entry.local),
|
|
468
553
|
// Without a re-read, keep the last known remote hash: the next diff then shows what really changed.
|
|
469
554
|
remote: refreshed ? (remote ? (0, workspace_1.hashFiles)(remote.files) : '') : (previous?.remote ?? ''),
|
|
470
555
|
...(r.pushed_version_id !== undefined ? { pushed_version_id: r.pushed_version_id, pushed_serial: r.pushed_serial } : {}),
|
package/dist/commands/verify.js
CHANGED
|
@@ -38,7 +38,7 @@ exports.parseVerifyArgs = parseVerifyArgs;
|
|
|
38
38
|
exports.resolveTargets = resolveTargets;
|
|
39
39
|
exports.runVerifyCommand = runVerifyCommand;
|
|
40
40
|
/**
|
|
41
|
-
* `metrio verify` — runs cases.yaml against prompts and
|
|
41
|
+
* `metrio verify` — runs cases.yaml against prompts, workflows and Metrio agents and checks every reply.
|
|
42
42
|
* Execution uses the project API key, like `chat` and `run-batch`. Targets come from the
|
|
43
43
|
* workspace (.metrio/state.json) or from -p / -w with --cases. Every cases.yaml is
|
|
44
44
|
* validated before the first request.
|
|
@@ -56,10 +56,13 @@ const report_1 = require("../verify/report");
|
|
|
56
56
|
exports.VERIFY_HELP = `metrio verify - run cases.yaml and check every reply
|
|
57
57
|
|
|
58
58
|
Usage:
|
|
59
|
-
metrio verify [paths…] [options] Workspace prompts/<name>/cases.yaml
|
|
60
|
-
workflows/<id>/cases.yaml
|
|
59
|
+
metrio verify [paths…] [options] Workspace prompts/<name>/cases.yaml,
|
|
60
|
+
workflows/<id>/cases.yaml and
|
|
61
|
+
agents/<name>/cases.yaml
|
|
61
62
|
metrio verify -p <prompt-id> --cases <file> One prompt, any cases file
|
|
62
63
|
metrio verify -w <workflow-id> --cases <file> One workflow, any cases file
|
|
64
|
+
metrio verify -a <id|dir> --cases <file> One Metrio agent (external id, or an agents/<dir>
|
|
65
|
+
name from .metrio/state.json), any cases file
|
|
63
66
|
|
|
64
67
|
In a workspace (see "metrio pull --help") each cases.yaml runs against the resource
|
|
65
68
|
recorded in .metrio/state.json. Paths narrow the run: a resource directory, prompts/,
|
|
@@ -68,7 +71,8 @@ workflows/ or the root.
|
|
|
68
71
|
Options:
|
|
69
72
|
--version active|pushed|<n> active (default): the live version; pushed: the version
|
|
70
73
|
the last "metrio push" created; <n>: prompt version serial
|
|
71
|
-
or workflow version
|
|
74
|
+
or workflow version. Agents always run live (also for
|
|
75
|
+
pushed); a number is rejected for them
|
|
72
76
|
(a number needs a single prompt or workflow path)
|
|
73
77
|
--concurrency <n> Cases in flight (default 4)
|
|
74
78
|
--json Print results as JSON instead of the text report
|
|
@@ -99,6 +103,8 @@ cases.yaml:
|
|
|
99
103
|
max_latency_ms: 8000
|
|
100
104
|
max_tokens: 4000 # input + output
|
|
101
105
|
max_llm_calls: 3 # workflows
|
|
106
|
+
stop_reason: final # agents: final | terminal_tool | max_turns |
|
|
107
|
+
# interrupted | cancelled
|
|
102
108
|
|
|
103
109
|
Agent loop: pull → edit → push → verify --version pushed → push --activate
|
|
104
110
|
|
|
@@ -151,6 +157,14 @@ function parseVerifyArgs(argv) {
|
|
|
151
157
|
o.workflowId = v;
|
|
152
158
|
break;
|
|
153
159
|
}
|
|
160
|
+
case '-a':
|
|
161
|
+
case '--agent-id': {
|
|
162
|
+
const v = value().trim();
|
|
163
|
+
if (!v)
|
|
164
|
+
throw new errors_1.CliError(`${arg} cannot be empty`);
|
|
165
|
+
o.agent = v;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
154
168
|
case '--cases':
|
|
155
169
|
o.cases = value();
|
|
156
170
|
break;
|
|
@@ -173,22 +187,44 @@ function parseVerifyArgs(argv) {
|
|
|
173
187
|
o.paths.push(arg);
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
|
-
const direct = o.promptId !== undefined || o.workflowId !== undefined;
|
|
177
|
-
if (o.promptId
|
|
178
|
-
throw new errors_1.CliError('Use -p
|
|
190
|
+
const direct = o.promptId !== undefined || o.workflowId !== undefined || o.agent !== undefined;
|
|
191
|
+
if ([o.promptId, o.workflowId, o.agent].filter((v) => v !== undefined).length > 1)
|
|
192
|
+
throw new errors_1.CliError('Use only one of -p, -w, -a');
|
|
179
193
|
if (direct && !o.cases)
|
|
180
|
-
throw new errors_1.CliError('-p / -w need --cases <file>');
|
|
194
|
+
throw new errors_1.CliError('-p / -w / -a need --cases <file>');
|
|
181
195
|
if (!direct && o.cases)
|
|
182
|
-
throw new errors_1.CliError('--cases needs -p <prompt-id> or -
|
|
196
|
+
throw new errors_1.CliError('--cases needs -p <prompt-id>, -w <workflow-id> or -a <agent-id|dir>');
|
|
183
197
|
if (direct && o.paths.length)
|
|
184
|
-
throw new errors_1.CliError('Workspace paths cannot be combined with -p / -w');
|
|
185
|
-
|
|
198
|
+
throw new errors_1.CliError('Workspace paths cannot be combined with -p / -w / -a');
|
|
199
|
+
// Agents always run live, so --version pushed is a no-op for -a.
|
|
200
|
+
if (direct && o.agent === undefined && o.version === 'pushed') {
|
|
186
201
|
throw new errors_1.CliError('--version pushed reads .metrio/state.json; with -p / -w use active or a version number');
|
|
187
202
|
}
|
|
203
|
+
if (o.agent !== undefined && typeof o.version === 'number')
|
|
204
|
+
throw new errors_1.CliError('Agents have no versions; --version <n> does not apply to -a');
|
|
188
205
|
return o;
|
|
189
206
|
}
|
|
207
|
+
/** `-a` takes an external id, or a workspace directory (agents/<dir> or <dir>) resolved through state. */
|
|
208
|
+
function agentRef(raw) {
|
|
209
|
+
if (/^\d+$/.test(raw))
|
|
210
|
+
return agentId(raw, `-a ${raw}`);
|
|
211
|
+
const root = (0, workspace_1.findRoot)();
|
|
212
|
+
if (!root)
|
|
213
|
+
throw new errors_1.CliError(`-a ${raw}: not a numeric id, and not inside a metrio workspace to look the directory up`);
|
|
214
|
+
const key = raw.startsWith('agents/') ? raw : `agents/${raw}`;
|
|
215
|
+
const entry = (0, workspace_1.readState)(root, (0, workspace_1.readProject)(root)).resources[key];
|
|
216
|
+
if (!entry || entry.kind !== 'agent')
|
|
217
|
+
throw new errors_1.CliError(`-a ${raw}: ${key} is not an agent in .metrio/state.json; run "metrio push" first`);
|
|
218
|
+
return agentId(entry.id, key);
|
|
219
|
+
}
|
|
220
|
+
function agentId(raw, label) {
|
|
221
|
+
const n = Number(raw);
|
|
222
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
223
|
+
throw new errors_1.CliError(`${label}: agent id "${raw}" is not a positive integer`);
|
|
224
|
+
return n;
|
|
225
|
+
}
|
|
190
226
|
function directTarget(o) {
|
|
191
|
-
const kind = o.promptId !== undefined ? 'prompt' : 'workflow';
|
|
227
|
+
const kind = o.promptId !== undefined ? 'prompt' : o.agent !== undefined ? 'agent' : 'workflow';
|
|
192
228
|
const file = o.cases;
|
|
193
229
|
let raw;
|
|
194
230
|
try {
|
|
@@ -199,12 +235,12 @@ function directTarget(o) {
|
|
|
199
235
|
catch (err) {
|
|
200
236
|
throw new errors_1.CliError(`Cannot read cases file ${file}: ${err.message}`);
|
|
201
237
|
}
|
|
202
|
-
const ref = kind === 'prompt' ? o.promptId : o.workflowId;
|
|
238
|
+
const ref = kind === 'prompt' ? o.promptId : kind === 'agent' ? agentRef(o.agent) : o.workflowId;
|
|
203
239
|
return {
|
|
204
|
-
label: `${kind} ${ref}`,
|
|
240
|
+
label: `${kind} ${kind === 'agent' ? o.agent : ref}`,
|
|
205
241
|
kind,
|
|
206
242
|
ref,
|
|
207
|
-
version: typeof o.version === 'number' ? o.version : undefined,
|
|
243
|
+
version: kind !== 'agent' && typeof o.version === 'number' ? o.version : undefined,
|
|
208
244
|
cases: (0, cases_1.parseCasesYaml)(raw, { kind, file, baseDir: path.dirname(path.resolve(file)) }),
|
|
209
245
|
};
|
|
210
246
|
}
|
|
@@ -215,7 +251,7 @@ function resolveTargets(o) {
|
|
|
215
251
|
if (!root)
|
|
216
252
|
throw new errors_1.CliError('Not in a metrio workspace (no metrio.yaml). Run it inside one, or use -p / -w with --cases.');
|
|
217
253
|
const state = (0, workspace_1.readState)(root, (0, workspace_1.readProject)(root));
|
|
218
|
-
const listed = (0, workspace_1.listLocal)(root, ['prompt', 'workflow']);
|
|
254
|
+
const listed = (0, workspace_1.listLocal)(root, ['prompt', 'workflow', 'agent']);
|
|
219
255
|
// Filter by path first so an unrelated resource's cases.yaml is never read.
|
|
220
256
|
const inScope = (0, sync_1.filterEntries)(listed, root, o.paths);
|
|
221
257
|
const read = (rs) => rs.map((r) => ({ ...r, cases: (0, workspace_1.readResourceFile)(root, r.key, 'cases.yaml') }));
|
|
@@ -225,18 +261,23 @@ function resolveTargets(o) {
|
|
|
225
261
|
}
|
|
226
262
|
const selected = read(inScope).filter((r) => r.cases !== undefined);
|
|
227
263
|
if (!selected.length) {
|
|
228
|
-
throw new errors_1.CliError('No cases.yaml in this workspace. Add prompts/<name>/cases.yaml
|
|
264
|
+
throw new errors_1.CliError('No cases.yaml in this workspace. Add prompts/<name>/cases.yaml, workflows/<id>/cases.yaml or agents/<name>/cases.yaml.');
|
|
229
265
|
}
|
|
230
266
|
if (typeof o.version === 'number' && selected.length > 1) {
|
|
231
267
|
throw new errors_1.CliError(`--version <n> pins one resource; name a single prompt or workflow path (matched: ${selected.map((r) => r.key).join(', ')})`);
|
|
232
268
|
}
|
|
269
|
+
if (typeof o.version === 'number' && selected.some((r) => r.kind === 'agent')) {
|
|
270
|
+
throw new errors_1.CliError('Agents have no versions; --version <n> does not apply to agents');
|
|
271
|
+
}
|
|
233
272
|
return selected.map((r) => {
|
|
234
273
|
const entry = state.resources[r.key];
|
|
235
274
|
if (!entry)
|
|
236
275
|
throw new errors_1.CliError(`${r.key} is not in .metrio/state.json yet; run "metrio push" first`);
|
|
237
276
|
const kind = r.kind;
|
|
238
277
|
let version;
|
|
239
|
-
if (
|
|
278
|
+
if (kind === 'agent')
|
|
279
|
+
version = undefined;
|
|
280
|
+
else if (typeof o.version === 'number')
|
|
240
281
|
version = o.version;
|
|
241
282
|
else if (o.version === 'pushed') {
|
|
242
283
|
version = kind === 'prompt' ? entry.pushed_serial : entry.pushed_workflow_version;
|
|
@@ -244,9 +285,9 @@ function resolveTargets(o) {
|
|
|
244
285
|
throw new errors_1.CliError(`${r.key}: no pushed version recorded in .metrio/state.json; run "metrio push"; a resource created by its first push (e.g. a new workflow) has none, so use --version active for it`);
|
|
245
286
|
}
|
|
246
287
|
}
|
|
247
|
-
const ref = kind === 'prompt' ? Number(entry.id) : entry.id;
|
|
248
|
-
if (kind
|
|
249
|
-
throw new errors_1.CliError(`${r.key}:
|
|
288
|
+
const ref = kind === 'prompt' || kind === 'agent' ? Number(entry.id) : entry.id;
|
|
289
|
+
if (kind !== 'workflow' && (!Number.isInteger(ref) || ref <= 0)) {
|
|
290
|
+
throw new errors_1.CliError(`${r.key}: ${kind} id "${entry.id}" in .metrio/state.json is not a positive integer`);
|
|
250
291
|
}
|
|
251
292
|
return {
|
|
252
293
|
label: r.key,
|
|
@@ -265,11 +306,15 @@ async function runVerifyCommand(argv) {
|
|
|
265
306
|
const o = parseVerifyArgs(argv);
|
|
266
307
|
const targets = resolveTargets(o);
|
|
267
308
|
(0, shared_1.loadEnv)(o.envFile);
|
|
268
|
-
const
|
|
309
|
+
const apiKey = (0, shared_1.resolveApiKey)(o.apiKey);
|
|
310
|
+
// The SDK appends paths as-is, so a trailing slash would give `//v1/...`.
|
|
311
|
+
const baseUrl = ((0, shared_1.resolveBaseUrl)(o.baseUrl) || 'https://api.metrio.ai').replace(/\/+$/, ''); // the SDK's default
|
|
312
|
+
const client = new client_1.MetrioAI({ apiKey, baseUrl });
|
|
269
313
|
const results = await (0, execute_1.runAll)(client, targets, {
|
|
270
314
|
concurrency: o.concurrency,
|
|
271
315
|
failFast: o.failFast,
|
|
272
316
|
timezone: o.timezone,
|
|
317
|
+
apiKey,
|
|
273
318
|
onResult: (r, done, total) => process.stderr.write((0, sync_1.safe)(`[${done}/${total}] ${r.resource} › ${r.case} ${r.status}\n`)),
|
|
274
319
|
});
|
|
275
320
|
process.stdout.write(o.json ? (0, report_1.formatJson)(results) : (0, report_1.formatHuman)(results));
|
package/dist/index.js
CHANGED
|
@@ -40,10 +40,10 @@ Account (console OAuth):
|
|
|
40
40
|
whoami Show the signed-in user and scopes
|
|
41
41
|
|
|
42
42
|
Manage (console OAuth; run "metrio <group> --help"):
|
|
43
|
-
project | prompt | version | skill | workflow | schedule | mcp | run | admin | model-tiers
|
|
43
|
+
project | prompt | version | skill | workflow | agent | schedule | mcp | run | admin | model-tiers
|
|
44
44
|
e.g. metrio version create 12 -f system.md
|
|
45
45
|
metrio skill get 5 -o skill.md
|
|
46
|
-
pull | diff | push Sync prompts, skills and
|
|
46
|
+
pull | diff | push Sync prompts, skills, workflows and agents with local files ("metrio pull --help")
|
|
47
47
|
tools List the console tools your login can call
|
|
48
48
|
call <tool> Call any console tool with JSON args (-f file | --args json)
|
|
49
49
|
|
package/dist/manage/registry.js
CHANGED
|
@@ -42,6 +42,8 @@ const WF_VERSION = P('version', '<version>');
|
|
|
42
42
|
const SCHEDULE = P('schedule_id', '<schedule-id>');
|
|
43
43
|
const INSTANCE = P('mcp_instance_id', '<instance-id>');
|
|
44
44
|
const TASK = P('task_id', '<task-id>');
|
|
45
|
+
const AGENT = P('agent_id', '<agent>');
|
|
46
|
+
const RUN = P('run_id', '<run>');
|
|
45
47
|
exports.COMMANDS = [
|
|
46
48
|
{ path: ['project', 'list'], tool: 'list_projects', summary: 'List your projects' },
|
|
47
49
|
{ path: ['project', 'create'], tool: 'create_project', summary: 'Create a project (-f or --team-id/--name)' },
|
|
@@ -78,6 +80,17 @@ exports.COMMANDS = [
|
|
|
78
80
|
{ path: ['workflow', 'version', 'diff'], tool: 'diff_workflow_version', summary: 'Diff two workflow versions', args: [WF, P('from_version', '<from>'), P('to_version', '<to>')] },
|
|
79
81
|
{ path: ['workflow', 'version', 'publish'], tool: 'publish_workflow_version', summary: 'Publish a draft (--activate to go live)', args: [WF, WF_VERSION] },
|
|
80
82
|
{ path: ['workflow', 'version', 'activate'], tool: 'set_active_workflow_version', summary: 'Make a published version live (rollback too)', args: [WF, WF_VERSION] },
|
|
83
|
+
{ path: ['agent', 'list'], tool: 'list_agents', summary: 'List Metrio agents' },
|
|
84
|
+
{ path: ['agent', 'get'], tool: 'get_agent', summary: 'Get an agent with its sub-agents and tool policies', args: [AGENT] },
|
|
85
|
+
{ path: ['agent', 'create'], tool: 'create_agent', summary: 'Create an agent (-f agent.json, console field names)' },
|
|
86
|
+
{ path: ['agent', 'update'], tool: 'update_agent', summary: 'Update an agent (-f agent.json, console field names)', args: [AGENT] },
|
|
87
|
+
{ path: ['agent', 'delete'], tool: 'delete_agent', summary: 'Delete an agent (blocked while it is a sub-agent)', args: [AGENT], destructive: true },
|
|
88
|
+
{ path: ['agent', 'tools'], tool: 'set_agent_tools', summary: 'Replace sub-agents and tool policies (-f tools.yaml)', args: [AGENT] },
|
|
89
|
+
{ path: ['agent', 'bump-pins'], tool: 'bump_agent_pins', summary: 'Repoint sub-agent version pins (-f pins.yaml)', wrap: 'pins' },
|
|
90
|
+
{ path: ['agent', 'run', 'list'], tool: 'list_agent_runs', summary: 'List runs of an agent, newest first', args: [AGENT] },
|
|
91
|
+
{ path: ['agent', 'run', 'get'], tool: 'get_agent_run', summary: 'Get a run with children and pending approvals', args: [AGENT, RUN] },
|
|
92
|
+
{ path: ['agent', 'run', 'approve'], tool: 'approve_agent_run', summary: 'Approve or reject gated tool calls (-f decisions.yaml)', args: [AGENT, RUN], wrap: 'decisions' },
|
|
93
|
+
{ path: ['agent', 'run', 'cancel'], tool: 'cancel_agent_run', summary: 'Cancel an unfinished run', args: [AGENT, RUN], destructive: true },
|
|
81
94
|
{ path: ['schedule', 'list'], tool: 'list_schedules', summary: 'List schedules' },
|
|
82
95
|
{ path: ['schedule', 'create'], tool: 'create_schedule', summary: 'Create a schedule (-f schedule.yaml)' },
|
|
83
96
|
{ path: ['schedule', 'update'], tool: 'update_schedule', summary: 'Update a schedule (-f schedule.yaml)', args: [SCHEDULE] },
|
package/dist/manage/run.js
CHANGED
|
@@ -73,21 +73,28 @@ function isCommandPrefix(argv) {
|
|
|
73
73
|
const words = argv.filter((a) => !a.startsWith('-'));
|
|
74
74
|
return registry_1.COMMANDS.some((c) => words.length < c.path.length && words.every((w, i) => c.path[i] === w));
|
|
75
75
|
}
|
|
76
|
+
/** Positionals that may name a tracked directory instead of the numeric id. */
|
|
77
|
+
const DIR_REFS = {
|
|
78
|
+
prompt_external_id: { kind: 'prompt', noun: 'prompt' },
|
|
79
|
+
agent_id: { kind: 'agent', noun: 'agent' },
|
|
80
|
+
};
|
|
76
81
|
/**
|
|
77
|
-
* A prompt positional may name a tracked directory (prompts/<dir> or <dir>) instead of
|
|
78
|
-
* numeric id — only for the workspace's own project, since ids are per project.
|
|
82
|
+
* A prompt or agent positional may name a tracked directory (prompts/<dir> or <dir>) instead of
|
|
83
|
+
* the numeric id — only for the workspace's own project, since ids are per project.
|
|
79
84
|
*/
|
|
80
|
-
function
|
|
85
|
+
function dirRef(raw, project, prop) {
|
|
81
86
|
if (/^\d+$/.test(raw))
|
|
82
87
|
return raw;
|
|
88
|
+
const { kind, noun } = DIR_REFS[prop];
|
|
89
|
+
const prefix = workspace_1.KIND_DIR[kind];
|
|
83
90
|
const root = (0, workspace_1.findRoot)();
|
|
84
91
|
const dir = raw.replace(/\/+$/, '');
|
|
85
|
-
const key = dir.startsWith(
|
|
92
|
+
const key = dir.startsWith(`${prefix}/`) ? dir : `${prefix}/${dir}`;
|
|
86
93
|
// A broken workspace surfaces its own (informative) CliError.
|
|
87
94
|
const workspaceProject = root ? (0, workspace_1.readProject)(root) : undefined;
|
|
88
95
|
const entry = root ? (0, workspace_1.readState)(root, workspaceProject).resources[key] : undefined;
|
|
89
|
-
if (!entry || entry.kind !==
|
|
90
|
-
throw new errors_1.CliError(`Unknown
|
|
96
|
+
if (!entry || entry.kind !== kind) {
|
|
97
|
+
throw new errors_1.CliError(`Unknown ${noun} "${raw}": use the numeric id, or a ${prefix}/<dir> tracked in .metrio/state.json`);
|
|
91
98
|
}
|
|
92
99
|
if (project !== undefined && project !== workspaceProject) {
|
|
93
100
|
throw new errors_1.CliError(`"${raw}" is a directory of project ${workspaceProject}, but this command targets project ${String(project)}`);
|
|
@@ -144,11 +151,11 @@ async function runManageCommand(argv, readStdin) {
|
|
|
144
151
|
delete args.tier_key;
|
|
145
152
|
delete sources.tier_key;
|
|
146
153
|
}
|
|
147
|
-
// A
|
|
148
|
-
|
|
154
|
+
// A directory name resolves against the final project_slug, so it waits for it.
|
|
155
|
+
const dirRefs = [];
|
|
149
156
|
expected.forEach((arg, i) => {
|
|
150
|
-
if (arg.prop
|
|
151
|
-
|
|
157
|
+
if (arg.prop in DIR_REFS && !/^\d+$/.test(positionals[i]))
|
|
158
|
+
dirRefs.push({ prop: arg.prop, raw: positionals[i], label: arg.label });
|
|
152
159
|
else
|
|
153
160
|
put(arg.prop, (0, schema_1.coercePositional)(positionals[i], props[arg.prop], arg.label), arg.label);
|
|
154
161
|
});
|
|
@@ -163,8 +170,8 @@ async function runManageCommand(argv, readStdin) {
|
|
|
163
170
|
throw err;
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
|
-
|
|
167
|
-
put(
|
|
173
|
+
for (const ref of dirRefs) {
|
|
174
|
+
put(ref.prop, (0, schema_1.coercePositional)(dirRef(ref.raw, args.project_slug, ref.prop), props[ref.prop], ref.label), ref.label);
|
|
168
175
|
}
|
|
169
176
|
if ((schema.required ?? []).includes('project_slug') && args.project_slug === undefined) {
|
|
170
177
|
throw new errors_1.CliError('No project. Pass --project <slug>, set METRIO_PROJECT, or add "project: <slug>" to metrio.yaml.');
|