@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConsoleClient = exports.ConsoleApiError = void 0;
|
|
4
|
+
exports.resolveConsoleAuth = resolveConsoleAuth;
|
|
5
|
+
const credentials_1 = require("../auth/credentials");
|
|
6
|
+
const oauth_1 = require("../auth/oauth");
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
Object.defineProperty(exports, "ConsoleApiError", { enumerable: true, get: function () { return errors_1.ConsoleApiError; } });
|
|
9
|
+
/** Refresh this long before expiry so a request never starts with a dying token. */
|
|
10
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
11
|
+
/** After a failed refresh, how long to wait for a concurrent process to persist its rotated credentials. */
|
|
12
|
+
const ROTATION_READ_ATTEMPTS = 5;
|
|
13
|
+
const ROTATION_READ_DELAY_MS = 200;
|
|
14
|
+
/**
|
|
15
|
+
* METRIO_TOKEN (+ METRIO_CONSOLE_URL) wins over the stored login and is used
|
|
16
|
+
* as-is. Otherwise the stored login is used, refreshed first if it is about to
|
|
17
|
+
* expire.
|
|
18
|
+
*/
|
|
19
|
+
async function resolveConsoleAuth() {
|
|
20
|
+
const envToken = process.env.METRIO_TOKEN;
|
|
21
|
+
if (envToken) {
|
|
22
|
+
// Env path never reads the credentials file: a stored login may point elsewhere.
|
|
23
|
+
return {
|
|
24
|
+
consoleUrl: (0, credentials_1.normalizeConsoleUrl)(process.env.METRIO_CONSOLE_URL || credentials_1.DEFAULT_CONSOLE_URL),
|
|
25
|
+
token: envToken,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const stored = (0, credentials_1.readCredentials)();
|
|
29
|
+
if (!stored) {
|
|
30
|
+
throw new errors_1.CliError('Not logged in. Run "metrio login" (or set METRIO_TOKEN).', 3);
|
|
31
|
+
}
|
|
32
|
+
let current = stored;
|
|
33
|
+
const refresh = async () => {
|
|
34
|
+
try {
|
|
35
|
+
current = await (0, oauth_1.refreshCredentials)(current);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err instanceof errors_1.ConsoleApiError) {
|
|
39
|
+
// Refresh tokens rotate: another process may have already refreshed, but
|
|
40
|
+
// it may not have persisted the new pair yet, so re-read briefly.
|
|
41
|
+
for (let attempt = 0; attempt < ROTATION_READ_ATTEMPTS; attempt++) {
|
|
42
|
+
if (attempt > 0)
|
|
43
|
+
await new Promise((r) => setTimeout(r, ROTATION_READ_DELAY_MS));
|
|
44
|
+
const latest = (0, credentials_1.readCredentials)();
|
|
45
|
+
if (latest &&
|
|
46
|
+
latest.refresh_token !== current.refresh_token &&
|
|
47
|
+
latest.console_url === current.console_url) {
|
|
48
|
+
current = latest;
|
|
49
|
+
return current.access_token;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new errors_1.CliError('Session expired or revoked. Run "metrio login".', 3);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
(0, credentials_1.writeCredentials)(current);
|
|
57
|
+
return current.access_token;
|
|
58
|
+
};
|
|
59
|
+
if (Date.parse(current.expires_at) - EXPIRY_SKEW_MS <= Date.now()) {
|
|
60
|
+
await refresh();
|
|
61
|
+
}
|
|
62
|
+
return { consoleUrl: current.console_url, token: current.access_token, refresh };
|
|
63
|
+
}
|
|
64
|
+
class ConsoleClient {
|
|
65
|
+
auth;
|
|
66
|
+
constructor(auth) {
|
|
67
|
+
this.auth = auth;
|
|
68
|
+
}
|
|
69
|
+
async listTools() {
|
|
70
|
+
return (await this.request('GET', '/api/v1/tools'));
|
|
71
|
+
}
|
|
72
|
+
async callTool(name, args) {
|
|
73
|
+
return this.request('POST', `/api/v1/tools/${encodeURIComponent(name)}`, args ?? {});
|
|
74
|
+
}
|
|
75
|
+
/** GET a console endpoint that answers with a bare JSON body (e.g. /api/oauth/userinfo). */
|
|
76
|
+
async getJson(path) {
|
|
77
|
+
return this.send('GET', path, undefined, true);
|
|
78
|
+
}
|
|
79
|
+
async request(method, path, body) {
|
|
80
|
+
const json = (await this.send(method, path, body, true));
|
|
81
|
+
return json.data;
|
|
82
|
+
}
|
|
83
|
+
async send(method, path, body, retryOn401) {
|
|
84
|
+
const res = await fetch(`${this.auth.consoleUrl}${path}`, {
|
|
85
|
+
method,
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${this.auth.token}`,
|
|
88
|
+
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
89
|
+
},
|
|
90
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
91
|
+
});
|
|
92
|
+
if (res.status === 401 && retryOn401 && this.auth.refresh) {
|
|
93
|
+
this.auth.token = await this.auth.refresh();
|
|
94
|
+
return this.send(method, path, body, false);
|
|
95
|
+
}
|
|
96
|
+
const json = (await res.json().catch(() => ({})));
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
const error = (json.error ?? {});
|
|
99
|
+
const message = typeof json.error === 'string'
|
|
100
|
+
? String(json.error_description ?? json.error)
|
|
101
|
+
: String(error.message ?? `HTTP ${res.status}`);
|
|
102
|
+
const code = typeof json.error === 'string' ? json.error : String(error.code ?? 'http_error');
|
|
103
|
+
throw new errors_1.ConsoleApiError(message, res.status, code, error.issues);
|
|
104
|
+
}
|
|
105
|
+
return json;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.ConsoleClient = ConsoleClient;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConsoleApiError = exports.CliError = void 0;
|
|
4
|
+
/** A user-facing CLI error with the process exit code it should produce. */
|
|
5
|
+
class CliError extends Error {
|
|
6
|
+
exitCode;
|
|
7
|
+
constructor(message, exitCode = 2) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.exitCode = exitCode;
|
|
10
|
+
this.name = 'CliError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.CliError = CliError;
|
|
14
|
+
/** A non-2xx answer from the console (REST or OAuth endpoints). */
|
|
15
|
+
class ConsoleApiError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
code;
|
|
18
|
+
details;
|
|
19
|
+
constructor(message, status, code, details) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.details = details;
|
|
24
|
+
this.name = 'ConsoleApiError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.ConsoleApiError = ConsoleApiError;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.HELP = void 0;
|
|
5
|
+
exports.main = main;
|
|
6
|
+
exports.reportError = reportError;
|
|
7
|
+
/**
|
|
8
|
+
* metrio — the MetrioAI CLI.
|
|
9
|
+
*
|
|
10
|
+
* Execution commands (chat, workflow run, run-batch) call the MetrioAI API with
|
|
11
|
+
* a project API key. Management commands (tools, call) call the console's
|
|
12
|
+
* /api/v1/tools REST with an OAuth token from `metrio login`.
|
|
13
|
+
*/
|
|
14
|
+
const client_1 = require("@metrio-ai/client");
|
|
15
|
+
const errors_1 = require("./errors");
|
|
16
|
+
const auth_1 = require("./commands/auth");
|
|
17
|
+
const chat_1 = require("./commands/chat");
|
|
18
|
+
const prompt_1 = require("./commands/prompt");
|
|
19
|
+
const workflow_1 = require("./commands/workflow");
|
|
20
|
+
const tools_1 = require("./commands/tools");
|
|
21
|
+
exports.HELP = `metrio - MetrioAI CLI
|
|
22
|
+
|
|
23
|
+
Usage: metrio <command> [options]
|
|
24
|
+
|
|
25
|
+
Run (project API key, see "metrio chat --help"):
|
|
26
|
+
chat Run a chat completion
|
|
27
|
+
workflow run Run a workflow completion (metrio workflow --help)
|
|
28
|
+
workflow run-batch Run several workflow cases in one process
|
|
29
|
+
prompt run-batch Run several prompt cases in one process (metrio prompt --help)
|
|
30
|
+
prompt create-version Create a prompt version from a file
|
|
31
|
+
|
|
32
|
+
Account (console OAuth):
|
|
33
|
+
login Sign in to the Metrio console in your browser
|
|
34
|
+
logout Revoke and delete the stored login
|
|
35
|
+
whoami Show the signed-in user and scopes
|
|
36
|
+
|
|
37
|
+
Manage (console OAuth):
|
|
38
|
+
tools List the console tools your login can call
|
|
39
|
+
call <tool> Call a console tool with JSON args (-f file | --args json)
|
|
40
|
+
|
|
41
|
+
-h, --help Show this help
|
|
42
|
+
-v, --version Show the CLI version
|
|
43
|
+
`;
|
|
44
|
+
async function main(argv) {
|
|
45
|
+
const [command] = argv;
|
|
46
|
+
switch (command) {
|
|
47
|
+
case undefined:
|
|
48
|
+
case '-h':
|
|
49
|
+
case '--help':
|
|
50
|
+
process.stdout.write(exports.HELP);
|
|
51
|
+
return;
|
|
52
|
+
case '-v':
|
|
53
|
+
case '--version':
|
|
54
|
+
process.stdout.write(`${require('../package.json').version}\n`);
|
|
55
|
+
return;
|
|
56
|
+
case 'chat':
|
|
57
|
+
return (0, chat_1.runChatCommand)(argv.slice(1));
|
|
58
|
+
case 'workflow':
|
|
59
|
+
return (0, workflow_1.runWorkflowCommand)(argv.slice(1));
|
|
60
|
+
case 'prompt':
|
|
61
|
+
return (0, prompt_1.runPromptCommand)(argv.slice(1));
|
|
62
|
+
case 'login':
|
|
63
|
+
return (0, auth_1.runLoginCommand)(argv.slice(1));
|
|
64
|
+
case 'logout':
|
|
65
|
+
return (0, auth_1.runLogoutCommand)(argv.slice(1));
|
|
66
|
+
case 'whoami':
|
|
67
|
+
return (0, auth_1.runWhoamiCommand)(argv.slice(1));
|
|
68
|
+
case 'tools':
|
|
69
|
+
return (0, tools_1.runToolsCommand)(argv.slice(1));
|
|
70
|
+
case 'call':
|
|
71
|
+
return (0, tools_1.runCallCommand)(argv.slice(1));
|
|
72
|
+
default:
|
|
73
|
+
if (command.startsWith('-')) {
|
|
74
|
+
process.stderr.write('metrio: running chat without a subcommand is deprecated; use "metrio chat".\n');
|
|
75
|
+
return (0, chat_1.runChatCommand)(argv);
|
|
76
|
+
}
|
|
77
|
+
throw new errors_1.CliError(`Unknown command: ${command}. Run "metrio --help".`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Writes a friendly message for `err` to stderr and returns the exit code. */
|
|
81
|
+
function reportError(err) {
|
|
82
|
+
if (err instanceof client_1.MetrioApiError) {
|
|
83
|
+
process.stderr.write(`MetrioAI API error: ${err.message}\n`);
|
|
84
|
+
if (err.responseData) {
|
|
85
|
+
process.stderr.write(`${JSON.stringify(err.responseData, null, 2)}\n`);
|
|
86
|
+
}
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
if (err instanceof errors_1.ConsoleApiError) {
|
|
90
|
+
process.stderr.write(`Metrio console error (${err.status} ${err.code}): ${err.message}\n`);
|
|
91
|
+
if (err.details)
|
|
92
|
+
process.stderr.write(`${JSON.stringify(err.details, null, 2)}\n`);
|
|
93
|
+
if (err.status === 401 || err.status === 403)
|
|
94
|
+
return 3;
|
|
95
|
+
return err.status === 400 ? 2 : 1;
|
|
96
|
+
}
|
|
97
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
99
|
+
return err instanceof errors_1.CliError ? err.exitCode : 1;
|
|
100
|
+
}
|
|
101
|
+
if (require.main === module) {
|
|
102
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
103
|
+
process.exit(reportError(err));
|
|
104
|
+
});
|
|
105
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metrio-ai/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"packageManager": "yarn@4.14.1",
|
|
5
|
+
"description": "Metrio AI command-line interface: run prompts and workflows, manage Metrio resources",
|
|
6
|
+
"bin": {
|
|
7
|
+
"metrio": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
14
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"prepublishOnly": "yarn build"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"author": "Steven Shen <steven@jidou.ai>",
|
|
22
|
+
"license": "BSD-3-Clause",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/JidouAI/metrioai-backend.git",
|
|
26
|
+
"directory": "packages/cli"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@metrio-ai/client": "^1.5.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/jest": "^29.5.14",
|
|
36
|
+
"@types/node": "^20.10.0",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"ts-jest": "^29.2.5",
|
|
39
|
+
"typescript": "^5.6.3"
|
|
40
|
+
}
|
|
41
|
+
}
|