@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.
- package/LICENSE +29 -0
- package/README.md +38 -0
- package/dist/auth/credentials.js +81 -0
- package/dist/auth/login.js +143 -0
- package/dist/auth/oauth.js +44 -0
- package/dist/commands/auth.js +74 -0
- package/dist/commands/chat.js +235 -0
- package/dist/commands/prompt-create-version.js +211 -0
- package/dist/commands/prompt.js +284 -0
- package/dist/commands/shared.js +328 -0
- package/dist/commands/tools.js +114 -0
- package/dist/commands/workflow.js +666 -0
- package/dist/console/client.js +108 -0
- package/dist/errors.js +27 -0
- package/dist/index.js +105 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Steven Shen <steven@jidou.ai>
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @metrio-ai/cli
|
|
2
|
+
|
|
3
|
+
The `metrio` command-line interface for [MetrioAI](https://metrio.ai): run prompts and workflows, and manage Metrio resources from files.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i -g @metrio-ai/cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Run prompts and workflows (project API key)
|
|
10
|
+
|
|
11
|
+
Set `METRIOAI_API_KEY` (or `METRIO_API_KEY` / `METRIO_PROJECT_API_KEY`) in the environment or a `.env` file; `-e/--env <file>` picks another env file.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
metrio chat --messages messages.json --prompt-id 7
|
|
15
|
+
metrio workflow run -w my-flow --message "hello" --json
|
|
16
|
+
metrio workflow run-batch -w my-flow --cases cases.jsonl
|
|
17
|
+
metrio prompt run-batch -p 7 --cases cases.jsonl
|
|
18
|
+
metrio prompt create-version -p 7 -f system.md
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`metrio chat --help`, `metrio workflow --help` and `metrio prompt --help` list every option.
|
|
22
|
+
|
|
23
|
+
## Manage resources (console login)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
metrio login # browser sign-in; token stored in ~/.config/metrio/credentials.json
|
|
27
|
+
metrio whoami
|
|
28
|
+
metrio tools # every console tool your login can call
|
|
29
|
+
metrio call list_prompts --args '{"project_slug":"my-project"}'
|
|
30
|
+
metrio call create_workflow_version -f args.json
|
|
31
|
+
metrio logout
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
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
|
+
|
|
36
|
+
Exit codes: `0` ok, `1` failure, `2` usage or validation error, `3` not logged in / not allowed. Codes 2 and 3 apply to the account/management commands (`login`, `logout`, `whoami`, `tools`, `call`); the run commands keep their original behavior (errors exit 1).
|
|
37
|
+
|
|
38
|
+
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 …`.
|
|
@@ -0,0 +1,81 @@
|
|
|
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.DEFAULT_CONSOLE_URL = void 0;
|
|
37
|
+
exports.normalizeConsoleUrl = normalizeConsoleUrl;
|
|
38
|
+
exports.credentialsPath = credentialsPath;
|
|
39
|
+
exports.readCredentials = readCredentials;
|
|
40
|
+
exports.writeCredentials = writeCredentials;
|
|
41
|
+
exports.deleteCredentials = deleteCredentials;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
exports.DEFAULT_CONSOLE_URL = 'https://app.metrio.ai';
|
|
46
|
+
function normalizeConsoleUrl(url) {
|
|
47
|
+
return url.replace(/\/+$/, '');
|
|
48
|
+
}
|
|
49
|
+
function credentialsPath() {
|
|
50
|
+
const dir = process.env.METRIO_CONFIG_DIR || path.join(os.homedir(), '.config', 'metrio');
|
|
51
|
+
return path.join(dir, 'credentials.json');
|
|
52
|
+
}
|
|
53
|
+
function readCredentials() {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(credentialsPath(), 'utf8'));
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code === 'ENOENT')
|
|
59
|
+
return null;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function writeCredentials(creds) {
|
|
64
|
+
const file = credentialsPath();
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
66
|
+
// Atomic: concurrent CLI processes never observe a half-written file.
|
|
67
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
68
|
+
fs.writeFileSync(tmp, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
69
|
+
fs.renameSync(tmp, file);
|
|
70
|
+
}
|
|
71
|
+
function deleteCredentials() {
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(credentialsPath());
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (err.code === 'ENOENT')
|
|
78
|
+
return false;
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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.LOGIN_SCOPE = void 0;
|
|
37
|
+
exports.browserCommand = browserCommand;
|
|
38
|
+
exports.openBrowser = openBrowser;
|
|
39
|
+
exports.login = login;
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
42
|
+
const http = __importStar(require("http"));
|
|
43
|
+
const errors_1 = require("../errors");
|
|
44
|
+
const credentials_1 = require("./credentials");
|
|
45
|
+
const oauth_1 = require("./oauth");
|
|
46
|
+
/** admin is only granted to site admins; openid lets `whoami` read /api/oauth/userinfo. */
|
|
47
|
+
exports.LOGIN_SCOPE = 'read write admin openid';
|
|
48
|
+
/** Never routes the URL through a shell (cmd.exe would split it at `&`). */
|
|
49
|
+
function browserCommand(url, platform = process.platform) {
|
|
50
|
+
if (platform === 'darwin')
|
|
51
|
+
return ['open', [url]];
|
|
52
|
+
if (platform === 'win32')
|
|
53
|
+
return ['rundll32', ['url.dll,FileProtocolHandler', url]];
|
|
54
|
+
return ['xdg-open', [url]];
|
|
55
|
+
}
|
|
56
|
+
function openBrowser(url) {
|
|
57
|
+
const [cmd, args] = browserCommand(url);
|
|
58
|
+
const child = (0, child_process_1.spawn)(cmd, args, { stdio: 'ignore', detached: true });
|
|
59
|
+
child.on('error', () => undefined); // the URL is printed too, so a missing opener is fine
|
|
60
|
+
child.unref();
|
|
61
|
+
}
|
|
62
|
+
/** Resolves with the authorization code delivered to the loopback /callback. */
|
|
63
|
+
function waitForCallback(server, state, timeoutMs) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const timer = setTimeout(() => reject(new Error('Timed out waiting for the browser login.')), timeoutMs);
|
|
66
|
+
server.on('request', (req, res) => {
|
|
67
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
68
|
+
if (url.pathname !== '/callback') {
|
|
69
|
+
res.writeHead(404).end();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const finish = (status, message) => res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' }).end(message);
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
const error = url.searchParams.get('error');
|
|
75
|
+
if (error) {
|
|
76
|
+
finish(400, 'Metrio CLI login failed. You can close this tab.');
|
|
77
|
+
reject(new Error(`Authorization denied: ${url.searchParams.get('error_description') || error}`));
|
|
78
|
+
}
|
|
79
|
+
else if (url.searchParams.get('state') !== state) {
|
|
80
|
+
finish(400, 'Metrio CLI login failed (state mismatch). You can close this tab.');
|
|
81
|
+
reject(new Error('Login failed: state mismatch in the OAuth callback.'));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
finish(200, 'Metrio CLI login complete. You can close this tab.');
|
|
85
|
+
resolve(url.searchParams.get('code') ?? '');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Browser login against the console's OAuth server: register a client for this
|
|
92
|
+
* run's loopback port (the console matches loopback ports exactly), authorize
|
|
93
|
+
* with a random state, then exchange the code with the client secret.
|
|
94
|
+
*/
|
|
95
|
+
async function login(consoleUrlRaw, deps) {
|
|
96
|
+
const consoleUrl = (0, credentials_1.normalizeConsoleUrl)(consoleUrlRaw);
|
|
97
|
+
const server = http.createServer();
|
|
98
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
99
|
+
try {
|
|
100
|
+
const { port } = server.address();
|
|
101
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
102
|
+
const regRes = await fetch(`${consoleUrl}/api/oauth/register`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
client_name: 'metrio-cli',
|
|
107
|
+
redirect_uris: [redirectUri],
|
|
108
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
109
|
+
response_types: ['code'],
|
|
110
|
+
token_endpoint_auth_method: 'client_secret_post',
|
|
111
|
+
scope: exports.LOGIN_SCOPE,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
const reg = (await regRes.json().catch(() => ({})));
|
|
115
|
+
if (!regRes.ok) {
|
|
116
|
+
throw new errors_1.ConsoleApiError(reg.error_description || `Client registration failed (HTTP ${regRes.status})`, regRes.status, reg.error || 'registration_failed');
|
|
117
|
+
}
|
|
118
|
+
const state = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
119
|
+
const authorize = new URL(`${consoleUrl}/api/oauth/authorize`);
|
|
120
|
+
authorize.search = new URLSearchParams({
|
|
121
|
+
response_type: 'code',
|
|
122
|
+
client_id: reg.client_id,
|
|
123
|
+
redirect_uri: redirectUri,
|
|
124
|
+
scope: exports.LOGIN_SCOPE,
|
|
125
|
+
state,
|
|
126
|
+
}).toString();
|
|
127
|
+
const code = waitForCallback(server, state, deps.timeoutMs ?? 5 * 60_000);
|
|
128
|
+
deps.log(`Opening your browser to sign in. If it does not open, visit:\n ${authorize}\n`);
|
|
129
|
+
deps.openBrowser(authorize.toString());
|
|
130
|
+
const token = (await (0, oauth_1.postForm)(`${consoleUrl}/api/oauth/token`, {
|
|
131
|
+
grant_type: 'authorization_code',
|
|
132
|
+
code: await code,
|
|
133
|
+
redirect_uri: redirectUri,
|
|
134
|
+
client_id: reg.client_id,
|
|
135
|
+
client_secret: reg.client_secret,
|
|
136
|
+
}));
|
|
137
|
+
return (0, oauth_1.toCredentials)(consoleUrl, reg.client_id, reg.client_secret, token);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
server.closeAllConnections?.();
|
|
141
|
+
server.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.postForm = postForm;
|
|
4
|
+
exports.toCredentials = toCredentials;
|
|
5
|
+
exports.refreshCredentials = refreshCredentials;
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const credentials_1 = require("./credentials");
|
|
8
|
+
/** POSTs an x-www-form-urlencoded body (OAuth token/revoke endpoints). */
|
|
9
|
+
async function postForm(url, params) {
|
|
10
|
+
const res = await fetch(url, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
13
|
+
body: new URLSearchParams(params).toString(),
|
|
14
|
+
});
|
|
15
|
+
const body = (await res.json().catch(() => ({})));
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const message = String(body.error_description ?? body.error ?? `HTTP ${res.status}`);
|
|
18
|
+
throw new errors_1.ConsoleApiError(message, res.status, String(body.error ?? 'oauth_error'));
|
|
19
|
+
}
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
function toCredentials(consoleUrl, clientId, clientSecret, token, now = Date.now()) {
|
|
23
|
+
return {
|
|
24
|
+
console_url: (0, credentials_1.normalizeConsoleUrl)(consoleUrl),
|
|
25
|
+
client_id: clientId,
|
|
26
|
+
client_secret: clientSecret,
|
|
27
|
+
access_token: token.access_token,
|
|
28
|
+
refresh_token: token.refresh_token,
|
|
29
|
+
expires_at: new Date(now + token.expires_in * 1000).toISOString(),
|
|
30
|
+
scope: token.scope ?? '',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function refreshCredentials(creds) {
|
|
34
|
+
const token = (await postForm(`${creds.console_url}/api/oauth/token`, {
|
|
35
|
+
grant_type: 'refresh_token',
|
|
36
|
+
refresh_token: creds.refresh_token,
|
|
37
|
+
client_id: creds.client_id,
|
|
38
|
+
client_secret: creds.client_secret,
|
|
39
|
+
}));
|
|
40
|
+
return toCredentials(creds.console_url, creds.client_id, creds.client_secret, {
|
|
41
|
+
...token,
|
|
42
|
+
scope: token.scope ?? creds.scope,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runLoginCommand = runLoginCommand;
|
|
4
|
+
exports.runLogoutCommand = runLogoutCommand;
|
|
5
|
+
exports.runWhoamiCommand = runWhoamiCommand;
|
|
6
|
+
const credentials_1 = require("../auth/credentials");
|
|
7
|
+
const login_1 = require("../auth/login");
|
|
8
|
+
const oauth_1 = require("../auth/oauth");
|
|
9
|
+
const client_1 = require("../console/client");
|
|
10
|
+
const errors_1 = require("../errors");
|
|
11
|
+
const LOGIN_HELP = `metrio login - sign in to the Metrio console in your browser
|
|
12
|
+
|
|
13
|
+
Usage: metrio login [--console-url <url>]
|
|
14
|
+
|
|
15
|
+
--console-url <url> Console to sign in to (default: METRIO_CONSOLE_URL or ${credentials_1.DEFAULT_CONSOLE_URL})
|
|
16
|
+
|
|
17
|
+
The login is stored in ~/.config/metrio/credentials.json (override the directory
|
|
18
|
+
with METRIO_CONFIG_DIR). For CI or headless agents, set METRIO_TOKEN instead.
|
|
19
|
+
`;
|
|
20
|
+
async function runLoginCommand(argv, deps) {
|
|
21
|
+
let consoleUrl = process.env.METRIO_CONSOLE_URL || credentials_1.DEFAULT_CONSOLE_URL;
|
|
22
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
23
|
+
const arg = argv[i];
|
|
24
|
+
if (arg === '-h' || arg === '--help') {
|
|
25
|
+
process.stdout.write(LOGIN_HELP);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (arg === '--console-url' && argv[i + 1]) {
|
|
29
|
+
consoleUrl = argv[(i += 1)];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new errors_1.CliError(`Unknown argument: ${arg}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const creds = await (0, login_1.login)(consoleUrl, deps ?? { openBrowser: login_1.openBrowser, log: (m) => process.stderr.write(m) });
|
|
36
|
+
(0, credentials_1.writeCredentials)(creds);
|
|
37
|
+
process.stdout.write(`Logged in to ${creds.console_url} (scopes: ${creds.scope})\n`);
|
|
38
|
+
}
|
|
39
|
+
async function runLogoutCommand(argv) {
|
|
40
|
+
if (argv.length > 0)
|
|
41
|
+
throw new errors_1.CliError(`Unknown argument: ${argv[0]}`);
|
|
42
|
+
const creds = (0, credentials_1.readCredentials)();
|
|
43
|
+
if (!creds) {
|
|
44
|
+
process.stdout.write('Not logged in.\n');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Best effort, each independently: an unreachable console must not keep tokens on disk.
|
|
48
|
+
// Revoking the refresh token also drops its access token; the access-token call covers
|
|
49
|
+
// the reverse and is harmless if already gone.
|
|
50
|
+
const revoke = (token, hint) => (0, oauth_1.postForm)(`${creds.console_url}/api/oauth/revoke`, {
|
|
51
|
+
token,
|
|
52
|
+
token_type_hint: hint,
|
|
53
|
+
client_id: creds.client_id,
|
|
54
|
+
client_secret: creds.client_secret,
|
|
55
|
+
}).catch(() => undefined);
|
|
56
|
+
await revoke(creds.refresh_token, 'refresh_token');
|
|
57
|
+
await revoke(creds.access_token, 'access_token');
|
|
58
|
+
(0, credentials_1.deleteCredentials)();
|
|
59
|
+
process.stdout.write(`Logged out of ${creds.console_url}\n`);
|
|
60
|
+
}
|
|
61
|
+
async function runWhoamiCommand(argv) {
|
|
62
|
+
const asJson = argv.includes('--json');
|
|
63
|
+
const unknown = argv.find((a) => a !== '--json');
|
|
64
|
+
if (unknown)
|
|
65
|
+
throw new errors_1.CliError(`Unknown argument: ${unknown}`);
|
|
66
|
+
const auth = await (0, client_1.resolveConsoleAuth)();
|
|
67
|
+
const info = (await new client_1.ConsoleClient(auth).getJson('/api/oauth/userinfo'));
|
|
68
|
+
if (asJson) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify({ ...info, console_url: auth.consoleUrl }, null, 2)}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const who = info.email ? `${info.name} <${info.email}>` : String(info.name ?? info.sub);
|
|
73
|
+
process.stdout.write(`${who}\nconsole: ${auth.consoleUrl}\nscopes: ${info.scope}\n`);
|
|
74
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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.runChatCommand = runChatCommand;
|
|
37
|
+
const client_1 = require("@metrio-ai/client");
|
|
38
|
+
const shared_1 = require("./shared");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const HELP = `metrio - MetrioAI CLI
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
metrio chat [options] Run a chat completion
|
|
44
|
+
metrio workflow run [options] Run a workflow completion
|
|
45
|
+
(see: metrio workflow --help)
|
|
46
|
+
metrio prompt run-batch Run several prompt cases in one process
|
|
47
|
+
(see: metrio prompt --help)
|
|
48
|
+
metrio prompt create-version Create a prompt version from a file
|
|
49
|
+
|
|
50
|
+
Chat completion usage:
|
|
51
|
+
metrio chat --messages <file> [--prompt-id <id>] [options]
|
|
52
|
+
|
|
53
|
+
Required:
|
|
54
|
+
-m, --messages <file> Path to the messages JSON file
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
-p, --prompt-id <id> Prompt ID to run (integer; or set METRIO_PROMPT_ID)
|
|
58
|
+
-s, --system <text> System message prepended as the first message
|
|
59
|
+
-e, --env <file> Env file to load (default: .env in the current dir)
|
|
60
|
+
-t, --tag <tag> Version tag to pin (defaults to active version)
|
|
61
|
+
--var <name=value> Template variable (repeatable)
|
|
62
|
+
--request-tag <tag> Request-level tag recorded on the run, e.g. "test"
|
|
63
|
+
(repeatable; max 5; each 1-32 chars, lowercase
|
|
64
|
+
alphanumeric start, may contain "-" and "_")
|
|
65
|
+
--timezone <tz> IANA timezone, e.g. "Asia/Taipei"
|
|
66
|
+
--text Print only the response text (default: full JSON)
|
|
67
|
+
--stream Stream tokens to stderr live while running
|
|
68
|
+
--api-key <key> API key (overrides env file / environment)
|
|
69
|
+
--base-url <url> Base API URL (overrides env file / environment)
|
|
70
|
+
-h, --help Show this help
|
|
71
|
+
|
|
72
|
+
Environment (read from the env file or the shell; flags take precedence):
|
|
73
|
+
METRIOAI_API_KEY / METRIO_API_KEY / METRIO_PROJECT_API_KEY API key
|
|
74
|
+
METRIO_PROMPT_ID / METRIOAI_PROMPT_ID Prompt ID
|
|
75
|
+
METRIOAI_BASE_URL Base URL
|
|
76
|
+
|
|
77
|
+
Use -e/--env to keep one env file per project (e.g. project-a.env) and run
|
|
78
|
+
the same messages file against different projects for testing.
|
|
79
|
+
|
|
80
|
+
Messages file format (a JSON array):
|
|
81
|
+
[
|
|
82
|
+
{ "role": "system", "content": "You are a helpful assistant." },
|
|
83
|
+
{ "role": "user", "content": "Describe this image", "file": "./cat.png" },
|
|
84
|
+
{ "role": "user", "file": "./report.pdf" }
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
A message with both "content" and "file" is automatically split into two
|
|
88
|
+
messages (text first, then the file) because the API allows a single content
|
|
89
|
+
block per message.
|
|
90
|
+
`;
|
|
91
|
+
/** Parses argv into structured CLI options. Throws on malformed input. */
|
|
92
|
+
function parseArgs(argv) {
|
|
93
|
+
const options = {
|
|
94
|
+
variables: [],
|
|
95
|
+
tags: [],
|
|
96
|
+
text: false,
|
|
97
|
+
stream: false,
|
|
98
|
+
help: false,
|
|
99
|
+
};
|
|
100
|
+
const next = (i, flag) => {
|
|
101
|
+
const value = argv[i + 1];
|
|
102
|
+
if (value === undefined) {
|
|
103
|
+
throw new Error(`Missing value for ${flag}`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
};
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
const arg = argv[i];
|
|
109
|
+
switch (arg) {
|
|
110
|
+
case '-p':
|
|
111
|
+
case '--prompt-id': {
|
|
112
|
+
const raw = next(i++, arg);
|
|
113
|
+
const id = Number(raw);
|
|
114
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
115
|
+
throw new Error(`--prompt-id must be a positive integer, got "${raw}"`);
|
|
116
|
+
}
|
|
117
|
+
options.promptId = id;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case '-m':
|
|
121
|
+
case '--messages':
|
|
122
|
+
options.messagesFile = next(i++, arg);
|
|
123
|
+
break;
|
|
124
|
+
case '-s':
|
|
125
|
+
case '--system':
|
|
126
|
+
options.system = next(i++, arg);
|
|
127
|
+
break;
|
|
128
|
+
case '-e':
|
|
129
|
+
case '--env':
|
|
130
|
+
options.envFile = next(i++, arg);
|
|
131
|
+
break;
|
|
132
|
+
case '-t':
|
|
133
|
+
case '--tag':
|
|
134
|
+
options.tag = next(i++, arg);
|
|
135
|
+
break;
|
|
136
|
+
case '--timezone':
|
|
137
|
+
options.timezone = next(i++, arg);
|
|
138
|
+
break;
|
|
139
|
+
case '--var': {
|
|
140
|
+
const raw = next(i++, arg);
|
|
141
|
+
const eq = raw.indexOf('=');
|
|
142
|
+
if (eq === -1) {
|
|
143
|
+
throw new Error(`--var must be in the form name=value, got "${raw}"`);
|
|
144
|
+
}
|
|
145
|
+
const name = raw.slice(0, eq).trim();
|
|
146
|
+
if (!name) {
|
|
147
|
+
throw new Error(`--var name cannot be empty, got "${raw}"`);
|
|
148
|
+
}
|
|
149
|
+
options.variables.push({ name, value: raw.slice(eq + 1) });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case '--request-tag': {
|
|
153
|
+
const tag = next(i++, arg).trim();
|
|
154
|
+
if (!tag) {
|
|
155
|
+
throw new Error('--request-tag cannot be empty');
|
|
156
|
+
}
|
|
157
|
+
options.tags.push(tag);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case '--api-key':
|
|
161
|
+
options.apiKey = next(i++, arg);
|
|
162
|
+
break;
|
|
163
|
+
case '--base-url':
|
|
164
|
+
options.baseUrl = next(i++, arg);
|
|
165
|
+
break;
|
|
166
|
+
case '--text':
|
|
167
|
+
options.text = true;
|
|
168
|
+
break;
|
|
169
|
+
case '--stream':
|
|
170
|
+
options.stream = true;
|
|
171
|
+
break;
|
|
172
|
+
case '-h':
|
|
173
|
+
case '--help':
|
|
174
|
+
options.help = true;
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return options;
|
|
181
|
+
}
|
|
182
|
+
/** The default command: run a chat completion. */
|
|
183
|
+
async function runChatCommand(argv) {
|
|
184
|
+
const options = parseArgs(argv);
|
|
185
|
+
if (options.help) {
|
|
186
|
+
process.stdout.write(HELP);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!options.messagesFile) {
|
|
190
|
+
throw new Error('Missing required option: --messages');
|
|
191
|
+
}
|
|
192
|
+
(0, shared_1.loadEnv)(options.envFile);
|
|
193
|
+
// Prompt ID may come from the flag or the env file; the flag wins.
|
|
194
|
+
const promptId = (0, shared_1.resolvePromptId)(options.promptId);
|
|
195
|
+
const baseDir = path.dirname(path.resolve(options.messagesFile));
|
|
196
|
+
const messages = (0, shared_1.buildMessages)((0, shared_1.readMessagesFile)(options.messagesFile), baseDir);
|
|
197
|
+
if (messages.length === 0) {
|
|
198
|
+
throw new Error('Messages file cannot be empty');
|
|
199
|
+
}
|
|
200
|
+
// A --system message is prepended as the first message in the conversation.
|
|
201
|
+
if (options.system !== undefined) {
|
|
202
|
+
const systemPrompt = options.system.trim();
|
|
203
|
+
if (systemPrompt === '') {
|
|
204
|
+
throw new Error('--system message cannot be empty');
|
|
205
|
+
}
|
|
206
|
+
messages.unshift({ role: 'system', content: { type: 'text', text: systemPrompt } });
|
|
207
|
+
}
|
|
208
|
+
const client = new client_1.MetrioAI({
|
|
209
|
+
apiKey: (0, shared_1.resolveApiKey)(options.apiKey),
|
|
210
|
+
baseUrl: (0, shared_1.resolveBaseUrl)(options.baseUrl),
|
|
211
|
+
});
|
|
212
|
+
const params = {
|
|
213
|
+
promptId,
|
|
214
|
+
messages,
|
|
215
|
+
tag: options.tag,
|
|
216
|
+
timezone: options.timezone,
|
|
217
|
+
variables: options.variables.length ? options.variables : undefined,
|
|
218
|
+
tags: options.tags.length ? options.tags : undefined,
|
|
219
|
+
stream: options.stream,
|
|
220
|
+
};
|
|
221
|
+
const response = options.stream
|
|
222
|
+
? await client.chatCompletion(params, (chunk) => {
|
|
223
|
+
if (chunk.chunk)
|
|
224
|
+
process.stderr.write(chunk.chunk);
|
|
225
|
+
})
|
|
226
|
+
: await client.chatCompletion(params);
|
|
227
|
+
if (options.stream)
|
|
228
|
+
process.stderr.write('\n');
|
|
229
|
+
if (options.text) {
|
|
230
|
+
process.stdout.write(`${response.response}\n`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
234
|
+
}
|
|
235
|
+
}
|