@vellumai/cli 0.8.7 → 0.8.8
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/node_modules/@vellumai/local-mode/package.json +2 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +15 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +15 -8
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +42 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +22 -4
- package/node_modules/@vellumai/local-mode/src/index.ts +26 -4
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +9 -7
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/clean.test.ts +179 -0
- package/src/__tests__/client-token.test.ts +87 -0
- package/src/__tests__/client-tui-refresh.test.ts +170 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
- package/src/__tests__/connect-import.test.ts +317 -0
- package/src/__tests__/devices.test.ts +272 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/commands/client.ts +115 -26
- package/src/commands/connect/import.ts +217 -0
- package/src/commands/connect.ts +31 -0
- package/src/commands/devices.ts +247 -0
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +16 -0
- package/src/commands/retire.ts +20 -47
- package/src/commands/sleep.ts +7 -0
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/wake.ts +7 -0
- package/src/components/DefaultMainScreen.tsx +84 -13
- package/src/index.ts +16 -0
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +12 -0
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +25 -1
- package/src/lib/environments/resolve.ts +9 -30
- package/src/lib/guardian-token.ts +120 -4
- package/src/lib/local.ts +20 -6
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vellum pair [assistant] [--label <name>]`
|
|
3
|
+
*
|
|
4
|
+
* Mint a device-scoped token for another machine and print a pairing bundle.
|
|
5
|
+
* Runs on the machine hosting the assistant: it calls the local gateway's
|
|
6
|
+
* loopback-only `POST /v1/pair` (cli interface) with a freshly generated
|
|
7
|
+
* deviceId, then prints the credentials to hand to a second device.
|
|
8
|
+
*
|
|
9
|
+
* Each invocation generates a NEW random deviceId, so each pairing is an
|
|
10
|
+
* independent, separately-revocable device (see `vellum unpair`, forthcoming).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { nanoid } from "nanoid";
|
|
14
|
+
|
|
15
|
+
import { extractFlag } from "../lib/arg-utils.js";
|
|
16
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
17
|
+
import {
|
|
18
|
+
formatAssistantLookupError,
|
|
19
|
+
lookupAssistantByIdentifier,
|
|
20
|
+
resolveAssistant,
|
|
21
|
+
type AssistantEntry,
|
|
22
|
+
} from "../lib/assistant-config.js";
|
|
23
|
+
import {
|
|
24
|
+
CLI_INTERFACE_ID,
|
|
25
|
+
getClientRegistrationHeaders,
|
|
26
|
+
} from "../lib/client-identity.js";
|
|
27
|
+
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
28
|
+
import { getLocalLanIPv4 } from "../lib/local.js";
|
|
29
|
+
|
|
30
|
+
function isLoopbackHost(url: string): boolean {
|
|
31
|
+
try {
|
|
32
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
33
|
+
return host === "localhost" || host === "::1" || host.startsWith("127.");
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printUsage(): void {
|
|
40
|
+
console.log(`vellum pair - Mint a device-scoped token for another machine
|
|
41
|
+
|
|
42
|
+
USAGE:
|
|
43
|
+
vellum pair [assistant] [options]
|
|
44
|
+
|
|
45
|
+
ARGUMENTS:
|
|
46
|
+
[assistant] Instance name (default: active assistant)
|
|
47
|
+
|
|
48
|
+
OPTIONS:
|
|
49
|
+
--url <url> Reachable gateway URL to advertise in the bundle
|
|
50
|
+
(default: the assistant's runtime URL, not loopback)
|
|
51
|
+
--label <name> Human label for this pairing (echoed in the output)
|
|
52
|
+
--json Output the raw bundle as JSON
|
|
53
|
+
|
|
54
|
+
EXAMPLES:
|
|
55
|
+
vellum pair
|
|
56
|
+
vellum pair "My Assistant" --label "phone"
|
|
57
|
+
vellum pair --url https://abc123.ngrok.app
|
|
58
|
+
vellum pair --json
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface PairResponse {
|
|
63
|
+
token: string;
|
|
64
|
+
expiresAt: string;
|
|
65
|
+
guardianId: string;
|
|
66
|
+
assistantId: string;
|
|
67
|
+
// Present on the device-bound path: a long-lived refresh credential the
|
|
68
|
+
// imported client uses to renew its access token (ISO-8601 strings).
|
|
69
|
+
refreshToken?: string;
|
|
70
|
+
refreshTokenExpiresAt?: string;
|
|
71
|
+
refreshAfter?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function pair(): Promise<void> {
|
|
75
|
+
const rawArgs = process.argv.slice(3);
|
|
76
|
+
|
|
77
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
78
|
+
printUsage();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const jsonOutput = rawArgs.includes("--json");
|
|
83
|
+
let args = rawArgs.filter((a) => a !== "--json");
|
|
84
|
+
|
|
85
|
+
const [label, afterLabel] = extractFlag(args, "--label");
|
|
86
|
+
const [urlOverride, afterUrl] = extractFlag(afterLabel, "--url");
|
|
87
|
+
args = afterUrl;
|
|
88
|
+
|
|
89
|
+
// Resolve the target. An explicit argument is matched by display name OR id
|
|
90
|
+
// (with the standard ambiguity error); no argument falls back to the active
|
|
91
|
+
// assistant. Join positional tokens so multi-word display names work even
|
|
92
|
+
// unquoted (e.g. `vellum pair My Assistant`).
|
|
93
|
+
const assistantName = parseAssistantTargetArg(args);
|
|
94
|
+
let entry: AssistantEntry | null;
|
|
95
|
+
if (assistantName) {
|
|
96
|
+
const result = lookupAssistantByIdentifier(assistantName);
|
|
97
|
+
if (result.status !== "found") {
|
|
98
|
+
console.error(formatAssistantLookupError(assistantName, result));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
entry = result.entry;
|
|
102
|
+
} else {
|
|
103
|
+
entry = resolveAssistant();
|
|
104
|
+
if (!entry) {
|
|
105
|
+
console.error("No assistant instance found. Run `vellum hatch` first.");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Mint over loopback (localUrl avoids mDNS for same-machine calls), but
|
|
111
|
+
// advertise a REACHABLE url in the bundle — the loopback url would point the
|
|
112
|
+
// other machine at its own localhost. Prefer an explicit --url, then the
|
|
113
|
+
// runtime (LAN/tunnel) url.
|
|
114
|
+
const mintUrl = (
|
|
115
|
+
entry.localUrl ||
|
|
116
|
+
entry.runtimeUrl ||
|
|
117
|
+
`http://127.0.0.1:${GATEWAY_PORT}`
|
|
118
|
+
).replace(/\/+$/, "");
|
|
119
|
+
const advertisedUrl = (urlOverride || entry.runtimeUrl || mintUrl).replace(
|
|
120
|
+
/\/+$/,
|
|
121
|
+
"",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// A local hatch's runtimeUrl is itself loopback (http://localhost:<port>),
|
|
125
|
+
// so without an explicit --url the bundle would point the other machine at
|
|
126
|
+
// its own localhost. Refuse to advertise a loopback URL unless the user
|
|
127
|
+
// explicitly passed one. (An explicit --url is trusted as-is.)
|
|
128
|
+
if (!urlOverride && isLoopbackHost(advertisedUrl)) {
|
|
129
|
+
const lan = getLocalLanIPv4();
|
|
130
|
+
// Use THIS assistant's gateway port (not the global default) — second
|
|
131
|
+
// local instances listen on a different port.
|
|
132
|
+
let port = String(GATEWAY_PORT);
|
|
133
|
+
try {
|
|
134
|
+
port = new URL(mintUrl).port || port;
|
|
135
|
+
} catch {
|
|
136
|
+
/* keep default */
|
|
137
|
+
}
|
|
138
|
+
const suggestion = lan
|
|
139
|
+
? `http://${lan}:${port}`
|
|
140
|
+
: `http://<this-machine-ip>:${port}`;
|
|
141
|
+
console.error(
|
|
142
|
+
"Error: this assistant has no reachable gateway URL — its address is " +
|
|
143
|
+
`loopback (${advertisedUrl}), which the other machine can't connect to.`,
|
|
144
|
+
);
|
|
145
|
+
console.error(
|
|
146
|
+
`Re-run with a reachable URL, e.g.:\n vellum pair --url ${suggestion}`,
|
|
147
|
+
);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fresh per-pairing device identity — each `vellum pair` is independently
|
|
152
|
+
// revocable.
|
|
153
|
+
const deviceId = nanoid();
|
|
154
|
+
|
|
155
|
+
let response: Response;
|
|
156
|
+
try {
|
|
157
|
+
response = await fetch(`${mintUrl}/v1/pair`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
...getClientRegistrationHeaders(CLI_INTERFACE_ID),
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({ deviceId, platform: "cli" }),
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error(
|
|
167
|
+
`Error: could not reach the gateway at ${mintUrl} ` +
|
|
168
|
+
`(${err instanceof Error ? err.message : String(err)}).`,
|
|
169
|
+
);
|
|
170
|
+
console.error("Is the assistant running? Try `vellum wake`.");
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
const body = await response.text().catch(() => "");
|
|
176
|
+
console.error(
|
|
177
|
+
`Error: HTTP ${response.status}: ${body || response.statusText}`,
|
|
178
|
+
);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = (await response.json()) as PairResponse;
|
|
183
|
+
|
|
184
|
+
// Single-line, copy-pasteable blob for the consume side (`vellum connect
|
|
185
|
+
// import <blob>`, forthcoming).
|
|
186
|
+
const bundle = {
|
|
187
|
+
gatewayUrl: advertisedUrl,
|
|
188
|
+
assistantId: result.assistantId,
|
|
189
|
+
token: result.token,
|
|
190
|
+
deviceId,
|
|
191
|
+
// Carry the refresh credential through when the gateway issued one, so the
|
|
192
|
+
// imported client can renew without re-pairing. Omitted entirely for an
|
|
193
|
+
// access-only (older gateway) response so the bundle stays clean.
|
|
194
|
+
...(result.refreshToken
|
|
195
|
+
? {
|
|
196
|
+
refreshToken: result.refreshToken,
|
|
197
|
+
refreshTokenExpiresAt: result.refreshTokenExpiresAt,
|
|
198
|
+
refreshAfter: result.refreshAfter,
|
|
199
|
+
}
|
|
200
|
+
: {}),
|
|
201
|
+
};
|
|
202
|
+
const blob = Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
203
|
+
|
|
204
|
+
if (jsonOutput) {
|
|
205
|
+
console.log(
|
|
206
|
+
JSON.stringify({ ...bundle, expiresAt: result.expiresAt }, null, 2),
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const displayName = entry.name || entry.assistantName || entry.assistantId;
|
|
212
|
+
console.log(`Paired ${label ? `"${label}" ` : ""}with ${displayName}.`);
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log(` Gateway: ${advertisedUrl}`);
|
|
215
|
+
console.log(` Assistant: ${result.assistantId}`);
|
|
216
|
+
console.log(` Expires: ${result.expiresAt}`);
|
|
217
|
+
console.log("");
|
|
218
|
+
console.log("Hand this to the other machine (keep it secret):");
|
|
219
|
+
console.log("");
|
|
220
|
+
console.log(` ${blob}`);
|
|
221
|
+
console.log("");
|
|
222
|
+
}
|
package/src/commands/ps.ts
CHANGED
|
@@ -444,6 +444,22 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
444
444
|
return;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
if (cloud === "paired") {
|
|
448
|
+
// A remote assistant paired from another machine: no local process to
|
|
449
|
+
// list — probe the remote gateway's health over the bearer token instead.
|
|
450
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
451
|
+
const health = await checkHealth(entry.runtimeUrl, token);
|
|
452
|
+
const rows: TableRow[] = [
|
|
453
|
+
{
|
|
454
|
+
name: "gateway",
|
|
455
|
+
status: withStatusEmoji(health.status),
|
|
456
|
+
info: entry.runtimeUrl + (health.detail ? ` | ${health.detail}` : ""),
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
printTable(rows);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
447
463
|
let output: string;
|
|
448
464
|
try {
|
|
449
465
|
if (cloud === "gcp") {
|
package/src/commands/retire.ts
CHANGED
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
type AssistantEntry,
|
|
14
14
|
} from "../lib/assistant-config.js";
|
|
15
15
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
16
|
+
import {
|
|
17
|
+
canPromptForConfirmation,
|
|
18
|
+
confirmAction,
|
|
19
|
+
} from "../lib/confirm-action.js";
|
|
16
20
|
import { getConfigDir } from "../lib/environments/paths.js";
|
|
17
21
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
18
22
|
import {
|
|
@@ -152,51 +156,6 @@ function printRetireTarget(entry: AssistantEntry, cloud: string): void {
|
|
|
152
156
|
console.log("");
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
function canPromptForRetireConfirmation(): boolean {
|
|
156
|
-
return (
|
|
157
|
-
process.stdin.isTTY === true &&
|
|
158
|
-
process.stdout.isTTY === true &&
|
|
159
|
-
typeof process.stdin.setRawMode === "function"
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function confirmRetireInteractive(): Promise<boolean> {
|
|
164
|
-
const stdin = process.stdin;
|
|
165
|
-
const stdout = process.stdout;
|
|
166
|
-
const wasRaw = stdin.isRaw === true;
|
|
167
|
-
const wasPaused = stdin.isPaused();
|
|
168
|
-
|
|
169
|
-
stdout.write("Press Enter to retire, or Esc/q to cancel: ");
|
|
170
|
-
stdin.setRawMode(true);
|
|
171
|
-
stdin.resume();
|
|
172
|
-
|
|
173
|
-
return await new Promise<boolean>((resolve) => {
|
|
174
|
-
const cleanup = () => {
|
|
175
|
-
stdin.off("data", onData);
|
|
176
|
-
stdin.setRawMode(wasRaw);
|
|
177
|
-
if (wasPaused) {
|
|
178
|
-
stdin.pause();
|
|
179
|
-
}
|
|
180
|
-
stdout.write("\n");
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const onData = (chunk: Buffer) => {
|
|
184
|
-
const byte = chunk[0];
|
|
185
|
-
if (byte === 13 || byte === 10) {
|
|
186
|
-
cleanup();
|
|
187
|
-
resolve(true);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (byte === 27 || byte === 3 || byte === 113 || byte === 81) {
|
|
191
|
-
cleanup();
|
|
192
|
-
resolve(false);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
stdin.on("data", onData);
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
159
|
/** Patch console methods to also append output to the given log file descriptor. */
|
|
201
160
|
function teeConsoleToLogFile(fd: number | "ignore"): void {
|
|
202
161
|
if (fd === "ignore") return;
|
|
@@ -290,10 +249,22 @@ async function retireInner(): Promise<void> {
|
|
|
290
249
|
const assistantId = entry.assistantId;
|
|
291
250
|
const source = parsed.source;
|
|
292
251
|
const cloud = resolveCloud(entry);
|
|
252
|
+
|
|
253
|
+
if (cloud === "paired") {
|
|
254
|
+
// A remote assistant paired from another machine. Retiring tears the
|
|
255
|
+
// assistant down — that can only happen on its host machine, never from a
|
|
256
|
+
// paired machine, which holds nothing but a pairing record. (Removing that
|
|
257
|
+
// local record is `vellum unpair`'s job, not retire's.)
|
|
258
|
+
console.error(
|
|
259
|
+
`Error: '${assistantId}' is a remote assistant paired from another machine — it can't be retired from here. Retiring tears down the assistant, which can only be done on its host machine. To remove the local pairing record on this machine, run \`vellum unpair ${assistantId}\`.`,
|
|
260
|
+
);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
293
264
|
printRetireTarget(entry, cloud);
|
|
294
265
|
|
|
295
266
|
if (!parsed.yes) {
|
|
296
|
-
if (!
|
|
267
|
+
if (!canPromptForConfirmation()) {
|
|
297
268
|
console.error(
|
|
298
269
|
"Error: Refusing to retire without confirmation in a non-interactive terminal.",
|
|
299
270
|
);
|
|
@@ -301,7 +272,9 @@ async function retireInner(): Promise<void> {
|
|
|
301
272
|
process.exit(1);
|
|
302
273
|
}
|
|
303
274
|
|
|
304
|
-
const confirmed = await
|
|
275
|
+
const confirmed = await confirmAction(
|
|
276
|
+
"Press Enter to retire, or Esc/q to cancel: ",
|
|
277
|
+
);
|
|
305
278
|
if (!confirmed) {
|
|
306
279
|
console.log("Retire cancelled.");
|
|
307
280
|
process.exit(1);
|
package/src/commands/sleep.ts
CHANGED
|
@@ -82,6 +82,13 @@ export async function sleep(): Promise<void> {
|
|
|
82
82
|
process.exit(1);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (entry.cloud === "paired") {
|
|
86
|
+
console.error(
|
|
87
|
+
`Error: '${entry.assistantId}' is a remote assistant paired from another machine — its lifecycle is managed on its host machine, not here. Use \`vellum client ${entry.assistantId}\` to chat with it.`,
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
if (entry.cloud && entry.cloud !== "local") {
|
|
86
93
|
console.error(
|
|
87
94
|
`Error: 'vellum sleep' only works with local and docker assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
|
|
1
3
|
import { resolveAssistant } from "../lib/assistant-config";
|
|
4
|
+
import { runCloudflareTunnel } from "../lib/cloudflare-tunnel.js";
|
|
2
5
|
import { runNgrokTunnel } from "../lib/ngrok";
|
|
3
6
|
|
|
4
7
|
const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
|
|
@@ -21,17 +24,45 @@ function parseArgs(): TunnelArgs {
|
|
|
21
24
|
if (arg === "--help" || arg === "-h") {
|
|
22
25
|
console.log("Usage: vellum tunnel [<name>] [options]");
|
|
23
26
|
console.log("");
|
|
24
|
-
console.log(
|
|
27
|
+
console.log(
|
|
28
|
+
"Expose a locally running assistant to the internet via a tunnel.",
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
"The public URL is saved to the workspace config as the ingress base URL,",
|
|
32
|
+
);
|
|
33
|
+
console.log(
|
|
34
|
+
"enabling webhook integrations (Telegram, Twilio, etc.) to reach the assistant.",
|
|
35
|
+
);
|
|
25
36
|
console.log("");
|
|
26
37
|
console.log("Arguments:");
|
|
27
38
|
console.log(
|
|
28
|
-
" <name> Name of the assistant (defaults to
|
|
39
|
+
" <name> Name of the assistant (defaults to active or only local)",
|
|
29
40
|
);
|
|
30
41
|
console.log("");
|
|
31
42
|
console.log("Options:");
|
|
32
43
|
console.log(
|
|
33
44
|
` --provider <provider> Tunnel provider: ${VALID_PROVIDERS.join(", ")} (default: ${DEFAULT_PROVIDER})`,
|
|
34
45
|
);
|
|
46
|
+
console.log("");
|
|
47
|
+
console.log("Providers:");
|
|
48
|
+
console.log(
|
|
49
|
+
" vellum Managed tunnel via Vellum Cloud (default; requires account)",
|
|
50
|
+
);
|
|
51
|
+
console.log(
|
|
52
|
+
" ngrok ngrok tunnel — install: brew install ngrok/ngrok/ngrok",
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
" cloudflare Cloudflare quick tunnel — install: brew install cloudflare/cloudflare/cloudflared",
|
|
56
|
+
);
|
|
57
|
+
console.log(
|
|
58
|
+
" No Cloudflare account required for quick tunnels.",
|
|
59
|
+
);
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log("Examples:");
|
|
62
|
+
console.log(" $ vellum tunnel");
|
|
63
|
+
console.log(" $ vellum tunnel --provider ngrok");
|
|
64
|
+
console.log(" $ vellum tunnel --provider cloudflare");
|
|
65
|
+
console.log(" $ vellum tunnel my-assistant --provider cloudflare");
|
|
35
66
|
process.exit(0);
|
|
36
67
|
} else if (arg === "--provider") {
|
|
37
68
|
const next = args[i + 1];
|
|
@@ -78,5 +109,18 @@ export async function tunnel(): Promise<void> {
|
|
|
78
109
|
return;
|
|
79
110
|
}
|
|
80
111
|
|
|
112
|
+
if (provider === "cloudflare") {
|
|
113
|
+
const resources = entry.resources;
|
|
114
|
+
await runCloudflareTunnel(
|
|
115
|
+
resources
|
|
116
|
+
? {
|
|
117
|
+
port: resources.gatewayPort,
|
|
118
|
+
workspaceDir: join(resources.instanceDir, ".vellum", "workspace"),
|
|
119
|
+
}
|
|
120
|
+
: {},
|
|
121
|
+
);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
throw new Error(`Tunnel provider '${provider}' is not yet implemented.`);
|
|
82
126
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vellum unpair <name> [--yes]`
|
|
3
|
+
*
|
|
4
|
+
* Forget a pairing imported from another machine via `vellum connect import`:
|
|
5
|
+
* remove its lockfile entry and stored guardian token from THIS machine. Only
|
|
6
|
+
* paired assistants (`cloud: "paired"`) can be unpaired — `vellum retire` owns
|
|
7
|
+
* local and managed assistants.
|
|
8
|
+
*
|
|
9
|
+
* This is client-side only: it forgets the connection here but does not revoke
|
|
10
|
+
* the device on the host. (Host-side revocation is `vellum devices`.)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
formatAssistantLookupError,
|
|
15
|
+
getAssistantDisplayName,
|
|
16
|
+
lookupAssistantByIdentifier,
|
|
17
|
+
removeAssistantEntry,
|
|
18
|
+
} from "../lib/assistant-config";
|
|
19
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
20
|
+
import {
|
|
21
|
+
canPromptForConfirmation,
|
|
22
|
+
confirmAction,
|
|
23
|
+
} from "../lib/confirm-action.js";
|
|
24
|
+
import { deleteGuardianToken } from "../lib/guardian-token";
|
|
25
|
+
|
|
26
|
+
function printUsage(): void {
|
|
27
|
+
console.log(`vellum unpair - Forget a paired assistant imported from another machine
|
|
28
|
+
|
|
29
|
+
USAGE:
|
|
30
|
+
vellum unpair <name> [--yes]
|
|
31
|
+
|
|
32
|
+
ARGUMENTS:
|
|
33
|
+
<name> Name or id of the paired assistant to forget
|
|
34
|
+
|
|
35
|
+
OPTIONS:
|
|
36
|
+
--yes Skip the interactive confirmation prompt (for automation)
|
|
37
|
+
|
|
38
|
+
Removes the local connection (lockfile entry + stored token). Only paired
|
|
39
|
+
assistants (imported via 'vellum connect import') can be unpaired; use
|
|
40
|
+
'vellum retire' for local or managed assistants.
|
|
41
|
+
|
|
42
|
+
EXAMPLES:
|
|
43
|
+
vellum unpair paired-desk
|
|
44
|
+
vellum unpair "Desk Box"
|
|
45
|
+
vellum unpair paired-desk --yes
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function unpair(): Promise<void> {
|
|
50
|
+
const args = process.argv.slice(3);
|
|
51
|
+
|
|
52
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
53
|
+
printUsage();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const yes = args.includes("--yes");
|
|
58
|
+
const name = parseAssistantTargetArg(args, []);
|
|
59
|
+
if (!name) {
|
|
60
|
+
console.error("Error: assistant name or id is required.");
|
|
61
|
+
printUsage();
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lookup = lookupAssistantByIdentifier(name);
|
|
66
|
+
if (lookup.status !== "found") {
|
|
67
|
+
console.error(formatAssistantLookupError(name, lookup));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const entry = lookup.entry;
|
|
71
|
+
|
|
72
|
+
if (entry.cloud !== "paired") {
|
|
73
|
+
console.error(
|
|
74
|
+
`Error: '${name}' is not a paired assistant. Use \`vellum retire\` to remove a local or managed assistant.`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Print the resolved identity before acting (cli/AGENTS.md).
|
|
80
|
+
const displayName = getAssistantDisplayName(entry);
|
|
81
|
+
console.log("Pairing to unpair:");
|
|
82
|
+
if (displayName !== entry.assistantId) {
|
|
83
|
+
console.log(` Name: ${displayName}`);
|
|
84
|
+
}
|
|
85
|
+
console.log(` ID: ${entry.assistantId}`);
|
|
86
|
+
if (entry.runtimeUrl) {
|
|
87
|
+
console.log(` Host: ${entry.runtimeUrl}`);
|
|
88
|
+
}
|
|
89
|
+
console.log("");
|
|
90
|
+
|
|
91
|
+
if (!yes) {
|
|
92
|
+
if (!canPromptForConfirmation()) {
|
|
93
|
+
console.error(
|
|
94
|
+
"Error: Refusing to unpair without confirmation in a non-interactive terminal.",
|
|
95
|
+
);
|
|
96
|
+
console.error("Re-run with --yes to confirm from automation.");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const confirmed = await confirmAction(
|
|
100
|
+
"Press Enter to unpair, or Esc/q to cancel: ",
|
|
101
|
+
);
|
|
102
|
+
if (!confirmed) {
|
|
103
|
+
console.log("Unpair cancelled.");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
removeAssistantEntry(entry.assistantId);
|
|
109
|
+
deleteGuardianToken(entry.assistantId);
|
|
110
|
+
|
|
111
|
+
console.log(
|
|
112
|
+
`Unpaired '${name}' — removed the local connection (lockfile entry + token).`,
|
|
113
|
+
);
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log(
|
|
116
|
+
"Note: this only forgets the connection on this machine. The assistant's host can fully revoke this device from its side.",
|
|
117
|
+
);
|
|
118
|
+
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -68,6 +68,13 @@ export async function wake(): Promise<void> {
|
|
|
68
68
|
process.exit(1);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (entry.cloud === "paired") {
|
|
72
|
+
console.error(
|
|
73
|
+
`Error: '${entry.assistantId}' is a remote assistant paired from another machine — its lifecycle is managed on its host machine, not here. Use \`vellum client ${entry.assistantId}\` to chat with it.`,
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
if (entry.cloud && entry.cloud !== "local") {
|
|
72
79
|
console.error(
|
|
73
80
|
`Error: 'vellum wake' only works with local and docker assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
|