@vavio/cli 0.0.0 → 0.1.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/bin/vavio.mjs +4 -0
- package/dist/vavio.mjs +1206 -0
- package/package.json +36 -4
- package/README.md +0 -3
package/bin/vavio.mjs
ADDED
package/dist/vavio.mjs
ADDED
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { discoverOAuthServerInfo } from "@modelcontextprotocol/client";
|
|
7
|
+
|
|
8
|
+
// src/connection.ts
|
|
9
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
10
|
+
|
|
11
|
+
// src/constants.ts
|
|
12
|
+
var CLI_NAME = "@vavio/cli";
|
|
13
|
+
var CLI_VERSION = "0.1.0";
|
|
14
|
+
var CONTRACT_VERSION = 1;
|
|
15
|
+
var CONTRACT_HEADER = "Vavio-Contract";
|
|
16
|
+
var CONTRACT_MIN_HEADER = "Vavio-Contract-Min";
|
|
17
|
+
var DEFAULT_MCP_URL = "https://vavio.ai/api/mcp";
|
|
18
|
+
var DEFAULT_LOGIN_PORT = 47681;
|
|
19
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
20
|
+
var KEYCHAIN_SERVICE = "vavio-cli";
|
|
21
|
+
var OAUTH_CLIENT_NAME = "Vavio CLI";
|
|
22
|
+
var REVOKE_TOOL = "revoke_connection";
|
|
23
|
+
var EXIT = {
|
|
24
|
+
ok: 0,
|
|
25
|
+
refused: 1,
|
|
26
|
+
usage: 2,
|
|
27
|
+
human: 3,
|
|
28
|
+
auth: 4,
|
|
29
|
+
unavailable: 5,
|
|
30
|
+
upgrade: 6
|
|
31
|
+
};
|
|
32
|
+
var EXIT_CODE_LINES = [
|
|
33
|
+
"0 done",
|
|
34
|
+
"1 Vavio refused the request, or it failed",
|
|
35
|
+
"2 usage error: unknown command or option, missing or invalid argument",
|
|
36
|
+
"3 the owner must act: see next.text (and next.url)",
|
|
37
|
+
"4 not signed in, sign-in expired, or this connection was revoked: run vavio login",
|
|
38
|
+
"5 Vavio is unreachable or this endpoint is not available",
|
|
39
|
+
"6 this CLI is too old for the server: upgrade @vavio/cli"
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// src/errors.ts
|
|
43
|
+
var ERROR_CODES = {
|
|
44
|
+
usage: EXIT.usage,
|
|
45
|
+
not_signed_in: EXIT.auth,
|
|
46
|
+
signin_failed: EXIT.auth,
|
|
47
|
+
signin_timeout: EXIT.auth,
|
|
48
|
+
unavailable: EXIT.unavailable,
|
|
49
|
+
unreachable: EXIT.unavailable,
|
|
50
|
+
login_port_busy: EXIT.unavailable,
|
|
51
|
+
upgrade_required: EXIT.upgrade,
|
|
52
|
+
unknown_tool: EXIT.usage,
|
|
53
|
+
failed: EXIT.refused,
|
|
54
|
+
/** An MCP-level tool error without a Vavio result (for example input validation). */
|
|
55
|
+
tool_error: EXIT.refused
|
|
56
|
+
};
|
|
57
|
+
var CliError = class extends Error {
|
|
58
|
+
constructor(code, text) {
|
|
59
|
+
super(text);
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.exitCode = ERROR_CODES[code];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var usage = (text) => new CliError("usage", text);
|
|
65
|
+
var notSignedIn = (text = "Not signed in to Vavio. Run: vavio login") => new CliError("not_signed_in", text);
|
|
66
|
+
|
|
67
|
+
// src/connection.ts
|
|
68
|
+
function userAgent() {
|
|
69
|
+
return `vavio-cli/${CLI_VERSION} (node ${process.versions.node}; ${process.platform})`;
|
|
70
|
+
}
|
|
71
|
+
function contractFetch(base, mcpUrl2) {
|
|
72
|
+
const seen = { status: null, minContract: null, failure: null };
|
|
73
|
+
const fetchFn = async (input, init) => {
|
|
74
|
+
const request = input;
|
|
75
|
+
const url = new URL(typeof request === "string" ? request : request instanceof URL ? request.href : request.url);
|
|
76
|
+
const ours = url.origin === mcpUrl2.origin;
|
|
77
|
+
const headers = new Headers(init?.headers ?? (request instanceof Request ? request.headers : void 0));
|
|
78
|
+
if (!headers.has("user-agent")) headers.set("user-agent", userAgent());
|
|
79
|
+
if (ours) headers.set(CONTRACT_HEADER, String(CONTRACT_VERSION));
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await base(input, { ...init, headers });
|
|
83
|
+
} catch {
|
|
84
|
+
const failure = new CliError("unreachable", `Could not reach ${url.origin}. Check the network, then try again.`);
|
|
85
|
+
if (ours) {
|
|
86
|
+
seen.status = 0;
|
|
87
|
+
seen.failure = failure;
|
|
88
|
+
}
|
|
89
|
+
throw failure;
|
|
90
|
+
}
|
|
91
|
+
if (ours) {
|
|
92
|
+
seen.status = response.status;
|
|
93
|
+
const min = Number(response.headers.get(CONTRACT_MIN_HEADER));
|
|
94
|
+
if (Number.isInteger(min) && min > 0) seen.minContract = min;
|
|
95
|
+
if (response.status === 426 || seen.minContract !== null && seen.minContract > CONTRACT_VERSION) {
|
|
96
|
+
seen.failure = new CliError("upgrade_required", "This vavio CLI is too old for Vavio. Upgrade it: npm install -g @vavio/cli@latest");
|
|
97
|
+
throw seen.failure;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return response;
|
|
101
|
+
};
|
|
102
|
+
return { fetchFn, seen };
|
|
103
|
+
}
|
|
104
|
+
function failureOf(error, seen) {
|
|
105
|
+
for (let cause = error, depth = 0; cause && depth < 5; cause = cause.cause, depth += 1) {
|
|
106
|
+
if (cause instanceof CliError) return cause;
|
|
107
|
+
}
|
|
108
|
+
if (seen.failure) return seen.failure;
|
|
109
|
+
const status = seen.status;
|
|
110
|
+
const name = error?.name ?? "";
|
|
111
|
+
if (status === 401 || name === "UnauthorizedError") return notSignedIn("Vavio did not accept this sign-in. Run: vavio login");
|
|
112
|
+
if (status === 403) return notSignedIn("This sign-in may not use Vavio's tools. Run: vavio login");
|
|
113
|
+
if (status === 404) return new CliError("unavailable", "Vavio's AI connection is not available at this address yet.");
|
|
114
|
+
if (status === 429) return new CliError("unavailable", "Vavio is rate limiting this sign-in. Wait a minute, then try again.");
|
|
115
|
+
if (status !== null && status >= 500) return new CliError("unavailable", `Vavio answered ${status}. Try again in a minute.`);
|
|
116
|
+
if (status === 0) return new CliError("unreachable", "Could not reach Vavio. Check the network, then try again.");
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
if (/not found|unknown tool/i.test(message)) return new CliError("unknown_tool", message.slice(0, 200));
|
|
119
|
+
return new CliError("failed", `Vavio's MCP server answered with an error: ${message.slice(0, 200)}`);
|
|
120
|
+
}
|
|
121
|
+
async function openSession(input) {
|
|
122
|
+
const mode = input.era === "legacy" ? "legacy" : input.era === "modern" ? { pin: "2026-07-28" } : "auto";
|
|
123
|
+
const client = new Client({ name: "vavio-cli", version: CLI_VERSION }, { versionNegotiation: { mode } });
|
|
124
|
+
const transport = new StreamableHTTPClientTransport(input.url, { fetch: input.fetchFn, authProvider: input.auth });
|
|
125
|
+
const guard = async (run) => {
|
|
126
|
+
try {
|
|
127
|
+
return await run();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw failureOf(error, input.seen);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
await guard(() => client.connect(transport));
|
|
133
|
+
let tools = null;
|
|
134
|
+
const listTools = async () => {
|
|
135
|
+
if (!tools) {
|
|
136
|
+
const all = [];
|
|
137
|
+
let cursor;
|
|
138
|
+
do {
|
|
139
|
+
const page = await guard(() => client.listTools(cursor ? { cursor } : {}));
|
|
140
|
+
all.push(...page.tools);
|
|
141
|
+
cursor = page.nextCursor;
|
|
142
|
+
} while (cursor);
|
|
143
|
+
tools = all;
|
|
144
|
+
}
|
|
145
|
+
return tools;
|
|
146
|
+
};
|
|
147
|
+
const server = client.getServerVersion();
|
|
148
|
+
return {
|
|
149
|
+
era: client.getProtocolEra?.() === "modern" ? "modern" : "legacy",
|
|
150
|
+
protocolVersion: client.getNegotiatedProtocolVersion?.() ?? null,
|
|
151
|
+
server: server ? { name: server.name, version: server.version } : null,
|
|
152
|
+
listTools,
|
|
153
|
+
async hasTool(name) {
|
|
154
|
+
return (await listTools()).some((tool) => tool.name === name);
|
|
155
|
+
},
|
|
156
|
+
async callTool(name, args) {
|
|
157
|
+
const result = await guard(() => client.callTool({ name, arguments: args }));
|
|
158
|
+
let value = result.structuredContent;
|
|
159
|
+
const text = () => Array.isArray(result.content) ? result.content.map((part) => part && typeof part === "object" && "text" in part ? String(part.text) : "").join("") : "";
|
|
160
|
+
if (!value && result.isError) {
|
|
161
|
+
value = { outcome: "error", error: { code: "tool_error", text: text().slice(0, 500) } };
|
|
162
|
+
} else if (!value) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(text());
|
|
165
|
+
value = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { text: text() };
|
|
166
|
+
} catch {
|
|
167
|
+
value = { text: text() };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { value, isError: result.isError === true };
|
|
171
|
+
},
|
|
172
|
+
async close() {
|
|
173
|
+
await client.close().catch(() => void 0);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/oauth.ts
|
|
179
|
+
import { randomBytes } from "node:crypto";
|
|
180
|
+
import { createServer } from "node:http";
|
|
181
|
+
import {
|
|
182
|
+
auth
|
|
183
|
+
} from "@modelcontextprotocol/client";
|
|
184
|
+
function loopbackRedirect(port) {
|
|
185
|
+
return `http://127.0.0.1:${port}/callback`;
|
|
186
|
+
}
|
|
187
|
+
var CliOAuthProvider = class {
|
|
188
|
+
constructor(opts) {
|
|
189
|
+
this.opts = opts;
|
|
190
|
+
this.verifier = null;
|
|
191
|
+
this.stateValue = null;
|
|
192
|
+
/** Set by `redirectToAuthorization` in a login. */
|
|
193
|
+
this.authorizationUrl = null;
|
|
194
|
+
/** In a login: ignore the stored sign-in (it stays until the new one is saved). */
|
|
195
|
+
this.fresh = false;
|
|
196
|
+
}
|
|
197
|
+
get redirectUrl() {
|
|
198
|
+
return this.opts.redirectUri;
|
|
199
|
+
}
|
|
200
|
+
get clientMetadata() {
|
|
201
|
+
return {
|
|
202
|
+
client_name: OAUTH_CLIENT_NAME,
|
|
203
|
+
redirect_uris: [this.opts.redirectUri],
|
|
204
|
+
token_endpoint_auth_method: "none",
|
|
205
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
206
|
+
response_types: ["code"],
|
|
207
|
+
software_id: "vavio-cli",
|
|
208
|
+
software_version: CLI_VERSION
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
state() {
|
|
212
|
+
this.stateValue = randomBytes(16).toString("base64url");
|
|
213
|
+
return this.stateValue;
|
|
214
|
+
}
|
|
215
|
+
get expectedState() {
|
|
216
|
+
return this.stateValue;
|
|
217
|
+
}
|
|
218
|
+
clientInformation() {
|
|
219
|
+
const client = this.opts.config.server(this.opts.serverUrl).client;
|
|
220
|
+
if (!client?.client_id) return void 0;
|
|
221
|
+
const uris = client.redirect_uris;
|
|
222
|
+
if (Array.isArray(uris) && !uris.includes(this.opts.redirectUri)) return void 0;
|
|
223
|
+
return client;
|
|
224
|
+
}
|
|
225
|
+
saveClientInformation(info) {
|
|
226
|
+
const { client_secret: _secret, ...publicInfo } = info;
|
|
227
|
+
this.opts.config.update(this.opts.serverUrl, { client: publicInfo });
|
|
228
|
+
}
|
|
229
|
+
async signIn() {
|
|
230
|
+
if (this.cached !== void 0) return this.cached;
|
|
231
|
+
if (this.fresh) return null;
|
|
232
|
+
const raw = await this.opts.store.get(this.opts.serverUrl);
|
|
233
|
+
let parsed = null;
|
|
234
|
+
try {
|
|
235
|
+
const value = raw ? JSON.parse(raw) : null;
|
|
236
|
+
parsed = value?.tokens?.access_token ? value : null;
|
|
237
|
+
} catch {
|
|
238
|
+
parsed = null;
|
|
239
|
+
}
|
|
240
|
+
this.opts.redactor.add(parsed?.tokens.access_token);
|
|
241
|
+
this.opts.redactor.add(parsed?.tokens.refresh_token);
|
|
242
|
+
this.cached = parsed;
|
|
243
|
+
return parsed;
|
|
244
|
+
}
|
|
245
|
+
async tokens() {
|
|
246
|
+
return (await this.signIn())?.tokens;
|
|
247
|
+
}
|
|
248
|
+
async saveTokens(tokens) {
|
|
249
|
+
this.opts.redactor.add(tokens.access_token);
|
|
250
|
+
this.opts.redactor.add(tokens.refresh_token);
|
|
251
|
+
this.opts.redactor.add(tokens.id_token);
|
|
252
|
+
const value = { tokens, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
253
|
+
await this.opts.store.set(this.opts.serverUrl, JSON.stringify(value));
|
|
254
|
+
this.opts.config.update(this.opts.serverUrl, { store: this.opts.store.kind });
|
|
255
|
+
this.cached = value;
|
|
256
|
+
}
|
|
257
|
+
redirectToAuthorization(url) {
|
|
258
|
+
if (!this.opts.interactive) throw notSignedIn("Your Vavio sign-in has ended. Run: vavio login");
|
|
259
|
+
this.authorizationUrl = url;
|
|
260
|
+
}
|
|
261
|
+
saveCodeVerifier(verifier) {
|
|
262
|
+
this.opts.redactor.add(verifier);
|
|
263
|
+
this.verifier = verifier;
|
|
264
|
+
}
|
|
265
|
+
codeVerifier() {
|
|
266
|
+
if (!this.verifier) throw new CliError("signin_failed", "Sign-in lost its PKCE verifier. Run: vavio login");
|
|
267
|
+
return this.verifier;
|
|
268
|
+
}
|
|
269
|
+
saveDiscoveryState(state) {
|
|
270
|
+
this.discovery = state;
|
|
271
|
+
}
|
|
272
|
+
discoveryState() {
|
|
273
|
+
return this.discovery;
|
|
274
|
+
}
|
|
275
|
+
async invalidateCredentials(scope) {
|
|
276
|
+
if (scope === "all" || scope === "tokens") await this.forgetTokens();
|
|
277
|
+
if (scope === "all" || scope === "client") this.opts.config.update(this.opts.serverUrl, { client: void 0 });
|
|
278
|
+
if (scope === "all" || scope === "verifier") this.verifier = null;
|
|
279
|
+
if (scope === "all" || scope === "discovery") this.discovery = void 0;
|
|
280
|
+
}
|
|
281
|
+
async forgetTokens() {
|
|
282
|
+
await this.opts.store.delete(this.opts.serverUrl);
|
|
283
|
+
this.cached = null;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
var PAGE = (title, text) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;line-height:1.5"><h1 style="font-size:1.25rem">${title}</h1><p>${text}</p></body></html>`;
|
|
287
|
+
var escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
288
|
+
async function startLoopback(port) {
|
|
289
|
+
let settle = null;
|
|
290
|
+
const pending = [];
|
|
291
|
+
const server = createServer((request, response) => {
|
|
292
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
293
|
+
if (request.method !== "GET" || url.pathname !== "/callback") {
|
|
294
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("Not found");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const code = url.searchParams.get("code");
|
|
298
|
+
const error = url.searchParams.get("error");
|
|
299
|
+
const outcome = code ? { code, state: url.searchParams.get("state"), iss: url.searchParams.get("iss") } : new CliError("signin_failed", `Vavio did not sign you in (${(error ?? "no code").slice(0, 80)}). Run: vavio login`);
|
|
300
|
+
response.writeHead(code ? 200 : 400, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(
|
|
301
|
+
code ? PAGE("Signed in to Vavio", "You can close this tab and go back to the terminal.") : PAGE("Sign-in did not finish", `Vavio answered: ${escapeHtml((error ?? "no code").slice(0, 80))}. Go back to the terminal and run vavio login again.`)
|
|
302
|
+
);
|
|
303
|
+
if (settle) settle(outcome);
|
|
304
|
+
else pending.push(outcome);
|
|
305
|
+
});
|
|
306
|
+
await new Promise((resolve, reject) => {
|
|
307
|
+
server.once(
|
|
308
|
+
"error",
|
|
309
|
+
(error) => reject(
|
|
310
|
+
error.code === "EADDRINUSE" ? new CliError("login_port_busy", `Port ${port} on 127.0.0.1 is busy. Run: vavio login --port <another port>`) : new CliError("unavailable", `Could not listen on 127.0.0.1:${port} for the sign-in.`)
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
314
|
+
});
|
|
315
|
+
const address = server.address();
|
|
316
|
+
const actual = typeof address === "object" && address ? address.port : port;
|
|
317
|
+
return {
|
|
318
|
+
redirectUri: loopbackRedirect(actual),
|
|
319
|
+
wait(timeoutMs) {
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
const timer = setTimeout(() => reject(new CliError("signin_timeout", "No sign-in arrived in time. Run: vavio login")), timeoutMs);
|
|
322
|
+
timer.unref?.();
|
|
323
|
+
settle = (result) => {
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
if (result instanceof Error) reject(result);
|
|
326
|
+
else resolve(result);
|
|
327
|
+
};
|
|
328
|
+
const early = pending.shift();
|
|
329
|
+
if (early) settle(early);
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
close() {
|
|
333
|
+
server.closeAllConnections?.();
|
|
334
|
+
server.close();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function login(input) {
|
|
339
|
+
const { serverUrl, provider, fetchFn } = input;
|
|
340
|
+
provider.fresh = true;
|
|
341
|
+
const first = await auth(provider, { serverUrl, fetchFn });
|
|
342
|
+
if (first === "AUTHORIZED") return;
|
|
343
|
+
const url = provider.authorizationUrl;
|
|
344
|
+
if (!url) throw new CliError("signin_failed", "Vavio did not return a sign-in page. Run: vavio login");
|
|
345
|
+
await input.openUrl(url.href);
|
|
346
|
+
const callback = await input.loopback.wait(input.timeoutMs);
|
|
347
|
+
input.redactor.add(callback.code);
|
|
348
|
+
if (!provider.expectedState || callback.state !== provider.expectedState) {
|
|
349
|
+
throw new CliError("signin_failed", "The sign-in answer did not match this login. Run: vavio login");
|
|
350
|
+
}
|
|
351
|
+
const second = await auth(provider, { serverUrl, fetchFn, authorizationCode: callback.code, ...callback.iss ? { iss: callback.iss } : {} });
|
|
352
|
+
if (second !== "AUTHORIZED") throw new CliError("signin_failed", "Vavio did not finish the sign-in. Run: vavio login");
|
|
353
|
+
}
|
|
354
|
+
function registeredPort(client) {
|
|
355
|
+
const uris = client?.redirect_uris;
|
|
356
|
+
if (Array.isArray(uris)) {
|
|
357
|
+
for (const uri of uris) {
|
|
358
|
+
const match = typeof uri === "string" ? uri.match(/^http:\/\/127\.0\.0\.1:(\d+)\/callback$/) : null;
|
|
359
|
+
if (match && Number(match[1]) > 0) return Number(match[1]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return DEFAULT_LOGIN_PORT;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/paths.ts
|
|
366
|
+
import path from "node:path";
|
|
367
|
+
function configDir(env, platform, home) {
|
|
368
|
+
if (env.VAVIO_CONFIG_DIR) return path.resolve(env.VAVIO_CONFIG_DIR);
|
|
369
|
+
if (platform === "win32") return path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "vavio");
|
|
370
|
+
if (platform === "darwin") return path.join(home, "Library", "Application Support", "vavio");
|
|
371
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "vavio");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/redact.ts
|
|
375
|
+
var JWT = /eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]+/g;
|
|
376
|
+
var Redactor = class {
|
|
377
|
+
constructor() {
|
|
378
|
+
this.secrets = /* @__PURE__ */ new Set();
|
|
379
|
+
}
|
|
380
|
+
add(value) {
|
|
381
|
+
if (typeof value === "string" && value.length >= 6) this.secrets.add(value);
|
|
382
|
+
}
|
|
383
|
+
apply(text) {
|
|
384
|
+
let out = text;
|
|
385
|
+
for (const secret of this.secrets) out = out.split(secret).join("[redacted]");
|
|
386
|
+
return out.replace(JWT, "[redacted]");
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/store.ts
|
|
391
|
+
import { spawn } from "node:child_process";
|
|
392
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
393
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
394
|
+
import path2 from "node:path";
|
|
395
|
+
var spawnRunner = (command, args, input) => new Promise((resolve) => {
|
|
396
|
+
let child;
|
|
397
|
+
try {
|
|
398
|
+
child = spawn(command, [...args], { stdio: ["pipe", "pipe", "pipe"], shell: false, windowsHide: true });
|
|
399
|
+
} catch {
|
|
400
|
+
resolve({ code: 127, stdout: "", stderr: "spawn failed" });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let stdout = "";
|
|
404
|
+
let stderr = "";
|
|
405
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString("utf8"));
|
|
406
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString("utf8"));
|
|
407
|
+
child.on("error", () => resolve({ code: 127, stdout, stderr: stderr || "spawn failed" }));
|
|
408
|
+
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
409
|
+
child.stdin.on("error", () => void 0);
|
|
410
|
+
child.stdin.end(input ?? "");
|
|
411
|
+
});
|
|
412
|
+
function writePrivateFile(file, content) {
|
|
413
|
+
mkdirSync(path2.dirname(file), { recursive: true, mode: 448 });
|
|
414
|
+
try {
|
|
415
|
+
chmodSync(path2.dirname(file), 448);
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
const temp = `${file}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
419
|
+
writeFileSync(temp, content, { mode: 384 });
|
|
420
|
+
chmodSync(temp, 384);
|
|
421
|
+
renameSync(temp, file);
|
|
422
|
+
}
|
|
423
|
+
function readJson(file) {
|
|
424
|
+
try {
|
|
425
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
426
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
427
|
+
} catch {
|
|
428
|
+
return {};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
var FileStore = class {
|
|
432
|
+
constructor(dir) {
|
|
433
|
+
this.kind = "file";
|
|
434
|
+
this.file = path2.join(dir, "credentials.json");
|
|
435
|
+
}
|
|
436
|
+
async get(account) {
|
|
437
|
+
const value = readJson(this.file)[account];
|
|
438
|
+
return typeof value === "string" ? value : null;
|
|
439
|
+
}
|
|
440
|
+
async set(account, value) {
|
|
441
|
+
writePrivateFile(this.file, `${JSON.stringify({ ...readJson(this.file), [account]: value }, null, 2)}
|
|
442
|
+
`);
|
|
443
|
+
}
|
|
444
|
+
async delete(account) {
|
|
445
|
+
if (!existsSync(this.file)) return;
|
|
446
|
+
const rest = readJson(this.file);
|
|
447
|
+
delete rest[account];
|
|
448
|
+
if (Object.keys(rest).length === 0) rmSync(this.file, { force: true });
|
|
449
|
+
else writePrivateFile(this.file, `${JSON.stringify(rest, null, 2)}
|
|
450
|
+
`);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var KEYCHAIN_CHUNK = 3e3;
|
|
454
|
+
var MAX_CHUNKS = 16;
|
|
455
|
+
function safeArg(value) {
|
|
456
|
+
if (/["\\\r\n]/.test(value)) throw new CliError("failed", "This Vavio address cannot be used as a keychain account name.");
|
|
457
|
+
return `"${value}"`;
|
|
458
|
+
}
|
|
459
|
+
var KeychainStore = class {
|
|
460
|
+
constructor(run, keychain) {
|
|
461
|
+
this.run = run;
|
|
462
|
+
this.keychain = keychain;
|
|
463
|
+
this.kind = "keychain";
|
|
464
|
+
}
|
|
465
|
+
tail() {
|
|
466
|
+
return this.keychain ? [this.keychain] : [];
|
|
467
|
+
}
|
|
468
|
+
async read(account) {
|
|
469
|
+
const result = await this.run("/usr/bin/security", ["find-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE, "-w", ...this.tail()]);
|
|
470
|
+
return result.code === 0 ? result.stdout.replace(/\n$/, "") : null;
|
|
471
|
+
}
|
|
472
|
+
async get(account) {
|
|
473
|
+
const head = await this.read(account);
|
|
474
|
+
const match = head?.match(/^v1\.(\d+)\.([A-Za-z0-9_-]*)$/);
|
|
475
|
+
if (!match) return null;
|
|
476
|
+
const parts = [match[2]];
|
|
477
|
+
for (let i = 1; i < Number(match[1]); i += 1) {
|
|
478
|
+
const part = await this.read(`${account}#${i}`);
|
|
479
|
+
if (part === null || !/^[A-Za-z0-9_-]*$/.test(part)) return null;
|
|
480
|
+
parts.push(part);
|
|
481
|
+
}
|
|
482
|
+
return Buffer.from(parts.join(""), "base64url").toString("utf8");
|
|
483
|
+
}
|
|
484
|
+
async set(account, value) {
|
|
485
|
+
const encoded = Buffer.from(value, "utf8").toString("base64url");
|
|
486
|
+
const chunks = [];
|
|
487
|
+
for (let i = 0; i < encoded.length || i === 0; i += KEYCHAIN_CHUNK) chunks.push(encoded.slice(i, i + KEYCHAIN_CHUNK));
|
|
488
|
+
if (chunks.length > MAX_CHUNKS) throw new CliError("failed", "The sign-in is too large for the keychain. Set VAVIO_CREDENTIAL_STORE=file.");
|
|
489
|
+
const keychain = this.keychain ? ` ${safeArg(this.keychain)}` : "";
|
|
490
|
+
const lines = chunks.map((chunk, i) => {
|
|
491
|
+
const name = i === 0 ? account : `${account}#${i}`;
|
|
492
|
+
const data = i === 0 ? `v1.${chunks.length}.${chunk}` : chunk;
|
|
493
|
+
return `add-generic-password -U -a ${safeArg(name)} -s ${safeArg(KEYCHAIN_SERVICE)} -l ${safeArg(OAUTH_CLIENT_NAME)} -w ${safeArg(data)}${keychain}`;
|
|
494
|
+
});
|
|
495
|
+
const result = await this.run("/usr/bin/security", ["-i"], `${lines.join("\n")}
|
|
496
|
+
`);
|
|
497
|
+
if (result.code !== 0 || await this.read(account) === null) {
|
|
498
|
+
throw new CliError("failed", "Could not save the sign-in in the macOS keychain. Unlock it, or set VAVIO_CREDENTIAL_STORE=file.");
|
|
499
|
+
}
|
|
500
|
+
for (let i = chunks.length; i < MAX_CHUNKS; i += 1) {
|
|
501
|
+
const gone = await this.run("/usr/bin/security", ["delete-generic-password", "-a", `${account}#${i}`, "-s", KEYCHAIN_SERVICE, ...this.tail()]);
|
|
502
|
+
if (gone.code !== 0) break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async delete(account) {
|
|
506
|
+
for (let i = 0; i < MAX_CHUNKS; i += 1) {
|
|
507
|
+
const name = i === 0 ? account : `${account}#${i}`;
|
|
508
|
+
const gone = await this.run("/usr/bin/security", ["delete-generic-password", "-a", name, "-s", KEYCHAIN_SERVICE, ...this.tail()]);
|
|
509
|
+
if (gone.code !== 0 && i > 0) break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var SecretServiceStore = class {
|
|
514
|
+
constructor(run) {
|
|
515
|
+
this.run = run;
|
|
516
|
+
this.kind = "secret-service";
|
|
517
|
+
}
|
|
518
|
+
async get(account) {
|
|
519
|
+
const result = await this.run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "account", account]);
|
|
520
|
+
return result.code === 0 && result.stdout ? result.stdout : null;
|
|
521
|
+
}
|
|
522
|
+
async set(account, value) {
|
|
523
|
+
const result = await this.run("secret-tool", ["store", "--label", OAUTH_CLIENT_NAME, "service", KEYCHAIN_SERVICE, "account", account], value);
|
|
524
|
+
if (result.code !== 0) throw new CliError("failed", "Could not save the sign-in in the Secret Service keyring. Set VAVIO_CREDENTIAL_STORE=file.");
|
|
525
|
+
}
|
|
526
|
+
async delete(account) {
|
|
527
|
+
await this.run("secret-tool", ["clear", "service", KEYCHAIN_SERVICE, "account", account]);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
function storeOf(kind, choice) {
|
|
531
|
+
if (kind === "keychain") return new KeychainStore(choice.run, choice.env.VAVIO_KEYCHAIN || void 0);
|
|
532
|
+
if (kind === "secret-service") return new SecretServiceStore(choice.run);
|
|
533
|
+
return new FileStore(choice.dir);
|
|
534
|
+
}
|
|
535
|
+
async function chooseStore(choice) {
|
|
536
|
+
const wanted = (choice.env.VAVIO_CREDENTIAL_STORE ?? "auto").toLowerCase();
|
|
537
|
+
if (wanted === "file") return storeOf("file", choice);
|
|
538
|
+
if (choice.platform === "darwin" && (wanted === "keychain" || wanted === "auto")) {
|
|
539
|
+
if (existsSync("/usr/bin/security") || wanted === "keychain") return storeOf("keychain", choice);
|
|
540
|
+
}
|
|
541
|
+
if (choice.platform === "linux" && (wanted === "keychain" || wanted === "auto")) {
|
|
542
|
+
const probe = await choice.run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "account", "vavio-cli-probe"]);
|
|
543
|
+
if (probe.code !== 127 && probe.stderr.trim() === "") return storeOf("secret-service", choice);
|
|
544
|
+
if (wanted === "keychain") throw new CliError("failed", "No Secret Service keyring answered (secret-tool). Set VAVIO_CREDENTIAL_STORE=file.");
|
|
545
|
+
}
|
|
546
|
+
if (wanted === "keychain") throw new CliError("failed", "No OS keychain on this platform. Set VAVIO_CREDENTIAL_STORE=file.");
|
|
547
|
+
return storeOf("file", choice);
|
|
548
|
+
}
|
|
549
|
+
var ConfigFile = class {
|
|
550
|
+
constructor(dir) {
|
|
551
|
+
this.file = path2.join(dir, "config.json");
|
|
552
|
+
}
|
|
553
|
+
server(url) {
|
|
554
|
+
const servers = readJson(this.file).servers;
|
|
555
|
+
return servers?.[url] ?? {};
|
|
556
|
+
}
|
|
557
|
+
update(url, patch) {
|
|
558
|
+
const all = readJson(this.file);
|
|
559
|
+
const servers = { ...all.servers ?? {} };
|
|
560
|
+
if (patch === null) delete servers[url];
|
|
561
|
+
else servers[url] = { ...servers[url], ...patch };
|
|
562
|
+
writePrivateFile(this.file, `${JSON.stringify({ version: 1, servers }, null, 2)}
|
|
563
|
+
`);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// ../agent-types-shared/src/stored-uuid.ts
|
|
568
|
+
var STORED_UUID_BODY = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
569
|
+
var STORED_UUID_PATTERN = new RegExp(`^${STORED_UUID_BODY}$`, "i");
|
|
570
|
+
function isStoredUuid(value) {
|
|
571
|
+
return typeof value === "string" && STORED_UUID_PATTERN.test(value);
|
|
572
|
+
}
|
|
573
|
+
var RFC_UUID_BODY = "[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
|
|
574
|
+
var RFC_UUID_PATTERN = new RegExp(`^${RFC_UUID_BODY}$`, "i");
|
|
575
|
+
|
|
576
|
+
// src/cli.ts
|
|
577
|
+
var OPTIONS = {
|
|
578
|
+
json: { type: "boolean" },
|
|
579
|
+
url: { type: "string" },
|
|
580
|
+
help: { type: "boolean", short: "h" },
|
|
581
|
+
version: { type: "boolean", short: "v" },
|
|
582
|
+
args: { type: "string" },
|
|
583
|
+
"args-file": { type: "string" },
|
|
584
|
+
open: { type: "boolean" },
|
|
585
|
+
limit: { type: "string" },
|
|
586
|
+
before: { type: "string" },
|
|
587
|
+
"no-browser": { type: "boolean" },
|
|
588
|
+
port: { type: "string" }
|
|
589
|
+
};
|
|
590
|
+
var GLOBAL_FLAGS = ["json", "url", "help", "version"];
|
|
591
|
+
var AUTH_REFUSALS = /* @__PURE__ */ new Set(["no_grant", "other_resource", "session_ended", "grant_not_live", "operation_not_granted", "reconnect_required", "bad_actor"]);
|
|
592
|
+
var COMMANDS = {
|
|
593
|
+
login: {
|
|
594
|
+
usage: "vavio login [--no-browser] [--port <n>]",
|
|
595
|
+
summary: "Sign in to Vavio in your browser and save the sign-in on this computer.",
|
|
596
|
+
details: [
|
|
597
|
+
"Opens Vavio's consent page; approve it for one workspace. The CLI then acts only on that workspace.",
|
|
598
|
+
"--no-browser Print the sign-in address instead of opening a browser.",
|
|
599
|
+
"--port <n> Local port for the sign-in answer (default 47681).",
|
|
600
|
+
"The sign-in is saved in the OS keychain, or in a private file where there is none."
|
|
601
|
+
],
|
|
602
|
+
flags: ["no-browser", "port"]
|
|
603
|
+
},
|
|
604
|
+
logout: {
|
|
605
|
+
usage: "vavio logout",
|
|
606
|
+
summary: "Remove this computer's sign-in and revoke it on Vavio.",
|
|
607
|
+
details: ["Exit code 3 means the local sign-in is removed but Vavio could not revoke it from here: next.text says what to do."]
|
|
608
|
+
},
|
|
609
|
+
whoami: { usage: "vavio whoami", summary: "Show the signed-in workspace. Same result as vavio account." },
|
|
610
|
+
account: {
|
|
611
|
+
usage: "vavio account",
|
|
612
|
+
summary: "Read the account: workspace, Hermes instances, offerings, included Hermes and open operations."
|
|
613
|
+
},
|
|
614
|
+
"instances list": { usage: "vavio instances list", summary: "List the workspace's Hermes instances with their ids and access state." },
|
|
615
|
+
"instance status": {
|
|
616
|
+
usage: "vavio instance <instance-id> status",
|
|
617
|
+
summary: "Read one Hermes instance. The id is required: take it from vavio instances list."
|
|
618
|
+
},
|
|
619
|
+
"operations list": {
|
|
620
|
+
usage: "vavio operations list [--open] [--limit <1-50>] [--before <operation-id>]",
|
|
621
|
+
summary: "List operations, newest first.",
|
|
622
|
+
details: [
|
|
623
|
+
"--open Only operations still queued, running, waiting for the owner, or with an unknown outcome.",
|
|
624
|
+
"--before <id> Older page: pass next_before from the previous page."
|
|
625
|
+
],
|
|
626
|
+
flags: ["open", "limit", "before"]
|
|
627
|
+
},
|
|
628
|
+
"operations get": { usage: "vavio operations get <operation-id>", summary: "Read one operation: status, result or error, and the next step." },
|
|
629
|
+
offerings: { usage: "vavio offerings", summary: "List what Vavio offers and whether it can be bought now." },
|
|
630
|
+
tools: { usage: "vavio tools", summary: "List every tool Vavio's server offers, with its input schema, in server order." },
|
|
631
|
+
call: {
|
|
632
|
+
usage: "vavio call <tool> [--args '<json object>' | --args-file <path|->]",
|
|
633
|
+
summary: "Call any Vavio tool by name. Prints the tool's result.",
|
|
634
|
+
details: [
|
|
635
|
+
"--args-file - Read the JSON arguments from stdin.",
|
|
636
|
+
"Use vavio tools to see names and input schemas. Tool results follow the same exit codes as the other commands."
|
|
637
|
+
],
|
|
638
|
+
flags: ["args", "args-file"]
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
var GROUPS = {
|
|
642
|
+
instances: ["instances list"],
|
|
643
|
+
instance: ["instance status"],
|
|
644
|
+
operations: ["operations list", "operations get"]
|
|
645
|
+
};
|
|
646
|
+
function mainHelp() {
|
|
647
|
+
const width = Math.max(...Object.values(COMMANDS).map((c) => c.usage.replace(/^vavio /, "").split(" [")[0].length)) + 2;
|
|
648
|
+
const lines = Object.values(COMMANDS).map((c) => ` ${c.usage.replace(/^vavio /, "").split(" [")[0].padEnd(width)}${c.summary}`);
|
|
649
|
+
return [
|
|
650
|
+
"vavio: operate your Vavio account and Hermes instances from a terminal or an AI assistant.",
|
|
651
|
+
"",
|
|
652
|
+
"Usage: vavio <command> [options]",
|
|
653
|
+
"",
|
|
654
|
+
"Commands:",
|
|
655
|
+
...lines,
|
|
656
|
+
"",
|
|
657
|
+
"Options for every command:",
|
|
658
|
+
" --json Print exactly one JSON object on stdout, errors included.",
|
|
659
|
+
` --url <url> Vavio MCP endpoint (default ${DEFAULT_MCP_URL}, or VAVIO_URL).`,
|
|
660
|
+
" -h, --help Show help for a command: vavio <command> --help",
|
|
661
|
+
" -v, --version Print the version.",
|
|
662
|
+
"",
|
|
663
|
+
"Exit codes:",
|
|
664
|
+
...EXIT_CODE_LINES.map((line) => ` ${line}`),
|
|
665
|
+
"",
|
|
666
|
+
"Environment:",
|
|
667
|
+
" VAVIO_URL MCP endpoint, as --url.",
|
|
668
|
+
" VAVIO_CONFIG_DIR Folder for config.json and, without a keychain, credentials.json.",
|
|
669
|
+
" VAVIO_CREDENTIAL_STORE auto (default), keychain or file.",
|
|
670
|
+
" VAVIO_ACCESS_TOKEN Use this token for the run; it is never saved or refreshed.",
|
|
671
|
+
"",
|
|
672
|
+
"Never put secrets (API keys, bot tokens, passwords) in arguments. Vavio collects them on its own web pages."
|
|
673
|
+
].join("\n");
|
|
674
|
+
}
|
|
675
|
+
function commandHelp(key) {
|
|
676
|
+
const c = COMMANDS[key];
|
|
677
|
+
return [`Usage: ${c.usage}`, "", c.summary, ...c.details?.length ? ["", ...c.details] : [], "", "Also: --json, --url <url>. Exit codes: vavio --help."].join("\n");
|
|
678
|
+
}
|
|
679
|
+
var Output = class {
|
|
680
|
+
constructor(io, redactor, json) {
|
|
681
|
+
this.io = io;
|
|
682
|
+
this.redactor = redactor;
|
|
683
|
+
this.json = json;
|
|
684
|
+
}
|
|
685
|
+
out(text) {
|
|
686
|
+
this.io.stdout.write(this.redactor.apply(text.endsWith("\n") ? text : `${text}
|
|
687
|
+
`));
|
|
688
|
+
}
|
|
689
|
+
err(text) {
|
|
690
|
+
this.io.stderr.write(this.redactor.apply(text.endsWith("\n") ? text : `${text}
|
|
691
|
+
`));
|
|
692
|
+
}
|
|
693
|
+
value(value) {
|
|
694
|
+
this.out(JSON.stringify(value));
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
function nextOf(value) {
|
|
698
|
+
const next = value.next;
|
|
699
|
+
return next && typeof next === "object" ? next : null;
|
|
700
|
+
}
|
|
701
|
+
function exitForResult(value, isError) {
|
|
702
|
+
const refused = value.outcome === "refused" ? value.refused : void 0;
|
|
703
|
+
if (refused) {
|
|
704
|
+
if (AUTH_REFUSALS.has(String(refused.code))) return EXIT.auth;
|
|
705
|
+
return nextOf(value)?.kind === "human" ? EXIT.human : EXIT.refused;
|
|
706
|
+
}
|
|
707
|
+
if (isError) return EXIT.refused;
|
|
708
|
+
return nextOf(value)?.kind === "human" ? EXIT.human : EXIT.ok;
|
|
709
|
+
}
|
|
710
|
+
function nextLine(value) {
|
|
711
|
+
const next = nextOf(value);
|
|
712
|
+
if (!next) return null;
|
|
713
|
+
if (next.kind === "human") return `Next (you): ${next.text ?? ""}${next.url ? ` ${next.url}` : ""}`;
|
|
714
|
+
if (next.kind === "call") {
|
|
715
|
+
const wait = next.after_seconds ? ` after ${next.after_seconds}s` : "";
|
|
716
|
+
return `Next: ${next.tool}${wait}${next.text ? `: ${next.text}` : ""}`;
|
|
717
|
+
}
|
|
718
|
+
return next.text ? `Next: ${next.text}` : null;
|
|
719
|
+
}
|
|
720
|
+
function table(rows) {
|
|
721
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((row) => row[i].length)));
|
|
722
|
+
return rows.map((row) => row.map((cell, i) => i === row.length - 1 ? cell : cell.padEnd(widths[i])).join(" ")).join("\n");
|
|
723
|
+
}
|
|
724
|
+
var str = (value) => value === null || value === void 0 ? "-" : String(value);
|
|
725
|
+
var FORMAT = {
|
|
726
|
+
account(v) {
|
|
727
|
+
const ws = v.workspace ?? {};
|
|
728
|
+
const instances = v.instances ?? [];
|
|
729
|
+
const offerings = v.offerings ?? [];
|
|
730
|
+
const ent = v.entitlements?.hermes ?? {};
|
|
731
|
+
return [
|
|
732
|
+
`Workspace ${str(ws.name)} (${str(ws.id)})`,
|
|
733
|
+
`Role ${str(v.role)}`,
|
|
734
|
+
`Hermes ${instances.length} instance(s); included ${str(ent.used)} of ${str(ent.granted)}`,
|
|
735
|
+
...instances.map((i) => ` ${str(i.id)} ${str(i.access)} ${str(i.address)}`),
|
|
736
|
+
`Offerings ${offerings.map((o) => `${str(o.title)}: ${o.purchasable ? "for sale" : "not for sale yet"}`).join("; ") || "none"}`,
|
|
737
|
+
`Open operations ${str(v.open_operations)}`
|
|
738
|
+
].join("\n");
|
|
739
|
+
},
|
|
740
|
+
whoami(v) {
|
|
741
|
+
const ws = v.workspace ?? {};
|
|
742
|
+
return `Signed in as the ${str(v.role)} of workspace ${str(ws.name)} (${str(ws.id)}).`;
|
|
743
|
+
},
|
|
744
|
+
instances(v) {
|
|
745
|
+
const rows = (v.instances ?? []).map((i) => [str(i.id), str(i.access), str(i.lifecycle_state), str(i.address)]);
|
|
746
|
+
return rows.length ? table([["ID", "ACCESS", "STATE", "ADDRESS"], ...rows]) : "No Hermes instances in this workspace.";
|
|
747
|
+
},
|
|
748
|
+
instance(v) {
|
|
749
|
+
const i = v.instance ?? v ?? {};
|
|
750
|
+
return Object.entries(i).filter(([key]) => !["outcome", "observed_at", "source", "next"].includes(key)).map(([key, value]) => `${key.padEnd(16)}${typeof value === "object" && value !== null ? JSON.stringify(value) : str(value)}`).join("\n");
|
|
751
|
+
},
|
|
752
|
+
operations(v) {
|
|
753
|
+
const rows = (v.operations ?? []).map((o) => [str(o.id), str(o.kind), str(o.status), str(o.updated_at)]);
|
|
754
|
+
const more = v.next_before ? `
|
|
755
|
+
Older: vavio operations list --before ${str(v.next_before)}` : "";
|
|
756
|
+
return (rows.length ? table([["ID", "KIND", "STATUS", "UPDATED"], ...rows]) : "No operations.") + more;
|
|
757
|
+
},
|
|
758
|
+
operation(v) {
|
|
759
|
+
const o = v.operation ?? {};
|
|
760
|
+
const lines = [`Operation ${str(o.id)}`, `Kind ${str(o.kind)}`, `Status ${str(o.status)}`, `Updated ${str(o.updated_at)}`];
|
|
761
|
+
if (o.result) lines.push(`Result ${JSON.stringify(o.result)}`);
|
|
762
|
+
const error = o.error;
|
|
763
|
+
if (error) lines.push(`Error ${str(error.code)}: ${str(error.text)} (retry ${str(error.retry)})`);
|
|
764
|
+
const human = o.human;
|
|
765
|
+
if (human) lines.push(`Owner step ${str(human.text)}${human.url ? ` ${str(human.url)}` : ""}`);
|
|
766
|
+
return lines.join("\n");
|
|
767
|
+
},
|
|
768
|
+
offerings(v) {
|
|
769
|
+
const rows = (v.offerings ?? []).map((o) => {
|
|
770
|
+
const price = o.price;
|
|
771
|
+
return [str(o.id), str(o.title), price ? `${str(price.amount)} ${str(price.currency)}/${str(price.interval)}` : "no price set", o.purchasable ? "for sale" : `not for sale${o.reason ? `: ${str(o.reason)}` : ""}`];
|
|
772
|
+
});
|
|
773
|
+
return rows.length ? table([["ID", "TITLE", "PRICE", "STATUS"], ...rows]) : "No offerings.";
|
|
774
|
+
},
|
|
775
|
+
raw(v) {
|
|
776
|
+
return JSON.stringify(v, null, 2);
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
function mcpUrl(raw) {
|
|
780
|
+
let url;
|
|
781
|
+
try {
|
|
782
|
+
url = new URL(raw || DEFAULT_MCP_URL);
|
|
783
|
+
} catch {
|
|
784
|
+
throw usage(`Not a URL: ${raw}`);
|
|
785
|
+
}
|
|
786
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
787
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) throw usage("The Vavio URL must use https (http only for localhost).");
|
|
788
|
+
url.hash = "";
|
|
789
|
+
return url;
|
|
790
|
+
}
|
|
791
|
+
async function storeFor(ctx) {
|
|
792
|
+
const kind = ctx.config.server(ctx.key).store;
|
|
793
|
+
const choice = { env: ctx.env, platform: ctx.platform, dir: ctx.dir, run: ctx.run };
|
|
794
|
+
return kind ? storeOf(kind, choice) : chooseStore(choice);
|
|
795
|
+
}
|
|
796
|
+
function accessTokenOf(ctx) {
|
|
797
|
+
return ctx.io.accessToken ?? ctx.env.VAVIO_ACCESS_TOKEN ?? null;
|
|
798
|
+
}
|
|
799
|
+
async function connect(ctx) {
|
|
800
|
+
const era = (ctx.io.era ?? ctx.env.VAVIO_MCP_ERA) || "auto";
|
|
801
|
+
if (!["auto", "legacy", "modern"].includes(era)) throw usage("VAVIO_MCP_ERA must be auto, legacy or modern.");
|
|
802
|
+
const token = accessTokenOf(ctx);
|
|
803
|
+
if (token) {
|
|
804
|
+
ctx.redactor.add(token);
|
|
805
|
+
return openSession({ url: ctx.url, fetchFn: ctx.fetch.fetchFn, seen: ctx.fetch.seen, era, auth: { token: async () => token } });
|
|
806
|
+
}
|
|
807
|
+
const store = await storeFor(ctx);
|
|
808
|
+
const client = ctx.config.server(ctx.key).client;
|
|
809
|
+
const provider = new CliOAuthProvider({
|
|
810
|
+
serverUrl: ctx.key,
|
|
811
|
+
config: ctx.config,
|
|
812
|
+
store,
|
|
813
|
+
redactor: ctx.redactor,
|
|
814
|
+
interactive: false,
|
|
815
|
+
redirectUri: loopbackRedirect(registeredPort(client))
|
|
816
|
+
});
|
|
817
|
+
if (!await provider.signIn()) throw notSignedIn();
|
|
818
|
+
return openSession({ url: ctx.url, fetchFn: ctx.fetch.fetchFn, seen: ctx.fetch.seen, era, auth: provider });
|
|
819
|
+
}
|
|
820
|
+
async function withSession(ctx, work) {
|
|
821
|
+
const session = await connect(ctx);
|
|
822
|
+
try {
|
|
823
|
+
return await work(session);
|
|
824
|
+
} finally {
|
|
825
|
+
await session.close();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function report(ctx, value, isError, format) {
|
|
829
|
+
const code = exitForResult(value, isError);
|
|
830
|
+
if (ctx.out.json) {
|
|
831
|
+
ctx.out.value(value);
|
|
832
|
+
return code;
|
|
833
|
+
}
|
|
834
|
+
if (value.outcome === "refused") {
|
|
835
|
+
const refused = value.refused ?? {};
|
|
836
|
+
ctx.out.err(`Refused (${str(refused.code)}): ${str(refused.text)}`);
|
|
837
|
+
} else {
|
|
838
|
+
ctx.out.out(format(value));
|
|
839
|
+
}
|
|
840
|
+
const line = nextLine(value);
|
|
841
|
+
if (line && (code !== EXIT.ok || nextOf(value)?.kind === "human")) ctx.out.err(line);
|
|
842
|
+
return code;
|
|
843
|
+
}
|
|
844
|
+
async function callAndReport(ctx, tool, args, format) {
|
|
845
|
+
return withSession(ctx, async (session) => {
|
|
846
|
+
if (!await session.hasTool(tool)) throw new CliError("unavailable", `This Vavio server does not offer ${tool} yet.`);
|
|
847
|
+
const answer = await session.callTool(tool, args);
|
|
848
|
+
return report(ctx, answer.value, answer.isError, format);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function defaultOpenUrl(platform) {
|
|
852
|
+
return (url) => {
|
|
853
|
+
const [command, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
854
|
+
try {
|
|
855
|
+
const child = spawn2(command, args, { stdio: "ignore", detached: true, shell: false });
|
|
856
|
+
child.on("error", () => void 0);
|
|
857
|
+
child.unref();
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
async function readArgs(ctx) {
|
|
863
|
+
const inline = ctx.flags.args;
|
|
864
|
+
const file = ctx.flags["args-file"];
|
|
865
|
+
if (inline !== void 0 && file !== void 0) throw usage("Use --args or --args-file, not both.");
|
|
866
|
+
let text = inline;
|
|
867
|
+
if (file !== void 0) {
|
|
868
|
+
try {
|
|
869
|
+
text = file === "-" ? await (ctx.io.readStdin ?? readAllStdin)() : readFileSync2(file, "utf8");
|
|
870
|
+
} catch {
|
|
871
|
+
throw usage(`Could not read ${file === "-" ? "stdin" : file}.`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (text === void 0 || text.trim() === "") return {};
|
|
875
|
+
let parsed;
|
|
876
|
+
try {
|
|
877
|
+
parsed = JSON.parse(text);
|
|
878
|
+
} catch {
|
|
879
|
+
throw usage("The arguments are not valid JSON. Pass one JSON object, for example --args '{}'.");
|
|
880
|
+
}
|
|
881
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw usage("The arguments must be one JSON object.");
|
|
882
|
+
return parsed;
|
|
883
|
+
}
|
|
884
|
+
function readAllStdin() {
|
|
885
|
+
return new Promise((resolve, reject) => {
|
|
886
|
+
let text = "";
|
|
887
|
+
process.stdin.setEncoding("utf8");
|
|
888
|
+
process.stdin.on("data", (chunk) => text += chunk);
|
|
889
|
+
process.stdin.on("end", () => resolve(text));
|
|
890
|
+
process.stdin.on("error", reject);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
async function cmdLogin(ctx) {
|
|
894
|
+
const portFlag = ctx.flags.port;
|
|
895
|
+
const port = portFlag === void 0 ? registeredPort(ctx.config.server(ctx.key).client) : Number(portFlag);
|
|
896
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw usage("--port must be a port number.");
|
|
897
|
+
const choice = { env: ctx.env, platform: ctx.platform, dir: ctx.dir, run: ctx.run };
|
|
898
|
+
const store = await chooseStore(choice);
|
|
899
|
+
const loopback = await startLoopback(port);
|
|
900
|
+
const provider = new CliOAuthProvider({ serverUrl: ctx.key, config: ctx.config, store, redactor: ctx.redactor, interactive: true, redirectUri: loopback.redirectUri });
|
|
901
|
+
const open = ctx.flags["no-browser"] ? null : ctx.io.openUrl ?? defaultOpenUrl(ctx.platform);
|
|
902
|
+
try {
|
|
903
|
+
await login({
|
|
904
|
+
serverUrl: ctx.key,
|
|
905
|
+
provider,
|
|
906
|
+
loopback,
|
|
907
|
+
fetchFn: ctx.fetch.fetchFn,
|
|
908
|
+
redactor: ctx.redactor,
|
|
909
|
+
timeoutMs: ctx.io.loginTimeoutMs ?? LOGIN_TIMEOUT_MS,
|
|
910
|
+
openUrl: async (url) => {
|
|
911
|
+
ctx.out.err(open ? `Opening your browser to sign in to Vavio. If it does not open, go to:
|
|
912
|
+
${url}` : `To sign in to Vavio, open:
|
|
913
|
+
${url}`);
|
|
914
|
+
ctx.out.err("Waiting for you to approve in the browser...");
|
|
915
|
+
await open?.(url);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (error instanceof CliError) throw error;
|
|
920
|
+
throw new CliError("signin_failed", `Sign-in failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}. Run: vavio login`);
|
|
921
|
+
} finally {
|
|
922
|
+
loopback.close();
|
|
923
|
+
}
|
|
924
|
+
if (!await provider.signIn()) throw new CliError("signin_failed", "Vavio did not finish the sign-in. Run: vavio login");
|
|
925
|
+
const clientId = String(ctx.config.server(ctx.key).client?.client_id ?? "");
|
|
926
|
+
let workspace = null;
|
|
927
|
+
try {
|
|
928
|
+
const answer = await withSession(ctx, (session) => session.callTool("account_overview", {}));
|
|
929
|
+
if (answer.value.outcome === "ok") workspace = answer.value.workspace ?? null;
|
|
930
|
+
} catch {
|
|
931
|
+
workspace = null;
|
|
932
|
+
}
|
|
933
|
+
const value = { outcome: "ok", signed_in: true, server: ctx.key, client_id: clientId, credential_store: store.kind, workspace };
|
|
934
|
+
if (ctx.out.json) ctx.out.value(value);
|
|
935
|
+
else {
|
|
936
|
+
const where = store.kind === "file" ? "a private file" : store.kind === "keychain" ? "the macOS keychain" : "the Secret Service keyring";
|
|
937
|
+
ctx.out.out(`Signed in to Vavio${workspace ? `: workspace ${str(workspace.name)} (${str(workspace.id)})` : ""}. Saved in ${where}.`);
|
|
938
|
+
}
|
|
939
|
+
return EXIT.ok;
|
|
940
|
+
}
|
|
941
|
+
async function revokeOnServer(ctx, provider) {
|
|
942
|
+
try {
|
|
943
|
+
const viaTool = await withSession(ctx, async (session) => {
|
|
944
|
+
if (!await session.hasTool(REVOKE_TOOL)) return null;
|
|
945
|
+
const answer = await session.callTool(REVOKE_TOOL, {});
|
|
946
|
+
return answer.value.outcome === "ok" && !answer.isError;
|
|
947
|
+
});
|
|
948
|
+
if (viaTool !== null) return viaTool ? "revoked" : "failed";
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof CliError && error.exitCode === EXIT.auth) return "revoked";
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const info = await discoverOAuthServerInfo(ctx.key, { fetchFn: ctx.fetch.fetchFn });
|
|
954
|
+
const endpoint = info.authorizationServerMetadata?.revocation_endpoint;
|
|
955
|
+
const tokens = await provider.tokens();
|
|
956
|
+
const clientId = ctx.config.server(ctx.key).client?.client_id;
|
|
957
|
+
if (!endpoint || !tokens || typeof clientId !== "string") return "not_supported";
|
|
958
|
+
let ok = true;
|
|
959
|
+
for (const [token, hint] of [
|
|
960
|
+
[tokens.refresh_token, "refresh_token"],
|
|
961
|
+
[tokens.access_token, "access_token"]
|
|
962
|
+
]) {
|
|
963
|
+
if (!token) continue;
|
|
964
|
+
const response = await ctx.fetch.fetchFn(endpoint, {
|
|
965
|
+
method: "POST",
|
|
966
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
967
|
+
body: new URLSearchParams({ token, token_type_hint: hint, client_id: clientId }).toString()
|
|
968
|
+
});
|
|
969
|
+
ok &&= response.ok;
|
|
970
|
+
}
|
|
971
|
+
return ok ? "revoked" : "failed";
|
|
972
|
+
} catch {
|
|
973
|
+
return "failed";
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function cmdLogout(ctx) {
|
|
977
|
+
const store = await storeFor(ctx);
|
|
978
|
+
const provider = new CliOAuthProvider({
|
|
979
|
+
serverUrl: ctx.key,
|
|
980
|
+
config: ctx.config,
|
|
981
|
+
store,
|
|
982
|
+
redactor: ctx.redactor,
|
|
983
|
+
interactive: false,
|
|
984
|
+
redirectUri: loopbackRedirect(registeredPort(ctx.config.server(ctx.key).client))
|
|
985
|
+
});
|
|
986
|
+
if (!await provider.signIn()) {
|
|
987
|
+
const value2 = { outcome: "ok", signed_out: false, server: ctx.key, text: "Not signed in on this computer." };
|
|
988
|
+
if (ctx.out.json) ctx.out.value(value2);
|
|
989
|
+
else ctx.out.out(value2.text);
|
|
990
|
+
return EXIT.ok;
|
|
991
|
+
}
|
|
992
|
+
const revocation = await revokeOnServer(ctx, provider);
|
|
993
|
+
await provider.forgetTokens();
|
|
994
|
+
const revoked = revocation === "revoked";
|
|
995
|
+
const value = {
|
|
996
|
+
outcome: "ok",
|
|
997
|
+
signed_out: true,
|
|
998
|
+
server: ctx.key,
|
|
999
|
+
server_revoked: revoked,
|
|
1000
|
+
revocation,
|
|
1001
|
+
next: revoked ? { kind: "none", text: "Signed out, and Vavio revoked this connection." } : { kind: "human", text: "This computer is signed out, but Vavio has not revoked the connection. Remove Vavio CLI from the assistants connected to your Vavio account." }
|
|
1002
|
+
};
|
|
1003
|
+
if (ctx.out.json) ctx.out.value(value);
|
|
1004
|
+
else {
|
|
1005
|
+
ctx.out.out(revoked ? "Signed out. Vavio revoked this connection." : "Signed out on this computer.");
|
|
1006
|
+
if (!revoked) ctx.out.err(`Next (you): ${value.next.text}`);
|
|
1007
|
+
}
|
|
1008
|
+
return revoked ? EXIT.ok : EXIT.human;
|
|
1009
|
+
}
|
|
1010
|
+
async function cmdInstanceStatus(ctx, id) {
|
|
1011
|
+
if (!isStoredUuid(id)) throw usage(`Not an instance id: ${id}. Take one from vavio instances list.`);
|
|
1012
|
+
return withSession(ctx, async (session) => {
|
|
1013
|
+
if (await session.hasTool("get_instance")) {
|
|
1014
|
+
const answer2 = await session.callTool("get_instance", { instance_id: id });
|
|
1015
|
+
return report(ctx, answer2.value, answer2.isError, FORMAT.instance);
|
|
1016
|
+
}
|
|
1017
|
+
const answer = await session.callTool("list_instances", {});
|
|
1018
|
+
const value = answer.value;
|
|
1019
|
+
if (value.outcome !== "ok") return report(ctx, value, answer.isError, FORMAT.instance);
|
|
1020
|
+
const instance = (value.instances ?? []).find((item) => item.id === id);
|
|
1021
|
+
if (!instance) {
|
|
1022
|
+
return report(
|
|
1023
|
+
ctx,
|
|
1024
|
+
{
|
|
1025
|
+
outcome: "refused",
|
|
1026
|
+
refused: { code: "target_not_found", text: "No such item in this workspace." },
|
|
1027
|
+
observed_at: value.observed_at,
|
|
1028
|
+
source: "vavio",
|
|
1029
|
+
next: { kind: "call", tool: "list_instances", args: {} }
|
|
1030
|
+
},
|
|
1031
|
+
true,
|
|
1032
|
+
FORMAT.instance
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
return report(ctx, { outcome: "ok", instance, observed_at: value.observed_at, source: "vavio", next: value.next }, false, FORMAT.instance);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async function cmdOperationsList(ctx) {
|
|
1039
|
+
const args = { status: ctx.flags.open ? "open" : "all" };
|
|
1040
|
+
if (ctx.flags.limit !== void 0) {
|
|
1041
|
+
const limit = Number(ctx.flags.limit);
|
|
1042
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw usage("--limit must be a whole number from 1 to 50.");
|
|
1043
|
+
args.limit = limit;
|
|
1044
|
+
}
|
|
1045
|
+
if (ctx.flags.before !== void 0) {
|
|
1046
|
+
if (!isStoredUuid(ctx.flags.before)) throw usage("--before must be an operation id (next_before from the previous page).");
|
|
1047
|
+
args.before = ctx.flags.before;
|
|
1048
|
+
}
|
|
1049
|
+
return callAndReport(ctx, "list_operations", args, FORMAT.operations);
|
|
1050
|
+
}
|
|
1051
|
+
async function cmdTools(ctx) {
|
|
1052
|
+
return withSession(ctx, async (session) => {
|
|
1053
|
+
const tools = await session.listTools();
|
|
1054
|
+
const value = { outcome: "ok", tools, era: session.era, protocol_version: session.protocolVersion, server: session.server };
|
|
1055
|
+
if (ctx.out.json) ctx.out.value(value);
|
|
1056
|
+
else ctx.out.out(tools.map((tool) => `${tool.name}
|
|
1057
|
+
${tool.description ?? ""}`).join("\n"));
|
|
1058
|
+
return EXIT.ok;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
async function cmdCall(ctx, tool) {
|
|
1062
|
+
const args = await readArgs(ctx);
|
|
1063
|
+
return withSession(ctx, async (session) => {
|
|
1064
|
+
if (!await session.hasTool(tool)) throw new CliError("unknown_tool", `Vavio offers no tool named ${tool}. Run: vavio tools`);
|
|
1065
|
+
const answer = await session.callTool(tool, args);
|
|
1066
|
+
return report(ctx, answer.value, answer.isError, FORMAT.raw);
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function route(positionals) {
|
|
1070
|
+
const [first, second, third, ...rest] = positionals;
|
|
1071
|
+
const exact = (n) => {
|
|
1072
|
+
if (positionals.length > n) throw usage(`Unexpected argument: ${positionals[n]}. See vavio --help.`);
|
|
1073
|
+
};
|
|
1074
|
+
switch (first) {
|
|
1075
|
+
case "login":
|
|
1076
|
+
exact(1);
|
|
1077
|
+
return { key: "login", run: cmdLogin };
|
|
1078
|
+
case "logout":
|
|
1079
|
+
exact(1);
|
|
1080
|
+
return { key: "logout", run: cmdLogout };
|
|
1081
|
+
case "whoami":
|
|
1082
|
+
exact(1);
|
|
1083
|
+
return { key: "whoami", run: (ctx) => callAndReport(ctx, "account_overview", {}, FORMAT.whoami) };
|
|
1084
|
+
case "account":
|
|
1085
|
+
exact(1);
|
|
1086
|
+
return { key: "account", run: (ctx) => callAndReport(ctx, "account_overview", {}, FORMAT.account) };
|
|
1087
|
+
case "offerings":
|
|
1088
|
+
exact(1);
|
|
1089
|
+
return { key: "offerings", run: (ctx) => callAndReport(ctx, "list_offerings", {}, FORMAT.offerings) };
|
|
1090
|
+
case "tools":
|
|
1091
|
+
exact(1);
|
|
1092
|
+
return { key: "tools", run: cmdTools };
|
|
1093
|
+
case "instances":
|
|
1094
|
+
if (second !== void 0 && second !== "list") throw usage(`Unknown command: instances ${second}. See vavio instances --help.`);
|
|
1095
|
+
exact(2);
|
|
1096
|
+
return { key: "instances list", run: (ctx) => callAndReport(ctx, "list_instances", {}, FORMAT.instances) };
|
|
1097
|
+
case "instance":
|
|
1098
|
+
if (second === void 0) throw usage("vavio instance needs an explicit instance id: vavio instance <instance-id> status. Take the id from vavio instances list.");
|
|
1099
|
+
if (third !== "status") throw usage(`Unknown command: instance <id> ${third ?? ""}. Use: vavio instance <instance-id> status`.trim());
|
|
1100
|
+
exact(3);
|
|
1101
|
+
return { key: "instance status", run: (ctx) => cmdInstanceStatus(ctx, second) };
|
|
1102
|
+
case "operations":
|
|
1103
|
+
if (second === "list") {
|
|
1104
|
+
exact(2);
|
|
1105
|
+
return { key: "operations list", run: cmdOperationsList };
|
|
1106
|
+
}
|
|
1107
|
+
if (second === "get") {
|
|
1108
|
+
if (third === void 0) throw usage("vavio operations get needs an operation id.");
|
|
1109
|
+
if (rest.length) exact(3);
|
|
1110
|
+
if (!isStoredUuid(third)) throw usage(`Not an operation id: ${third}.`);
|
|
1111
|
+
return { key: "operations get", run: (ctx) => callAndReport(ctx, "get_operation", { operation_id: third }, FORMAT.operation) };
|
|
1112
|
+
}
|
|
1113
|
+
throw usage(`Unknown command: operations ${second ?? ""}. Use: vavio operations list | vavio operations get <id>`.trim());
|
|
1114
|
+
case "call":
|
|
1115
|
+
if (second === void 0) throw usage("vavio call needs a tool name. Run vavio tools to see them.");
|
|
1116
|
+
exact(2);
|
|
1117
|
+
return { key: "call", run: (ctx) => cmdCall(ctx, second) };
|
|
1118
|
+
default:
|
|
1119
|
+
throw usage(`Unknown command: ${first}. See vavio --help.`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function helpFor(positionals) {
|
|
1123
|
+
const words = positionals[0] === "help" ? positionals.slice(1) : positionals;
|
|
1124
|
+
if (words.length === 0) return mainHelp();
|
|
1125
|
+
const [first, second] = words;
|
|
1126
|
+
const key = first === "instance" ? "instance status" : second && COMMANDS[`${first} ${second}`] ? `${first} ${second}` : first;
|
|
1127
|
+
if (COMMANDS[key]) return commandHelp(key);
|
|
1128
|
+
if (GROUPS[first]) return GROUPS[first].map(commandHelp).join("\n\n");
|
|
1129
|
+
throw usage(`Unknown command: ${first}. See vavio --help.`);
|
|
1130
|
+
}
|
|
1131
|
+
async function runCli(argv, io = {}) {
|
|
1132
|
+
const stdout = io.stdout ?? process.stdout;
|
|
1133
|
+
const stderr = io.stderr ?? process.stderr;
|
|
1134
|
+
const env = io.env ?? process.env;
|
|
1135
|
+
const redactor = new Redactor();
|
|
1136
|
+
let json = argv.includes("--json");
|
|
1137
|
+
const out = () => new Output({ stdout, stderr }, redactor, json);
|
|
1138
|
+
try {
|
|
1139
|
+
let parsed;
|
|
1140
|
+
try {
|
|
1141
|
+
parsed = parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true });
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
throw usage(`${error.message.split("\n")[0]} See vavio --help.`);
|
|
1144
|
+
}
|
|
1145
|
+
const flags = parsed.values;
|
|
1146
|
+
json = flags.json === true;
|
|
1147
|
+
const positionals = parsed.positionals;
|
|
1148
|
+
if (flags.version) {
|
|
1149
|
+
if (json) out().value({ name: CLI_NAME, version: CLI_VERSION, contract: CONTRACT_VERSION });
|
|
1150
|
+
else out().out(`${CLI_NAME} ${CLI_VERSION} (contract ${CONTRACT_VERSION})`);
|
|
1151
|
+
return EXIT.ok;
|
|
1152
|
+
}
|
|
1153
|
+
if (flags.help || positionals.length === 0 || positionals[0] === "help") {
|
|
1154
|
+
const text = helpFor(positionals);
|
|
1155
|
+
if (json) out().value({ outcome: "ok", help: text });
|
|
1156
|
+
else out().out(text);
|
|
1157
|
+
return EXIT.ok;
|
|
1158
|
+
}
|
|
1159
|
+
const target = route(positionals);
|
|
1160
|
+
const allowed = /* @__PURE__ */ new Set([...GLOBAL_FLAGS, ...COMMANDS[target.key].flags ?? []]);
|
|
1161
|
+
for (const name of Object.keys(flags)) {
|
|
1162
|
+
if (!allowed.has(name)) throw usage(`--${name} does not apply to vavio ${target.key}. See vavio ${target.key.split(" ")[0]} --help.`);
|
|
1163
|
+
}
|
|
1164
|
+
const url = mcpUrl(flags.url ?? env.VAVIO_URL);
|
|
1165
|
+
const platform = io.platform ?? process.platform;
|
|
1166
|
+
const dir = configDir(env, platform, io.home ?? os.homedir());
|
|
1167
|
+
const ctx = {
|
|
1168
|
+
io: { ...io, stdout, stderr },
|
|
1169
|
+
env,
|
|
1170
|
+
flags,
|
|
1171
|
+
out: out(),
|
|
1172
|
+
redactor,
|
|
1173
|
+
url,
|
|
1174
|
+
key: url.href,
|
|
1175
|
+
dir,
|
|
1176
|
+
config: new ConfigFile(dir),
|
|
1177
|
+
run: io.run ?? spawnRunner,
|
|
1178
|
+
platform,
|
|
1179
|
+
fetch: contractFetch(io.fetch ?? globalThis.fetch, url)
|
|
1180
|
+
};
|
|
1181
|
+
return await target.run(ctx);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
const failure = error instanceof CliError ? error : new CliError("failed", `Unexpected error: ${(error instanceof Error ? error.message : String(error)).slice(0, 200)}`);
|
|
1184
|
+
if (json) out().value({ outcome: "error", error: { code: failure.code, text: failure.message }, exit_code: failure.exitCode });
|
|
1185
|
+
else out().err(`vavio: ${failure.message}`);
|
|
1186
|
+
return failure.exitCode;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
async function main() {
|
|
1190
|
+
return runCli(process.argv.slice(2));
|
|
1191
|
+
}
|
|
1192
|
+
export {
|
|
1193
|
+
CLI_NAME,
|
|
1194
|
+
CLI_VERSION,
|
|
1195
|
+
CONTRACT_HEADER,
|
|
1196
|
+
CONTRACT_MIN_HEADER,
|
|
1197
|
+
CONTRACT_VERSION,
|
|
1198
|
+
DEFAULT_MCP_URL,
|
|
1199
|
+
ERROR_CODES,
|
|
1200
|
+
EXIT,
|
|
1201
|
+
EXIT_CODE_LINES,
|
|
1202
|
+
REVOKE_TOOL,
|
|
1203
|
+
exitForResult,
|
|
1204
|
+
main,
|
|
1205
|
+
runCli
|
|
1206
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vavio/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"homepage": "https://vavio.ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "vavio: operate your Vavio account and Hermes instances from a terminal or your own AI assistant, over Vavio's MCP server.",
|
|
6
5
|
"license": "UNLICENSED",
|
|
7
|
-
"
|
|
6
|
+
"homepage": "https://vavio.ai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Vavio-AI/vavio.git",
|
|
10
|
+
"directory": "packages/vavio-cli"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"vavio": "bin/vavio.mjs"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/vavio.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node build.mjs",
|
|
28
|
+
"test:clean-install": "node scripts/clean-install.mjs"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/client": "2.4.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"esbuild": "0.27.4"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": false
|
|
39
|
+
}
|
|
8
40
|
}
|
package/README.md
DELETED