@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
|
@@ -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,127 @@
|
|
|
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 listKey = Array.isArray(record.items)
|
|
114
|
+
? 'items'
|
|
115
|
+
: 'pagination' in record
|
|
116
|
+
? Object.keys(record).find((k) => Array.isArray(record[k]))
|
|
117
|
+
: undefined;
|
|
118
|
+
const rows = Array.isArray(data) ? data : listKey ? record[listKey] : undefined;
|
|
119
|
+
if (rows) {
|
|
120
|
+
process.stdout.write(rows.length ? `${renderTable(rows)}\n` : '(none)\n');
|
|
121
|
+
const p = asRecord(record.pagination);
|
|
122
|
+
if (p.page !== undefined)
|
|
123
|
+
process.stderr.write(`page ${p.page}/${p.total_pages} (total ${p.total})\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
process.stdout.write((0, yaml_1.stringify)(data));
|
|
127
|
+
}
|
|
@@ -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,185 @@
|
|
|
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 workspace_1 = require("../sync/workspace");
|
|
11
|
+
const schema_1 = require("./schema");
|
|
12
|
+
function parse(rest) {
|
|
13
|
+
const parsed = { tokens: [], json: false, yes: false };
|
|
14
|
+
const take = (i, name) => {
|
|
15
|
+
const v = rest[i + 1];
|
|
16
|
+
if (v === undefined)
|
|
17
|
+
throw new errors_1.CliError(`${name} needs a value`);
|
|
18
|
+
return v;
|
|
19
|
+
};
|
|
20
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
21
|
+
const arg = rest[i];
|
|
22
|
+
const opt = /^--(file|output|project)=(.*)$/s.exec(arg);
|
|
23
|
+
if (opt) {
|
|
24
|
+
if (opt[1] === 'file')
|
|
25
|
+
parsed.file = opt[2];
|
|
26
|
+
else if (opt[1] === 'output')
|
|
27
|
+
parsed.outFile = opt[2];
|
|
28
|
+
else
|
|
29
|
+
parsed.project = opt[2];
|
|
30
|
+
}
|
|
31
|
+
else if (arg === '-f' || arg === '--file')
|
|
32
|
+
parsed.file = take(i++, arg);
|
|
33
|
+
else if (arg === '-o' || arg === '--output')
|
|
34
|
+
parsed.outFile = take(i++, arg);
|
|
35
|
+
else if (arg === '--project')
|
|
36
|
+
parsed.project = take(i++, arg);
|
|
37
|
+
else if (arg === '--json')
|
|
38
|
+
parsed.json = true;
|
|
39
|
+
else if (arg === '--yes')
|
|
40
|
+
parsed.yes = true;
|
|
41
|
+
else if (arg.startsWith('--')) {
|
|
42
|
+
const eq = arg.indexOf('=');
|
|
43
|
+
parsed.tokens.push(eq === -1 ? { flag: arg.slice(2) } : { flag: arg.slice(2, eq), value: arg.slice(eq + 1) });
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
parsed.tokens.push({ positional: arg });
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
function fromFile(spec, file, readStdin) {
|
|
51
|
+
const loaded = (0, input_1.loadInputFile)(file, readStdin, { frontmatter: !!spec.frontmatterKeys, stdinText: !!spec.textField });
|
|
52
|
+
const cmd = `metrio ${spec.path.join(' ')}`;
|
|
53
|
+
if (loaded.kind === 'text') {
|
|
54
|
+
if (!spec.textField)
|
|
55
|
+
throw new errors_1.CliError(`${cmd} does not take a text file; use .json or .yaml`);
|
|
56
|
+
return { ...(loaded.frontmatter ?? {}), [spec.textField]: loaded.value };
|
|
57
|
+
}
|
|
58
|
+
if (loaded.kind === 'array') {
|
|
59
|
+
if (!spec.wrap)
|
|
60
|
+
throw new errors_1.CliError(`${file} holds a list; ${cmd} takes an object`);
|
|
61
|
+
return { [spec.wrap]: loaded.value };
|
|
62
|
+
}
|
|
63
|
+
return spec.wrap && !(spec.wrap in loaded.value) ? { [spec.wrap]: loaded.value } : loaded.value;
|
|
64
|
+
}
|
|
65
|
+
/** True when the non-option words are a proper prefix of some command path, e.g. `workflow version`. */
|
|
66
|
+
function isCommandPrefix(argv) {
|
|
67
|
+
const words = argv.filter((a) => !a.startsWith('-'));
|
|
68
|
+
return registry_1.COMMANDS.some((c) => words.length < c.path.length && words.every((w, i) => c.path[i] === w));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A prompt positional may name a tracked directory (prompts/<dir> or <dir>) instead of the
|
|
72
|
+
* numeric id — only for the workspace's own project, since ids are per project.
|
|
73
|
+
*/
|
|
74
|
+
function promptRef(raw, project) {
|
|
75
|
+
if (/^\d+$/.test(raw))
|
|
76
|
+
return raw;
|
|
77
|
+
const root = (0, workspace_1.findRoot)();
|
|
78
|
+
const dir = raw.replace(/\/+$/, '');
|
|
79
|
+
const key = dir.startsWith('prompts/') ? dir : `prompts/${dir}`;
|
|
80
|
+
// A broken workspace surfaces its own (informative) CliError.
|
|
81
|
+
const workspaceProject = root ? (0, workspace_1.readProject)(root) : undefined;
|
|
82
|
+
const entry = root ? (0, workspace_1.readState)(root, workspaceProject).resources[key] : undefined;
|
|
83
|
+
if (!entry || entry.kind !== 'prompt') {
|
|
84
|
+
throw new errors_1.CliError(`Unknown prompt "${raw}": use the numeric id, or a prompts/<dir> tracked in .metrio/state.json`);
|
|
85
|
+
}
|
|
86
|
+
if (project !== undefined && project !== workspaceProject) {
|
|
87
|
+
throw new errors_1.CliError(`"${raw}" is a directory of project ${workspaceProject}, but this command targets project ${String(project)}`);
|
|
88
|
+
}
|
|
89
|
+
return entry.id;
|
|
90
|
+
}
|
|
91
|
+
/** Runs one registry command; argv starts with the group, e.g. ['version', 'create', '12', '-f', 's.md']. */
|
|
92
|
+
async function runManageCommand(argv, readStdin) {
|
|
93
|
+
const match = (0, registry_1.findCommand)(argv);
|
|
94
|
+
const wantsHelp = argv.includes('-h') || argv.includes('--help');
|
|
95
|
+
if (!match || wantsHelp) {
|
|
96
|
+
if (match)
|
|
97
|
+
process.stdout.write((0, registry_1.commandHelp)(match.spec));
|
|
98
|
+
else if (wantsHelp || argv.length === 1 || isCommandPrefix(argv))
|
|
99
|
+
process.stdout.write((0, registry_1.groupHelp)(argv[0]));
|
|
100
|
+
else
|
|
101
|
+
throw new errors_1.CliError(`Unknown command: metrio ${argv.slice(0, 2).join(' ')}. Run "metrio ${argv[0]} --help".`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const { spec, rest } = match;
|
|
105
|
+
const opts = parse(rest);
|
|
106
|
+
const client = new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)());
|
|
107
|
+
const tool = (await client.listTools()).find((t) => t.name === spec.tool);
|
|
108
|
+
if (!tool) {
|
|
109
|
+
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);
|
|
110
|
+
}
|
|
111
|
+
const schema = tool.inputSchema;
|
|
112
|
+
const props = schema.properties ?? {};
|
|
113
|
+
const { values, positionals } = (0, schema_1.resolveFlags)(opts.tokens, schema, readStdin);
|
|
114
|
+
const expected = spec.args ?? [];
|
|
115
|
+
if (positionals.length !== expected.length) {
|
|
116
|
+
const labels = expected.map((a) => a.label).join(' ') || 'no arguments';
|
|
117
|
+
throw new errors_1.CliError(`metrio ${spec.path.join(' ')} expects ${labels}, got ${positionals.length ? positionals.join(' ') : 'none'}`);
|
|
118
|
+
}
|
|
119
|
+
const args = {};
|
|
120
|
+
const sources = {};
|
|
121
|
+
const put = (key, value, source) => {
|
|
122
|
+
args[key] = value;
|
|
123
|
+
sources[key] = source;
|
|
124
|
+
};
|
|
125
|
+
if (opts.file)
|
|
126
|
+
for (const [k, v] of Object.entries(fromFile(spec, opts.file, readStdin)))
|
|
127
|
+
put(k, v, `file ${opts.file}`);
|
|
128
|
+
if (opts.project && 'project_slug' in props)
|
|
129
|
+
put('project_slug', opts.project, 'project');
|
|
130
|
+
for (const [k, v] of Object.entries(values))
|
|
131
|
+
put(k, v, `--${(0, schema_1.kebab)(k)}`);
|
|
132
|
+
// A prompt directory resolves against the final project_slug, so it waits for it.
|
|
133
|
+
let promptDir;
|
|
134
|
+
expected.forEach((arg, i) => {
|
|
135
|
+
if (arg.prop === 'prompt_external_id' && !/^\d+$/.test(positionals[i]))
|
|
136
|
+
promptDir = { prop: arg.prop, raw: positionals[i], label: arg.label };
|
|
137
|
+
else
|
|
138
|
+
put(arg.prop, (0, schema_1.coercePositional)(positionals[i], props[arg.prop], arg.label), arg.label);
|
|
139
|
+
});
|
|
140
|
+
if ('project_slug' in props && !opts.project && args.project_slug === undefined) {
|
|
141
|
+
try {
|
|
142
|
+
const project = (0, project_1.resolveProject)(undefined);
|
|
143
|
+
if (project)
|
|
144
|
+
put('project_slug', project, 'project');
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if ((schema.required ?? []).includes('project_slug') || !(err instanceof errors_1.CliError))
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (promptDir) {
|
|
152
|
+
put(promptDir.prop, (0, schema_1.coercePositional)(promptRef(promptDir.raw, args.project_slug), props[promptDir.prop], promptDir.label), promptDir.label);
|
|
153
|
+
}
|
|
154
|
+
if ((schema.required ?? []).includes('project_slug') && args.project_slug === undefined) {
|
|
155
|
+
throw new errors_1.CliError('No project. Pass --project <slug>, set METRIO_PROJECT, or add "project: <slug>" to metrio.yaml.');
|
|
156
|
+
}
|
|
157
|
+
const { args: accepted, dropped } = (0, schema_1.dropUnaccepted)(args, schema);
|
|
158
|
+
if (dropped.length)
|
|
159
|
+
process.stderr.write(`metrio: ignoring fields ${spec.tool} does not accept: ${dropped.join(', ')}\n`);
|
|
160
|
+
(0, schema_1.validateArgs)(spec.tool, accepted, schema, sources);
|
|
161
|
+
if (spec.destructive && !opts.yes) {
|
|
162
|
+
process.stderr.write(`About to run ${spec.tool} with ${JSON.stringify(accepted)}.\n`);
|
|
163
|
+
throw new errors_1.CliError('Re-run with --yes to confirm.', 2);
|
|
164
|
+
}
|
|
165
|
+
const data = await client.callTool(spec.tool, accepted, { retry: tool.scope === 'read' });
|
|
166
|
+
await (0, output_1.writeOutput)(data, {
|
|
167
|
+
json: opts.json,
|
|
168
|
+
outFile: opts.outFile,
|
|
169
|
+
textField: spec.textField,
|
|
170
|
+
frontmatterKeys: spec.frontmatterKeys,
|
|
171
|
+
downloadField: spec.downloadField,
|
|
172
|
+
});
|
|
173
|
+
if (isRecord(data) && data.valid === false) {
|
|
174
|
+
const errors = Array.isArray(data.errors) ? data.errors : [];
|
|
175
|
+
const first = errors.length ? (typeof errors[0] === 'string' ? errors[0] : JSON.stringify(errors[0])) : 'no details';
|
|
176
|
+
const detail = errors.length > 1 ? `${first} (+${errors.length - 1} more)` : first;
|
|
177
|
+
throw new errors_1.CliError(`metrio: ${spec.tool} reported invalid input: ${detail}`, 2);
|
|
178
|
+
}
|
|
179
|
+
if (isRecord(data) && data.all_succeeded === false) {
|
|
180
|
+
const failed = Array.isArray(data.failed) ? data.failed.length : 0;
|
|
181
|
+
const total = failed + (Array.isArray(data.created) ? data.created.length : 0);
|
|
182
|
+
throw new errors_1.CliError(`metrio: ${spec.tool}: ${failed} of ${total} items failed`, 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|