@metrio-ai/cli 1.0.1 → 1.2.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 +36 -5
- package/dist/commands/sync.js +488 -0
- package/dist/console/client.js +21 -3
- package/dist/index.js +36 -4
- package/dist/manage/input.js +137 -0
- package/dist/manage/output.js +127 -0
- package/dist/manage/project.js +74 -0
- package/dist/manage/registry.js +119 -0
- package/dist/manage/run.js +185 -0
- package/dist/manage/schema.js +152 -0
- package/dist/sync/push.js +329 -0
- package/dist/sync/remote.js +79 -0
- package/dist/sync/render.js +181 -0
- package/dist/sync/status.js +85 -0
- package/dist/sync/workspace.js +419 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -25,14 +25,45 @@ 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
|
-
|
|
29
|
-
|
|
30
|
-
metrio
|
|
31
|
-
metrio
|
|
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
|
|
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 …`.
|
|
51
|
+
|
|
52
|
+
## Sync a project as files
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
mkdir my-project && cd my-project
|
|
56
|
+
metrio pull --project my-project # metrio.yaml, prompts/, skills/, workflows/, .metrio/state.json
|
|
57
|
+
$EDITOR prompts/pick-products/system.md
|
|
58
|
+
metrio diff # modified / remote-changed / conflict / new … with unified diffs
|
|
59
|
+
metrio push -m "tighter wording" # new prompt version (not live)
|
|
60
|
+
metrio push --activate # …or make it live right away
|
|
61
|
+
metrio version list pick-products # prompt directories work wherever a prompt id is expected
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`push` creates new versions and never overwrites changes made in the console since your last `pull` (use `--force` deliberately: local wins, bindings and MCP lists are replaced, and resources changed in the console may be pruned). Workflow pushes create draft versions (`--publish`, `--activate`). Deleting files deletes nothing remotely unless you pass `--prune --yes`. Commit `.metrio/state.json` with the files. Run `metrio pull --help` for every option.
|
|
65
|
+
|
|
66
|
+
- A second plain `metrio push` does not create duplicates ("already pushed as version N"); `metrio push --activate` (prompts) or `--publish` / `--activate` (workflows) makes the pending pushed version live.
|
|
67
|
+
- `pull` adopts untracked local files that are identical to the console. `pull` and `push` report per-resource failures, exit 1, and still save state for the resources that succeeded.
|
|
68
|
+
- 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.
|
|
69
|
+
- 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.
|
|
@@ -0,0 +1,488 @@
|
|
|
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.SYNC_HELP = void 0;
|
|
37
|
+
exports.parseSyncArgs = parseSyncArgs;
|
|
38
|
+
exports.openWorkspace = openWorkspace;
|
|
39
|
+
exports.filterEntries = filterEntries;
|
|
40
|
+
exports.loadEntries = loadEntries;
|
|
41
|
+
exports.runDiffCommand = runDiffCommand;
|
|
42
|
+
exports.runPullCommand = runPullCommand;
|
|
43
|
+
exports.runPushCommand = runPushCommand;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const diff_1 = require("diff");
|
|
46
|
+
const client_1 = require("../console/client");
|
|
47
|
+
const errors_1 = require("../errors");
|
|
48
|
+
const push_1 = require("../sync/push");
|
|
49
|
+
const remote_1 = require("../sync/remote");
|
|
50
|
+
const status_1 = require("../sync/status");
|
|
51
|
+
const workspace_1 = require("../sync/workspace");
|
|
52
|
+
const ONLY = { prompts: 'prompt', skills: 'skill', workflows: 'workflow' };
|
|
53
|
+
function parseSyncArgs(argv) {
|
|
54
|
+
const o = {
|
|
55
|
+
paths: [], kinds: ['prompt', 'skill', 'workflow'], force: false, activate: false, publish: false,
|
|
56
|
+
prune: false, yes: false, dryRun: false, json: false,
|
|
57
|
+
};
|
|
58
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
59
|
+
const arg = argv[i];
|
|
60
|
+
const value = () => {
|
|
61
|
+
const v = argv[i + 1];
|
|
62
|
+
if (v === undefined)
|
|
63
|
+
throw new errors_1.CliError(`${arg} needs a value`);
|
|
64
|
+
i += 1;
|
|
65
|
+
return v;
|
|
66
|
+
};
|
|
67
|
+
if (arg === '--only') {
|
|
68
|
+
const kind = ONLY[value()];
|
|
69
|
+
if (!kind)
|
|
70
|
+
throw new errors_1.CliError('--only must be prompts, skills or workflows');
|
|
71
|
+
o.kinds = [kind];
|
|
72
|
+
}
|
|
73
|
+
else if (arg === '-m' || arg === '--message')
|
|
74
|
+
o.message = value();
|
|
75
|
+
else if (arg === '--project')
|
|
76
|
+
o.project = value();
|
|
77
|
+
else if (arg === '--force')
|
|
78
|
+
o.force = true;
|
|
79
|
+
else if (arg === '--activate')
|
|
80
|
+
o.activate = true;
|
|
81
|
+
else if (arg === '--publish')
|
|
82
|
+
o.publish = true;
|
|
83
|
+
else if (arg === '--prune')
|
|
84
|
+
o.prune = true;
|
|
85
|
+
else if (arg === '--yes')
|
|
86
|
+
o.yes = true;
|
|
87
|
+
else if (arg === '--dry-run')
|
|
88
|
+
o.dryRun = true;
|
|
89
|
+
else if (arg === '--json')
|
|
90
|
+
o.json = true;
|
|
91
|
+
else if (arg.startsWith('-'))
|
|
92
|
+
throw new errors_1.CliError(`Unknown option ${arg}`);
|
|
93
|
+
else
|
|
94
|
+
o.paths.push(arg);
|
|
95
|
+
}
|
|
96
|
+
return o;
|
|
97
|
+
}
|
|
98
|
+
async function openWorkspace() {
|
|
99
|
+
const root = (0, workspace_1.findRoot)();
|
|
100
|
+
if (!root)
|
|
101
|
+
throw new errors_1.CliError('Not in a metrio workspace (no metrio.yaml). Run "metrio pull --project <slug>" first.');
|
|
102
|
+
return { root, project: (0, workspace_1.readProject)(root), client: new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)()) };
|
|
103
|
+
}
|
|
104
|
+
/** Entries whose key equals or sits under one of the given paths (relative to the workspace root). */
|
|
105
|
+
function filterEntries(entries, root, paths) {
|
|
106
|
+
if (!paths.length)
|
|
107
|
+
return entries;
|
|
108
|
+
const wanted = paths.map((p) => {
|
|
109
|
+
const rel = path.relative(root, path.resolve(p));
|
|
110
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
111
|
+
throw new errors_1.CliError(`${p} is outside the metrio workspace`);
|
|
112
|
+
}
|
|
113
|
+
return rel.split(path.sep).join('/').replace(/\/+$/, '');
|
|
114
|
+
});
|
|
115
|
+
const matches = (e, w) => w === '' || e.key === w || e.key.startsWith(`${w}/`) || w.startsWith(`${e.key}/`);
|
|
116
|
+
wanted.forEach((w, i) => {
|
|
117
|
+
if (!entries.some((e) => matches(e, w)))
|
|
118
|
+
throw new errors_1.CliError(`${paths[i]} matches nothing in this workspace`);
|
|
119
|
+
});
|
|
120
|
+
return entries.filter((e) => wanted.some((w) => matches(e, w)));
|
|
121
|
+
}
|
|
122
|
+
async function loadEntries(ctx, opts) {
|
|
123
|
+
const state = (0, workspace_1.readState)(ctx.root, ctx.project);
|
|
124
|
+
const remote = await (0, remote_1.fetchRemote)(ctx.client, ctx.project, opts.kinds);
|
|
125
|
+
return filterEntries((0, status_1.computeEntries)(ctx.root, state, remote, opts.kinds), ctx.root, opts.paths);
|
|
126
|
+
}
|
|
127
|
+
/** Console-sourced text must not carry terminal control sequences. */
|
|
128
|
+
function safe(text) {
|
|
129
|
+
return text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
130
|
+
}
|
|
131
|
+
exports.SYNC_HELP = `metrio pull | diff | push - sync prompts, skills and workflows with local files
|
|
132
|
+
|
|
133
|
+
metrio pull --project <slug> First pull: creates metrio.yaml here and writes:
|
|
134
|
+
prompts/<name>/prompt.yaml + system.md
|
|
135
|
+
skills/<name>.md
|
|
136
|
+
workflows/<id>/workflow.yaml + definition.json
|
|
137
|
+
metrio pull [paths…] Update files from the console (keeps local edits unless --force)
|
|
138
|
+
metrio diff [paths…] Show what differs: modified, remote-changed, conflict, new, …
|
|
139
|
+
metrio push [paths…] Send local edits: new prompt version (not live unless --activate),
|
|
140
|
+
skill update, workflow version (draft; --publish / --activate)
|
|
141
|
+
|
|
142
|
+
Options:
|
|
143
|
+
--only prompts|skills|workflows Limit to one kind
|
|
144
|
+
--force pull: overwrite local edits; push: push over console changes
|
|
145
|
+
(local wins — bindings and MCP list are replaced), allow
|
|
146
|
+
pruning resources changed in the console, and allow a prompt
|
|
147
|
+
version that drops the live messages/agent_description
|
|
148
|
+
-m, --message <text> push: description of the new versions
|
|
149
|
+
--activate push: make new prompt versions head / workflow versions live
|
|
150
|
+
--publish push: publish new workflow versions without activating
|
|
151
|
+
--prune --yes push: delete console resources whose local files were removed
|
|
152
|
+
--dry-run Print the plan without changing anything
|
|
153
|
+
--json diff: machine-readable statuses
|
|
154
|
+
|
|
155
|
+
State lives in .metrio/state.json (commit it with the files).
|
|
156
|
+
`;
|
|
157
|
+
const wantsHelp = (argv) => argv.includes('-h') || argv.includes('--help');
|
|
158
|
+
function patch(e) {
|
|
159
|
+
const local = e.local ?? {};
|
|
160
|
+
const remote = e.remote?.files ?? {};
|
|
161
|
+
return [...new Set([...Object.keys(local), ...Object.keys(remote)])]
|
|
162
|
+
.sort()
|
|
163
|
+
.filter((f) => local[f] !== remote[f])
|
|
164
|
+
.map((f) => (0, diff_1.createTwoFilesPatch)(`remote/${e.key}/${f}`, `local/${e.key}/${f}`, remote[f] ?? '', local[f] ?? ''))
|
|
165
|
+
.join('');
|
|
166
|
+
}
|
|
167
|
+
async function runDiffCommand(argv) {
|
|
168
|
+
if (wantsHelp(argv)) {
|
|
169
|
+
process.stdout.write(exports.SYNC_HELP);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const opts = parseSyncArgs(argv);
|
|
173
|
+
const ctx = await openWorkspace();
|
|
174
|
+
const entries = await loadEntries(ctx, opts);
|
|
175
|
+
if (opts.json) {
|
|
176
|
+
const rows = entries.map((e) => ({ key: e.key, kind: e.kind, status: e.status, ...(e.from ? { from: e.from } : {}) }));
|
|
177
|
+
process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let unchanged = 0;
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
if (e.status === 'unchanged') {
|
|
183
|
+
unchanged += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
process.stdout.write(safe(`${e.status.padEnd(15)}${e.key}${e.from ? ` (from ${e.from})` : ''}\n`));
|
|
187
|
+
if (e.status === 'modified' || e.status === 'conflict' || e.status === 'remote-changed')
|
|
188
|
+
process.stdout.write(safe(patch(e)));
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write(`${unchanged} unchanged\n`);
|
|
191
|
+
}
|
|
192
|
+
async function runPullCommand(argv) {
|
|
193
|
+
if (wantsHelp(argv)) {
|
|
194
|
+
process.stdout.write(exports.SYNC_HELP);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const opts = parseSyncArgs(argv);
|
|
198
|
+
const found = (0, workspace_1.findRoot)();
|
|
199
|
+
let root = found;
|
|
200
|
+
if (!root) {
|
|
201
|
+
if (!opts.project)
|
|
202
|
+
throw new errors_1.CliError('Not in a metrio workspace. Run "metrio pull --project <slug>" to start one here.');
|
|
203
|
+
root = process.cwd(); // metrio.yaml is only created once the console fetch succeeded
|
|
204
|
+
}
|
|
205
|
+
else if (opts.project && opts.project !== (0, workspace_1.readProject)(root)) {
|
|
206
|
+
(0, workspace_1.initRoot)(root, opts.project); // throws: metrio.yaml names another project
|
|
207
|
+
}
|
|
208
|
+
const ctx = { root, project: found ? (0, workspace_1.readProject)(root) : opts.project, client: new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)()) };
|
|
209
|
+
// A fresh workspace is locked once metrio.yaml exists (below).
|
|
210
|
+
let release = found && !opts.dryRun ? (0, workspace_1.acquireLock)(root) : undefined;
|
|
211
|
+
try {
|
|
212
|
+
const state = (0, workspace_1.readState)(ctx.root, ctx.project);
|
|
213
|
+
const entries = await loadEntries(ctx, opts);
|
|
214
|
+
if (!found && !opts.dryRun) {
|
|
215
|
+
(0, workspace_1.initRoot)(root, ctx.project);
|
|
216
|
+
release = (0, workspace_1.acquireLock)(root);
|
|
217
|
+
}
|
|
218
|
+
let unchanged = 0;
|
|
219
|
+
let failed = 0;
|
|
220
|
+
const record = (e, files) => {
|
|
221
|
+
const h = (0, workspace_1.hashFiles)(files);
|
|
222
|
+
state.resources[e.key] = { ...(state.resources[e.key] ?? {}), kind: e.kind, id: e.remote.id, base: h, remote: h };
|
|
223
|
+
};
|
|
224
|
+
const take = (e, verb) => {
|
|
225
|
+
if (!opts.dryRun) {
|
|
226
|
+
(0, workspace_1.writeLocal)(ctx.root, e.key, e.kind, e.remote.files);
|
|
227
|
+
record(e, e.remote.files);
|
|
228
|
+
}
|
|
229
|
+
process.stdout.write(safe(`${opts.dryRun ? 'would pull' : verb} ${e.key}\n`));
|
|
230
|
+
};
|
|
231
|
+
for (const e of entries) {
|
|
232
|
+
try {
|
|
233
|
+
switch (e.status) {
|
|
234
|
+
case 'unchanged':
|
|
235
|
+
if (e.refresh && !opts.dryRun)
|
|
236
|
+
record(e, e.remote.files);
|
|
237
|
+
if (e.refresh && !e.state)
|
|
238
|
+
process.stdout.write(safe(`${opts.dryRun ? 'would track' : 'tracked'} ${e.key}\n`));
|
|
239
|
+
else
|
|
240
|
+
unchanged += 1;
|
|
241
|
+
break;
|
|
242
|
+
case 'remote-new':
|
|
243
|
+
case 'remote-changed':
|
|
244
|
+
take(e, 'pulled');
|
|
245
|
+
break;
|
|
246
|
+
case 'deleted':
|
|
247
|
+
take(e, 'restored');
|
|
248
|
+
break;
|
|
249
|
+
case 'modified':
|
|
250
|
+
if (opts.force)
|
|
251
|
+
take(e, 'pulled');
|
|
252
|
+
else
|
|
253
|
+
process.stderr.write(safe(`skipped ${e.key}: local changes (use --force to overwrite)\n`));
|
|
254
|
+
break;
|
|
255
|
+
case 'conflict':
|
|
256
|
+
if (!e.state && e.kind === 'prompt' && e.remote.data.version === null) {
|
|
257
|
+
// A console prompt without a version has nothing to pull: track it as if its empty render
|
|
258
|
+
// had been pulled, so the local files show as edits and push creates the first version.
|
|
259
|
+
if (!opts.dryRun) {
|
|
260
|
+
const h = (0, workspace_1.hashFiles)(e.remote.files);
|
|
261
|
+
state.resources[e.key] = { kind: e.kind, id: e.remote.id, base: h, remote: h };
|
|
262
|
+
}
|
|
263
|
+
process.stdout.write(safe(`${opts.dryRun ? 'would track' : 'tracked'} ${e.key} (the console prompt has no version yet; push to create one)\n`));
|
|
264
|
+
}
|
|
265
|
+
else if (opts.force)
|
|
266
|
+
take(e, 'pulled');
|
|
267
|
+
else
|
|
268
|
+
process.stderr.write(safe(`skipped ${e.key}: local changes (use --force to overwrite)\n`));
|
|
269
|
+
break;
|
|
270
|
+
case 'renamed': {
|
|
271
|
+
const old = state.resources[e.from];
|
|
272
|
+
if (!opts.dryRun) {
|
|
273
|
+
state.resources[e.key] = old;
|
|
274
|
+
delete state.resources[e.from];
|
|
275
|
+
}
|
|
276
|
+
process.stdout.write(safe(`${opts.dryRun ? 'would rename' : 'renamed'} ${e.from} -> ${e.key}\n`));
|
|
277
|
+
if ((0, workspace_1.hashFiles)(e.remote.files) !== old.remote)
|
|
278
|
+
take(e, 'pulled');
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case 'remote-deleted':
|
|
282
|
+
if (!opts.dryRun)
|
|
283
|
+
delete state.resources[e.key];
|
|
284
|
+
process.stderr.write(safe(`${e.key}: deleted in the console; local files kept (push to recreate, or delete them)\n`));
|
|
285
|
+
break;
|
|
286
|
+
case 'new':
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
failed += 1;
|
|
292
|
+
process.stderr.write(safe(`failed ${e.key}: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!opts.dryRun)
|
|
296
|
+
(0, workspace_1.writeState)(ctx.root, state);
|
|
297
|
+
process.stdout.write(`${unchanged} unchanged\n`);
|
|
298
|
+
if (failed > 0)
|
|
299
|
+
throw new errors_1.CliError(`${failed} resources failed`, 1);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
release?.();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function remoteContent(r) {
|
|
306
|
+
try {
|
|
307
|
+
return (0, push_1.versionContent)(r.kind, r.files, r.kind);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const remoteMoved = (e) => (0, workspace_1.hashFiles)(e.remote.files) !== e.state.remote;
|
|
314
|
+
function planPush(entries, opts, options) {
|
|
315
|
+
const actions = [];
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
switch (entry.status) {
|
|
318
|
+
case 'unchanged':
|
|
319
|
+
if ((0, push_1.hasPendingVersion)(entry, options))
|
|
320
|
+
actions.push({ entry, op: 'update' });
|
|
321
|
+
break;
|
|
322
|
+
case 'modified':
|
|
323
|
+
actions.push({ entry, op: 'update' });
|
|
324
|
+
break;
|
|
325
|
+
case 'new':
|
|
326
|
+
actions.push({ entry, op: 'create' });
|
|
327
|
+
break;
|
|
328
|
+
case 'renamed':
|
|
329
|
+
if (entry.kind === 'workflow') {
|
|
330
|
+
actions.push({ entry, skip: 'workflow directories are named by workflow_id; rename it in the console instead', failure: true });
|
|
331
|
+
}
|
|
332
|
+
else if (remoteMoved(entry))
|
|
333
|
+
actions.push({ entry, skip: 'remote changed; run "metrio pull" first', failure: true });
|
|
334
|
+
else
|
|
335
|
+
actions.push({ entry, op: 'rename' });
|
|
336
|
+
break;
|
|
337
|
+
case 'conflict':
|
|
338
|
+
if (!entry.state) {
|
|
339
|
+
const reason = `exists in the console but is not tracked here. "metrio pull --force ${entry.key}" would OVERWRITE your local files; ` +
|
|
340
|
+
'to keep them, rename the local directory (it will be pushed as new) or delete the console copy first';
|
|
341
|
+
actions.push({ entry, skip: reason, failure: true });
|
|
342
|
+
}
|
|
343
|
+
else if (opts.force)
|
|
344
|
+
actions.push({ entry, op: 'update' });
|
|
345
|
+
else
|
|
346
|
+
actions.push({ entry, skip: 'conflict: changed locally and in the console (pull, merge, then push; or --force)', failure: true });
|
|
347
|
+
break;
|
|
348
|
+
case 'remote-changed':
|
|
349
|
+
actions.push({ entry, skip: 'remote changed; run "metrio pull" first', failure: true });
|
|
350
|
+
break;
|
|
351
|
+
case 'remote-deleted':
|
|
352
|
+
actions.push({ entry, skip: 'deleted in the console; run "metrio pull" to drop it, then push to recreate', failure: true });
|
|
353
|
+
break;
|
|
354
|
+
case 'deleted':
|
|
355
|
+
if (opts.prune && opts.yes) {
|
|
356
|
+
if (remoteMoved(entry) && !opts.force) {
|
|
357
|
+
actions.push({ entry, skip: 'changed in the console since your last pull; use --force to delete anyway', failure: true });
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
actions.push({ entry, op: 'delete' });
|
|
361
|
+
}
|
|
362
|
+
else if (opts.prune)
|
|
363
|
+
actions.push({ entry, skip: 'use --prune --yes to delete it in the console', failure: true });
|
|
364
|
+
else
|
|
365
|
+
actions.push({ entry, skip: 'deleted locally (use --prune --yes to delete it in the console)', failure: false });
|
|
366
|
+
break;
|
|
367
|
+
default:
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return sortForExecution(actions);
|
|
372
|
+
}
|
|
373
|
+
const KIND_RANK = { skill: 0, prompt: 1, workflow: 2 };
|
|
374
|
+
const DELETE_RANK = { prompt: 3, workflow: 3, skill: 4 };
|
|
375
|
+
/** Skills first (prompts may name new ones), then prompts, workflows; deletes last, skills after what uses them. */
|
|
376
|
+
function sortForExecution(actions) {
|
|
377
|
+
const rank = (a) => ('op' in a && a.op === 'delete' ? DELETE_RANK : KIND_RANK)[a.entry.kind];
|
|
378
|
+
return [...actions].sort((a, b) => rank(a) - rank(b));
|
|
379
|
+
}
|
|
380
|
+
const PAST = { create: 'created', update: 'updated', delete: 'deleted', rename: 'renamed' };
|
|
381
|
+
const describeOp = (op, e) => (op === 'rename' ? `${e.from} -> ${e.key}` : e.key);
|
|
382
|
+
async function runPushCommand(argv) {
|
|
383
|
+
if (wantsHelp(argv)) {
|
|
384
|
+
process.stdout.write(exports.SYNC_HELP);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const opts = parseSyncArgs(argv);
|
|
388
|
+
const ctx = await openWorkspace();
|
|
389
|
+
const release = opts.dryRun ? undefined : (0, workspace_1.acquireLock)(ctx.root);
|
|
390
|
+
try {
|
|
391
|
+
const state = (0, workspace_1.readState)(ctx.root, ctx.project);
|
|
392
|
+
const options = { activate: opts.activate, publish: opts.publish, message: opts.message, force: opts.force };
|
|
393
|
+
const entries = await loadEntries(ctx, opts);
|
|
394
|
+
const actions = planPush(entries, opts, options);
|
|
395
|
+
if (opts.dryRun) {
|
|
396
|
+
for (const a of actions) {
|
|
397
|
+
process.stdout.write(safe('op' in a ? `would ${a.op} ${describeOp(a.op, a.entry)}\n` : `skip ${a.entry.key}: ${a.skip}\n`));
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const pushCtx = { client: ctx.client, project: ctx.project, options };
|
|
402
|
+
const done = [];
|
|
403
|
+
let failed = 0;
|
|
404
|
+
for (const a of actions) {
|
|
405
|
+
if (!('op' in a)) {
|
|
406
|
+
if (a.failure) {
|
|
407
|
+
failed += 1;
|
|
408
|
+
process.stderr.write(safe(`skip ${a.entry.key}: ${a.skip}\n`));
|
|
409
|
+
}
|
|
410
|
+
else
|
|
411
|
+
process.stderr.write(safe(`${a.entry.key}: ${a.skip}\n`));
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
let result;
|
|
416
|
+
if (a.op === 'delete')
|
|
417
|
+
await (0, push_1.deleteEntry)(pushCtx, a.entry);
|
|
418
|
+
else if (a.op !== 'rename')
|
|
419
|
+
result = await (0, push_1.pushEntry)(pushCtx, a.entry);
|
|
420
|
+
done.push({ entry: a.entry, op: a.op, result });
|
|
421
|
+
process.stdout.write(safe(`${PAST[a.op]} ${describeOp(a.op, a.entry)}${result ? ` (${result.summary})` : ''}\n`));
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
failed += 1;
|
|
425
|
+
process.stderr.write(safe(`failed ${a.entry.key}: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Converged resources (same content on both sides, stale state): record them as synced.
|
|
429
|
+
const converged = entries.filter((e) => e.refresh && !done.some((d) => d.entry === e));
|
|
430
|
+
for (const e of converged) {
|
|
431
|
+
const h = (0, workspace_1.hashFiles)(e.local);
|
|
432
|
+
state.resources[e.key] = { ...(state.resources[e.key] ?? {}), kind: e.kind, id: e.remote.id, base: h, remote: h };
|
|
433
|
+
}
|
|
434
|
+
if (converged.length && !done.length)
|
|
435
|
+
(0, workspace_1.writeState)(ctx.root, state);
|
|
436
|
+
let refetchFailed = false;
|
|
437
|
+
if (done.length) {
|
|
438
|
+
const pushed = done.filter((d) => d.result);
|
|
439
|
+
let refreshed;
|
|
440
|
+
if (pushed.length) {
|
|
441
|
+
try {
|
|
442
|
+
refreshed = await (0, remote_1.fetchRemote)(ctx.client, ctx.project, [...new Set(pushed.map((d) => d.entry.kind))]);
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
refetchFailed = true;
|
|
446
|
+
process.stderr.write(safe(`could not re-read the console after pushing: ${error instanceof Error ? error.message : String(error)}; run "metrio diff" to check\n`));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
for (const d of done) {
|
|
450
|
+
const key = d.entry.key;
|
|
451
|
+
if (d.op === 'delete') {
|
|
452
|
+
delete state.resources[key];
|
|
453
|
+
}
|
|
454
|
+
else if (d.op === 'rename') {
|
|
455
|
+
state.resources[key] = state.resources[d.entry.from];
|
|
456
|
+
delete state.resources[d.entry.from];
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const r = d.result;
|
|
460
|
+
const previous = state.resources[key];
|
|
461
|
+
const remote = refreshed?.find((x) => x.kind === d.entry.kind && x.id === r.id);
|
|
462
|
+
state.resources[key] = {
|
|
463
|
+
...(previous ?? {}),
|
|
464
|
+
kind: d.entry.kind,
|
|
465
|
+
id: r.id,
|
|
466
|
+
base: (0, workspace_1.hashFiles)(d.entry.local),
|
|
467
|
+
// Without a re-read, keep the last known remote hash: the next diff then shows what really changed.
|
|
468
|
+
remote: refreshed ? (remote ? (0, workspace_1.hashFiles)(remote.files) : '') : (previous?.remote ?? ''),
|
|
469
|
+
...(r.pushed_version_id !== undefined ? { pushed_version_id: r.pushed_version_id, pushed_serial: r.pushed_serial } : {}),
|
|
470
|
+
...(r.pushed_workflow_version !== undefined ? { pushed_workflow_version: r.pushed_workflow_version } : {}),
|
|
471
|
+
...(r.pushed_content !== undefined ? { pushed_content: r.pushed_content } : {}),
|
|
472
|
+
// What the console made of a new/activated version (defaults filled, prompt_refs pinned): see push.ts.
|
|
473
|
+
...(refreshed && r.versioned ? { remote_content: remote ? remoteContent(remote) : undefined } : {}),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
(0, workspace_1.writeState)(ctx.root, state);
|
|
478
|
+
}
|
|
479
|
+
process.stdout.write(`${done.length} pushed, ${failed} failed\n`);
|
|
480
|
+
if (failed)
|
|
481
|
+
throw new errors_1.CliError(`${failed} of ${done.length + failed} resources failed`, 1);
|
|
482
|
+
if (refetchFailed)
|
|
483
|
+
throw new errors_1.CliError('pushed, but could not re-read the console; run "metrio diff" to check', 1);
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
release?.();
|
|
487
|
+
}
|
|
488
|
+
}
|
package/dist/console/client.js
CHANGED
|
@@ -63,14 +63,32 @@ async function resolveConsoleAuth() {
|
|
|
63
63
|
}
|
|
64
64
|
class ConsoleClient {
|
|
65
65
|
auth;
|
|
66
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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,9 @@ 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 sync_1 = require("./commands/sync");
|
|
23
|
+
const registry_1 = require("./manage/registry");
|
|
21
24
|
exports.HELP = `metrio - MetrioAI CLI
|
|
22
25
|
|
|
23
26
|
Usage: metrio <command> [options]
|
|
@@ -34,13 +37,34 @@ Account (console OAuth):
|
|
|
34
37
|
logout Revoke and delete the stored login
|
|
35
38
|
whoami Show the signed-in user and scopes
|
|
36
39
|
|
|
37
|
-
Manage (console OAuth):
|
|
40
|
+
Manage (console OAuth; run "metrio <group> --help"):
|
|
41
|
+
project | prompt | version | skill | workflow | schedule | mcp | run | admin | model-tiers
|
|
42
|
+
e.g. metrio version create 12 -f system.md
|
|
43
|
+
metrio skill get 5 -o skill.md
|
|
44
|
+
pull | diff | push Sync prompts, skills and workflows with local files ("metrio pull --help")
|
|
38
45
|
tools List the console tools your login can call
|
|
39
|
-
call <tool> Call
|
|
46
|
+
call <tool> Call any console tool with JSON args (-f file | --args json)
|
|
40
47
|
|
|
41
48
|
-h, --help Show this help
|
|
42
49
|
-v, --version Show the CLI version
|
|
43
50
|
`;
|
|
51
|
+
/** Actions of `prompt` / `workflow` that belong to the original execution commands. */
|
|
52
|
+
const EXECUTION_ACTIONS = {
|
|
53
|
+
prompt: ['run-batch', 'create-version'],
|
|
54
|
+
workflow: ['run', 'run-batch'],
|
|
55
|
+
};
|
|
56
|
+
async function runMixedGroup(group, argv) {
|
|
57
|
+
const action = argv[1];
|
|
58
|
+
const runExecution = group === 'prompt' ? prompt_1.runPromptCommand : workflow_1.runWorkflowCommand;
|
|
59
|
+
if (action !== undefined && EXECUTION_ACTIONS[group].includes(action))
|
|
60
|
+
return runExecution(argv.slice(1));
|
|
61
|
+
if (action === undefined || action === '-h' || action === '--help') {
|
|
62
|
+
await runExecution(['--help']);
|
|
63
|
+
process.stdout.write(`\n${(0, registry_1.groupHelp)(group)}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return (0, run_1.runManageCommand)(argv);
|
|
67
|
+
}
|
|
44
68
|
async function main(argv) {
|
|
45
69
|
const [command] = argv;
|
|
46
70
|
switch (command) {
|
|
@@ -56,9 +80,15 @@ async function main(argv) {
|
|
|
56
80
|
case 'chat':
|
|
57
81
|
return (0, chat_1.runChatCommand)(argv.slice(1));
|
|
58
82
|
case 'workflow':
|
|
59
|
-
return (
|
|
83
|
+
return runMixedGroup('workflow', argv);
|
|
60
84
|
case 'prompt':
|
|
61
|
-
return (
|
|
85
|
+
return runMixedGroup('prompt', argv);
|
|
86
|
+
case 'pull':
|
|
87
|
+
return (0, sync_1.runPullCommand)(argv.slice(1));
|
|
88
|
+
case 'diff':
|
|
89
|
+
return (0, sync_1.runDiffCommand)(argv.slice(1));
|
|
90
|
+
case 'push':
|
|
91
|
+
return (0, sync_1.runPushCommand)(argv.slice(1));
|
|
62
92
|
case 'login':
|
|
63
93
|
return (0, auth_1.runLoginCommand)(argv.slice(1));
|
|
64
94
|
case 'logout':
|
|
@@ -70,6 +100,8 @@ async function main(argv) {
|
|
|
70
100
|
case 'call':
|
|
71
101
|
return (0, tools_1.runCallCommand)(argv.slice(1));
|
|
72
102
|
default:
|
|
103
|
+
if (registry_1.MANAGE_GROUPS.includes(command))
|
|
104
|
+
return (0, run_1.runManageCommand)(argv);
|
|
73
105
|
if (command.startsWith('-')) {
|
|
74
106
|
process.stderr.write('metrio: running chat without a subcommand is deprecated; use "metrio chat".\n');
|
|
75
107
|
return (0, chat_1.runChatCommand)(argv);
|