@metrio-ai/cli 1.0.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.
@@ -0,0 +1,328 @@
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.readMessagesFile = readMessagesFile;
37
+ exports.buildMessages = buildMessages;
38
+ exports.loadEnv = loadEnv;
39
+ exports.resolveApiKey = resolveApiKey;
40
+ exports.resolveBaseUrl = resolveBaseUrl;
41
+ exports.parseJsonFlag = parseJsonFlag;
42
+ exports.resolvePromptId = resolvePromptId;
43
+ exports.parseJsonlObjects = parseJsonlObjects;
44
+ exports.caseIdFor = caseIdFor;
45
+ exports.readCasesSource = readCasesSource;
46
+ exports.mapWithConcurrency = mapWithConcurrency;
47
+ exports.emitBatchResults = emitBatchResults;
48
+ /**
49
+ * Shared CLI plumbing: env-file loading, credential resolution, and the
50
+ * conversion from the user-facing messages file format into the SDK's message
51
+ * shape. Every `metrio` subcommand builds on these.
52
+ */
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ /** Maps common file extensions to a MetrioAI file content type and MIME type. */
56
+ const FILE_TYPES = {
57
+ '.png': { type: 'image', mime: 'image/png' },
58
+ '.jpg': { type: 'image', mime: 'image/jpeg' },
59
+ '.jpeg': { type: 'image', mime: 'image/jpeg' },
60
+ '.gif': { type: 'image', mime: 'image/gif' },
61
+ '.webp': { type: 'image', mime: 'image/webp' },
62
+ '.pdf': { type: 'pdf', mime: 'application/pdf' },
63
+ };
64
+ /**
65
+ * Minimal `.env` loader so the CLI stays dependency-free. When `required` is
66
+ * true (an explicit --env file), a missing file is an error; otherwise the
67
+ * default `.env` is loaded if present and silently skipped if not.
68
+ */
69
+ function loadDotEnv(envPath, required) {
70
+ if (!fs.existsSync(envPath)) {
71
+ if (required)
72
+ throw new Error(`Env file not found: ${envPath}`);
73
+ return;
74
+ }
75
+ let contents;
76
+ try {
77
+ contents = fs.readFileSync(envPath, 'utf8');
78
+ }
79
+ catch (err) {
80
+ // An explicit --env file must be readable; for the default .env, fall back
81
+ // to whatever is already in the environment instead of crashing.
82
+ if (required) {
83
+ throw new Error(`Cannot read env file: ${envPath} (${err.message})`);
84
+ }
85
+ return;
86
+ }
87
+ const lines = contents.split('\n');
88
+ for (const rawLine of lines) {
89
+ const line = rawLine.trim();
90
+ if (!line || line.startsWith('#'))
91
+ continue;
92
+ const eq = line.indexOf('=');
93
+ if (eq === -1)
94
+ continue;
95
+ const key = line.slice(0, eq).trim();
96
+ let value = line.slice(eq + 1).trim();
97
+ // Strip surrounding single or double quotes.
98
+ if ((value.startsWith('"') && value.endsWith('"')) ||
99
+ (value.startsWith("'") && value.endsWith("'"))) {
100
+ value = value.slice(1, -1);
101
+ }
102
+ // Existing environment variables take precedence over the .env file.
103
+ if (process.env[key] === undefined) {
104
+ process.env[key] = value;
105
+ }
106
+ }
107
+ }
108
+ /** Reads and validates the messages file, returning the raw input entries. */
109
+ function readMessagesFile(file) {
110
+ let raw;
111
+ try {
112
+ raw = fs.readFileSync(file, 'utf8');
113
+ }
114
+ catch (err) {
115
+ throw new Error(`Cannot read messages file: ${file} (${err.message})`);
116
+ }
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(raw);
120
+ }
121
+ catch (err) {
122
+ throw new Error(`Messages file is not valid JSON: ${err.message}`);
123
+ }
124
+ if (!Array.isArray(parsed)) {
125
+ throw new Error('Messages file must contain a JSON array of messages');
126
+ }
127
+ return parsed;
128
+ }
129
+ /** Builds a file content block by reading and base64-encoding the file. */
130
+ function buildFileContent(input, baseDir) {
131
+ const filePath = path.resolve(baseDir, input.file);
132
+ let data;
133
+ try {
134
+ data = fs.readFileSync(filePath);
135
+ }
136
+ catch (err) {
137
+ throw new Error(`Cannot read attached file: ${filePath} (${err.message})`);
138
+ }
139
+ const ext = path.extname(filePath).toLowerCase();
140
+ const known = FILE_TYPES[ext];
141
+ const type = input.fileType ?? known?.type ?? 'binary';
142
+ const mime = input.mime ?? known?.mime ?? 'application/octet-stream';
143
+ return { type, mime, data: data.toString('base64') };
144
+ }
145
+ /**
146
+ * Converts user-friendly input messages into the SDK's message shape. A single
147
+ * input entry may expand into two messages when it carries both text and a file.
148
+ */
149
+ function buildMessages(inputs, baseDir) {
150
+ const messages = [];
151
+ inputs.forEach((input, index) => {
152
+ if (!input || typeof input !== 'object') {
153
+ throw new Error(`Message #${index} must be an object`);
154
+ }
155
+ if (input.role !== 'system' && input.role !== 'user' && input.role !== 'assistant') {
156
+ throw new Error(`Message #${index} has invalid role: ${JSON.stringify(input.role)}`);
157
+ }
158
+ const text = input.content ?? input.text;
159
+ const hasText = typeof text === 'string' && text.trim() !== '';
160
+ const hasFile = typeof input.file === 'string' && input.file !== '';
161
+ if (!hasText && !hasFile) {
162
+ throw new Error(`Message #${index} must have "content" text and/or a "file"`);
163
+ }
164
+ if (hasText) {
165
+ messages.push({ role: input.role, content: { type: 'text', text: text } });
166
+ }
167
+ if (hasFile) {
168
+ messages.push({ role: input.role, content: buildFileContent(input, baseDir) });
169
+ }
170
+ });
171
+ return messages;
172
+ }
173
+ /**
174
+ * Loads the env file. An explicit --env file must exist; the default `.env`
175
+ * is loaded if present and silently skipped otherwise.
176
+ */
177
+ function loadEnv(envFile) {
178
+ const envPath = envFile ? path.resolve(envFile) : path.join(process.cwd(), '.env');
179
+ loadDotEnv(envPath, envFile !== undefined);
180
+ }
181
+ /** Resolves the API key from a flag then the environment, throwing if absent. */
182
+ function resolveApiKey(flagKey) {
183
+ const apiKey = flagKey ||
184
+ process.env.METRIOAI_API_KEY ||
185
+ process.env.METRIO_API_KEY ||
186
+ process.env.METRIO_PROJECT_API_KEY;
187
+ if (!apiKey) {
188
+ throw new Error('No API key found. Set METRIOAI_API_KEY in .env or pass --api-key.');
189
+ }
190
+ return apiKey;
191
+ }
192
+ /** Resolves the base URL from a flag then the environment (undefined = default). */
193
+ function resolveBaseUrl(flagUrl) {
194
+ return flagUrl || process.env.METRIOAI_BASE_URL || process.env.METRIO_BASE_URL;
195
+ }
196
+ /** Parses a JSON CLI flag, throwing a friendly error on malformed input. */
197
+ function parseJsonFlag(raw, flag) {
198
+ try {
199
+ return JSON.parse(raw);
200
+ }
201
+ catch (err) {
202
+ throw new Error(`${flag} must be valid JSON: ${err.message}`);
203
+ }
204
+ }
205
+ /**
206
+ * Resolves the prompt id to run: the `--prompt-id`/`-p` flag if given,
207
+ * otherwise `METRIO_PROMPT_ID` or `METRIOAI_PROMPT_ID` from the environment
208
+ * (checked in that order). Throws if neither source provides one, or if the
209
+ * environment value is not a positive integer.
210
+ */
211
+ function resolvePromptId(flagId) {
212
+ if (flagId !== undefined)
213
+ return flagId;
214
+ const raw = process.env.METRIO_PROMPT_ID ?? process.env.METRIOAI_PROMPT_ID;
215
+ if (raw !== undefined && raw.trim() !== '') {
216
+ const id = Number(raw);
217
+ if (!Number.isInteger(id) || id <= 0) {
218
+ throw new Error(`METRIO_PROMPT_ID must be a positive integer, got "${raw}"`);
219
+ }
220
+ return id;
221
+ }
222
+ throw new Error('Missing prompt id. Pass --prompt-id or set METRIO_PROMPT_ID in your env file.');
223
+ }
224
+ /**
225
+ * Parses a JSONL cases file into objects. Blank lines and `//` comment lines
226
+ * are ignored so case files stay hand-editable; ignored lines still count
227
+ * toward the line numbers used for error messages and generated ids.
228
+ *
229
+ * Field validation belongs to each command — this only guarantees every entry
230
+ * is a JSON object and knows where it came from.
231
+ */
232
+ function parseJsonlObjects(raw) {
233
+ const entries = [];
234
+ raw.split('\n').forEach((rawLine, index) => {
235
+ const line = rawLine.trim();
236
+ if (!line || line.startsWith('//'))
237
+ return;
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(line);
241
+ }
242
+ catch (err) {
243
+ throw new Error(`Case on line ${index + 1} is not valid JSON: ${err.message}`);
244
+ }
245
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
246
+ throw new Error(`Case on line ${index + 1} must be a JSON object`);
247
+ }
248
+ entries.push({ line: index + 1, value: parsed });
249
+ });
250
+ if (entries.length === 0) {
251
+ throw new Error('Cases file contains no cases');
252
+ }
253
+ return entries;
254
+ }
255
+ /**
256
+ * The id a case is known by: its own `id` when it has a non-empty one,
257
+ * otherwise `case-<line>` so every output row stays addressable.
258
+ */
259
+ function caseIdFor(value, line) {
260
+ return typeof value.id === 'string' && value.id !== '' ? value.id : `case-${line}`;
261
+ }
262
+ /** Reads all of stdin as UTF-8 — backs `--cases -`. */
263
+ async function readStdin() {
264
+ const chunks = [];
265
+ for await (const chunk of process.stdin)
266
+ chunks.push(Buffer.from(chunk));
267
+ return Buffer.concat(chunks).toString('utf8');
268
+ }
269
+ /** Reads a `--cases` argument, where `-` means stdin. */
270
+ async function readCasesSource(casesFile) {
271
+ if (casesFile === '-') {
272
+ return { raw: await readStdin(), baseDir: process.cwd() };
273
+ }
274
+ try {
275
+ return {
276
+ raw: fs.readFileSync(casesFile, 'utf8'),
277
+ baseDir: path.dirname(path.resolve(casesFile)),
278
+ };
279
+ }
280
+ catch (err) {
281
+ throw new Error(`Cannot read cases file: ${casesFile} (${err.message})`);
282
+ }
283
+ }
284
+ /**
285
+ * Runs `worker` over `items` with at most `limit` in flight, resolving to
286
+ * results in input order — batch output is diffed across runs, so completion
287
+ * order must not leak into it.
288
+ */
289
+ async function mapWithConcurrency(items, limit, worker) {
290
+ const results = new Array(items.length);
291
+ let cursor = 0;
292
+ const runners = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
293
+ while (cursor < items.length) {
294
+ const index = cursor++;
295
+ results[index] = await worker(items[index], index);
296
+ }
297
+ });
298
+ await Promise.all(runners);
299
+ return results;
300
+ }
301
+ /**
302
+ * Emits a batch's result rows as JSONL — to `--out` when given, otherwise to
303
+ * stdout — then reports the tally on stderr and sets the exit code.
304
+ *
305
+ * The batch has already run by this point, so a failed `--out` write falls
306
+ * back to stdout rather than throwing the results away. A failing case is a
307
+ * result, not a crash: every row is still emitted, and only the exit code
308
+ * (1 when any case failed) marks the batch as unsuccessful.
309
+ */
310
+ function emitBatchResults(rows, out) {
311
+ const body = `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`;
312
+ if (out) {
313
+ try {
314
+ fs.writeFileSync(path.resolve(out), body);
315
+ }
316
+ catch (err) {
317
+ process.stderr.write(`Warning: could not write --out file ${out} (${err.message}); printing results to stdout instead\n`);
318
+ process.stdout.write(body);
319
+ }
320
+ }
321
+ else {
322
+ process.stdout.write(body);
323
+ }
324
+ const failed = rows.filter((row) => !row.ok).length;
325
+ process.stderr.write(`${rows.length} cases: ${rows.length - failed} ok, ${failed} failed\n`);
326
+ if (failed > 0)
327
+ process.exitCode = 1;
328
+ }
@@ -0,0 +1,114 @@
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.runToolsCommand = runToolsCommand;
37
+ exports.runCallCommand = runCallCommand;
38
+ const fs = __importStar(require("fs"));
39
+ const client_1 = require("../console/client");
40
+ const errors_1 = require("../errors");
41
+ const TOOLS_HELP = `metrio tools - list the console tools your login can call
42
+
43
+ Usage: metrio tools [--json]
44
+ `;
45
+ const CALL_HELP = `metrio call - call one console tool
46
+
47
+ Usage: metrio call <tool> [-f <args.json> | -f - | --args '<json>']
48
+
49
+ Arguments are a JSON object matching the tool's schema (see "metrio tools --json").
50
+ With no arguments, {} is sent.
51
+ `;
52
+ async function runToolsCommand(argv) {
53
+ if (argv.includes('-h') || argv.includes('--help')) {
54
+ process.stdout.write(TOOLS_HELP);
55
+ return;
56
+ }
57
+ const asJson = argv.includes('--json');
58
+ const unknown = argv.find((a) => a !== '--json');
59
+ if (unknown)
60
+ throw new errors_1.CliError(`Unknown argument: ${unknown}`);
61
+ const tools = await new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)()).listTools();
62
+ if (asJson) {
63
+ process.stdout.write(`${JSON.stringify(tools, null, 2)}\n`);
64
+ return;
65
+ }
66
+ const nameWidth = Math.max(...tools.map((t) => t.name.length), 0) + 2;
67
+ const scopeWidth = Math.max(...tools.map((t) => t.scope.length), 0) + 2;
68
+ for (const tool of tools) {
69
+ const summary = tool.description.split('\n')[0];
70
+ process.stdout.write(`${tool.name.padEnd(nameWidth)}${tool.scope.padEnd(scopeWidth)}${summary}\n`);
71
+ }
72
+ }
73
+ function parseArgsObject(raw, source) {
74
+ let value;
75
+ try {
76
+ value = JSON.parse(raw);
77
+ }
78
+ catch (err) {
79
+ throw new errors_1.CliError(`${source} must be valid JSON: ${err.message}`);
80
+ }
81
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
82
+ throw new errors_1.CliError(`${source} must be a JSON object`);
83
+ }
84
+ return value;
85
+ }
86
+ async function runCallCommand(argv, readStdin = () => fs.readFileSync(0, 'utf8')) {
87
+ if (argv.includes('-h') || argv.includes('--help')) {
88
+ process.stdout.write(CALL_HELP);
89
+ return;
90
+ }
91
+ const [name, ...rest] = argv;
92
+ if (!name || name.startsWith('-'))
93
+ throw new errors_1.CliError('Missing tool name. Usage: metrio call <tool> [-f args.json]');
94
+ let file;
95
+ let inline;
96
+ for (let i = 0; i < rest.length; i += 1) {
97
+ const arg = rest[i];
98
+ if ((arg === '-f' || arg === '--file') && rest[i + 1] !== undefined)
99
+ file = rest[(i += 1)];
100
+ else if (arg === '--args' && rest[i + 1] !== undefined)
101
+ inline = rest[(i += 1)];
102
+ else
103
+ throw new errors_1.CliError(`Unknown argument: ${arg}`);
104
+ }
105
+ if (file !== undefined && inline !== undefined)
106
+ throw new errors_1.CliError('Use either -f or --args, not both');
107
+ const args = file !== undefined
108
+ ? parseArgsObject(file === '-' ? readStdin() : fs.readFileSync(file, 'utf8'), file === '-' ? 'stdin' : file)
109
+ : inline !== undefined
110
+ ? parseArgsObject(inline, '--args')
111
+ : {};
112
+ const data = await new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)()).callTool(name, args);
113
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
114
+ }