@vellumai/cli 0.8.5 → 0.8.7-dev.202606052118.34cd356
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/AGENTS.md +6 -0
- package/bun.lock +8 -0
- package/knip.json +6 -1
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/bun.lock +29 -0
- package/node_modules/@vellumai/local-mode/package.json +22 -0
- 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 +108 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
- 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 +133 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/backup.test.ts +38 -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__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/recover.test.ts +307 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/__tests__/wake.test.ts +215 -0
- package/src/commands/backup.ts +2 -0
- package/src/commands/client.ts +569 -39
- 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/env.ts +1 -1
- package/src/commands/flags.ts +269 -0
- package/src/commands/gateway/token.ts +73 -0
- package/src/commands/gateway.ts +29 -0
- package/src/commands/logs.ts +6 -18
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +57 -41
- package/src/commands/recover.ts +47 -9
- package/src/commands/restore.ts +8 -1
- package/src/commands/retire.ts +23 -70
- package/src/commands/rollback.ts +2 -14
- package/src/commands/sleep.ts +7 -0
- package/src/commands/ssh.ts +5 -24
- package/src/commands/teleport.ts +34 -26
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/upgrade.ts +8 -16
- package/src/commands/wake.ts +75 -45
- package/src/components/DefaultMainScreen.tsx +100 -14
- package/src/index.ts +22 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/__tests__/step-runner.test.ts +49 -1
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +28 -3
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/config-utils.ts +24 -3
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +82 -8
- package/src/lib/environments/__tests__/paths.test.ts +2 -1
- package/src/lib/environments/__tests__/seeds.test.ts +2 -1
- package/src/lib/environments/paths.ts +1 -1
- package/src/lib/environments/resolve.ts +11 -35
- package/src/lib/guardian-token.ts +132 -9
- package/src/lib/hatch-local.ts +75 -33
- package/src/lib/http-client.ts +1 -3
- package/src/lib/lifecycle-reporter.ts +31 -0
- package/src/lib/local.ts +193 -298
- package/src/lib/orphan-detection.ts +9 -5
- package/src/lib/pgrep.ts +5 -1
- package/src/lib/platform-client.ts +97 -49
- package/src/lib/process.ts +109 -39
- package/src/lib/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- package/src/lib/step-runner.ts +67 -7
- package/src/lib/sync-cloud-assistants.ts +17 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { AssistantClient } from "../lib/assistant-client.js";
|
|
2
|
+
import {
|
|
3
|
+
formatAssistantLookupError,
|
|
4
|
+
lookupAssistantByIdentifier,
|
|
5
|
+
} from "../lib/assistant-config.js";
|
|
6
|
+
|
|
7
|
+
type FeatureFlagEntry = {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
defaultEnabled: boolean;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type FlagsResponse = {
|
|
16
|
+
flags: FeatureFlagEntry[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function pad(s: string, w: number): string {
|
|
20
|
+
return s + " ".repeat(Math.max(0, w - s.length));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printFlagTable(flags: FeatureFlagEntry[]): void {
|
|
24
|
+
const headers = {
|
|
25
|
+
key: "KEY",
|
|
26
|
+
enabled: "ENABLED",
|
|
27
|
+
default: "DEFAULT",
|
|
28
|
+
label: "LABEL",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const rows = flags
|
|
32
|
+
.slice()
|
|
33
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
34
|
+
.map((f) => ({
|
|
35
|
+
key: f.enabled !== f.defaultEnabled ? `* ${f.key}` : ` ${f.key}`,
|
|
36
|
+
enabled: String(f.enabled),
|
|
37
|
+
default: String(f.defaultEnabled),
|
|
38
|
+
label: f.label,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const all = [headers, ...rows];
|
|
42
|
+
const colWidths = {
|
|
43
|
+
key: Math.max(...all.map((r) => r.key.length)),
|
|
44
|
+
enabled: Math.max(...all.map((r) => r.enabled.length)),
|
|
45
|
+
default: Math.max(...all.map((r) => r.default.length)),
|
|
46
|
+
label: Math.max(...all.map((r) => r.label.length)),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const formatRow = (r: typeof headers) =>
|
|
50
|
+
`${pad(r.key, colWidths.key)} ${pad(r.enabled, colWidths.enabled)} ${pad(r.default, colWidths.default)} ${r.label}`;
|
|
51
|
+
|
|
52
|
+
console.log(formatRow(headers));
|
|
53
|
+
console.log(
|
|
54
|
+
`${"-".repeat(colWidths.key)} ${"-".repeat(colWidths.enabled)} ${"-".repeat(colWidths.default)} ${"-".repeat(colWidths.label)}`,
|
|
55
|
+
);
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
console.log(formatRow(row));
|
|
58
|
+
}
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log("* = overridden (differs from default)");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printHelp(): void {
|
|
64
|
+
console.log("Usage: vellum flags [subcommand] [options]");
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log("Show and toggle feature flags for the active assistant.");
|
|
67
|
+
console.log(
|
|
68
|
+
"Reads from the gateway's merged flag state (persisted overrides > remote > defaults).",
|
|
69
|
+
);
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log("Subcommands:");
|
|
72
|
+
console.log(" (none) List all feature flags in a table");
|
|
73
|
+
console.log(" get <key> Show details for a single flag");
|
|
74
|
+
console.log(" set <key> <bool> Set a flag override to true or false");
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log("Options:");
|
|
77
|
+
console.log(
|
|
78
|
+
" --assistant <name> Target a specific assistant (display name or ID)",
|
|
79
|
+
);
|
|
80
|
+
console.log(
|
|
81
|
+
" instead of the active one. Useful for scripted",
|
|
82
|
+
);
|
|
83
|
+
console.log(
|
|
84
|
+
" flows like eval harnesses that must not mutate",
|
|
85
|
+
);
|
|
86
|
+
console.log(" the user's active-assistant pointer.");
|
|
87
|
+
console.log(" --help, -h Show this help");
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log("Examples:");
|
|
90
|
+
console.log(
|
|
91
|
+
" $ vellum flags # list flags for active assistant",
|
|
92
|
+
);
|
|
93
|
+
console.log(
|
|
94
|
+
" $ vellum flags get query-complexity-routing # inspect one flag",
|
|
95
|
+
);
|
|
96
|
+
console.log(
|
|
97
|
+
" $ vellum flags set voice-mode true # enable a flag",
|
|
98
|
+
);
|
|
99
|
+
console.log(
|
|
100
|
+
" $ vellum flags set external-plugins true --assistant eval-1 # target by name/id",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createClient(assistantName?: string): AssistantClient {
|
|
105
|
+
// When `--assistant <name>` is provided, resolve the display name or
|
|
106
|
+
// explicit ID through the standard lookup helper (see cli/AGENTS.md
|
|
107
|
+
// "Assistant targeting convention"). Exact ID wins over display-name
|
|
108
|
+
// matches; ambiguous names fail loudly.
|
|
109
|
+
let assistantId: string | undefined;
|
|
110
|
+
if (assistantName) {
|
|
111
|
+
const result = lookupAssistantByIdentifier(assistantName);
|
|
112
|
+
if (result.status !== "found") {
|
|
113
|
+
throw new Error(formatAssistantLookupError(assistantName, result));
|
|
114
|
+
}
|
|
115
|
+
assistantId = result.entry.assistantId;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return new AssistantClient(assistantId ? { assistantId } : undefined);
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error(
|
|
121
|
+
assistantName
|
|
122
|
+
? `No assistant found matching '${assistantName}'.`
|
|
123
|
+
: "No assistant found. Hatch one with 'vellum hatch' first.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function rethrowFetchError(err: unknown): never {
|
|
129
|
+
if (
|
|
130
|
+
err instanceof TypeError &&
|
|
131
|
+
(err.message.includes("fetch") || err.message.includes("connect"))
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"Could not reach the assistant gateway. Is it running? Try 'vellum wake'.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function listFlags(assistantName?: string): Promise<void> {
|
|
141
|
+
const client = createClient(assistantName);
|
|
142
|
+
let res: Response;
|
|
143
|
+
try {
|
|
144
|
+
res = await client.get("/feature-flags");
|
|
145
|
+
} catch (err) {
|
|
146
|
+
rethrowFetchError(err);
|
|
147
|
+
}
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
const body = await res.text().catch(() => "");
|
|
150
|
+
throw new Error(`Failed to fetch flags: HTTP ${res.status} ${body}`.trim());
|
|
151
|
+
}
|
|
152
|
+
const data = (await res.json()) as FlagsResponse;
|
|
153
|
+
if (data.flags.length === 0) {
|
|
154
|
+
console.log("No feature flags found.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
printFlagTable(data.flags);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function getFlag(key: string, assistantName?: string): Promise<void> {
|
|
161
|
+
const client = createClient(assistantName);
|
|
162
|
+
let res: Response;
|
|
163
|
+
try {
|
|
164
|
+
res = await client.get("/feature-flags");
|
|
165
|
+
} catch (err) {
|
|
166
|
+
rethrowFetchError(err);
|
|
167
|
+
}
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const body = await res.text().catch(() => "");
|
|
170
|
+
throw new Error(`Failed to fetch flags: HTTP ${res.status} ${body}`.trim());
|
|
171
|
+
}
|
|
172
|
+
const data = (await res.json()) as FlagsResponse;
|
|
173
|
+
const flag = data.flags.find((f) => f.key === key);
|
|
174
|
+
if (!flag) {
|
|
175
|
+
throw new Error(`Flag "${key}" not found.`);
|
|
176
|
+
}
|
|
177
|
+
console.log(`Key: ${flag.key}`);
|
|
178
|
+
console.log(`Enabled: ${flag.enabled}`);
|
|
179
|
+
console.log(`Default: ${flag.defaultEnabled}`);
|
|
180
|
+
console.log(`Description: ${flag.description || "(none)"}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function setFlag(
|
|
184
|
+
key: string,
|
|
185
|
+
value: boolean,
|
|
186
|
+
assistantName?: string,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const client = createClient(assistantName);
|
|
189
|
+
let res: Response;
|
|
190
|
+
try {
|
|
191
|
+
res = await client.patch(`/feature-flags/${key}`, { enabled: value });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
rethrowFetchError(err);
|
|
194
|
+
}
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
const body = await res.text().catch(() => "");
|
|
197
|
+
throw new Error(`Failed to set flag: HTTP ${res.status} ${body}`.trim());
|
|
198
|
+
}
|
|
199
|
+
console.log(`Flag "${key}" set to ${value}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Strip `--assistant <name>` from argv and return the captured value.
|
|
204
|
+
*
|
|
205
|
+
* Mutates the input array so positional parsing downstream sees a clean
|
|
206
|
+
* shape (subcommand + key + value). Returns `undefined` if the flag is
|
|
207
|
+
* absent. Error-reports a missing value so the user gets a clear message
|
|
208
|
+
* rather than the flag being silently swallowed as a positional.
|
|
209
|
+
*/
|
|
210
|
+
function extractAssistantFlag(args: string[]): string | undefined {
|
|
211
|
+
for (let i = 0; i < args.length; i++) {
|
|
212
|
+
if (args[i] !== "--assistant") continue;
|
|
213
|
+
const value = args[i + 1];
|
|
214
|
+
if (!value || value.startsWith("-")) {
|
|
215
|
+
console.error("Missing value for --assistant <name>");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
args.splice(i, 2);
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function flags(): Promise<void> {
|
|
225
|
+
const args = process.argv.slice(3);
|
|
226
|
+
|
|
227
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
228
|
+
printHelp();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const assistantName = extractAssistantFlag(args);
|
|
233
|
+
|
|
234
|
+
const subcommand = args[0];
|
|
235
|
+
|
|
236
|
+
if (!subcommand) {
|
|
237
|
+
await listFlags(assistantName);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (subcommand === "get") {
|
|
242
|
+
const key = args[1];
|
|
243
|
+
if (!key) {
|
|
244
|
+
console.error("Usage: vellum flags get <key>");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
await getFlag(key, assistantName);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (subcommand === "set") {
|
|
252
|
+
const key = args[1];
|
|
253
|
+
const rawValue = args[2];
|
|
254
|
+
if (!key || rawValue === undefined) {
|
|
255
|
+
console.error("Usage: vellum flags set <key> <true|false>");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
if (rawValue !== "true" && rawValue !== "false") {
|
|
259
|
+
console.error(`Invalid value "${rawValue}". Must be "true" or "false".`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
await setFlag(key, rawValue === "true", assistantName);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
267
|
+
printHelp();
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lookupAssistantByIdentifier,
|
|
3
|
+
formatAssistantLookupError,
|
|
4
|
+
} from "../../lib/assistant-config.js";
|
|
5
|
+
import {
|
|
6
|
+
loadGuardianToken,
|
|
7
|
+
refreshGuardianToken,
|
|
8
|
+
} from "../../lib/guardian-token.js";
|
|
9
|
+
|
|
10
|
+
function printUsage(): void {
|
|
11
|
+
console.log("Usage: vellum gateway token <subcommand> <assistantId>");
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log("Manage gateway authentication tokens.");
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log("Subcommands:");
|
|
16
|
+
console.log(" get Print the current guardian access token");
|
|
17
|
+
console.log(" refresh Refresh an expired access token and print it");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function gatewayToken(): Promise<void> {
|
|
21
|
+
const args = process.argv.slice(4);
|
|
22
|
+
const subcommand = args[0];
|
|
23
|
+
|
|
24
|
+
if (subcommand === "--help" || subcommand === "-h" || !subcommand) {
|
|
25
|
+
printUsage();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (subcommand !== "get" && subcommand !== "refresh") {
|
|
30
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
31
|
+
printUsage();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const assistantId = args[1];
|
|
36
|
+
if (!assistantId) {
|
|
37
|
+
console.error("Missing required argument: <assistantId>");
|
|
38
|
+
printUsage();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = lookupAssistantByIdentifier(assistantId);
|
|
43
|
+
if (result.status !== "found") {
|
|
44
|
+
console.error(formatAssistantLookupError(assistantId, result));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const entry = result.entry;
|
|
48
|
+
|
|
49
|
+
const tokenData = loadGuardianToken(entry.assistantId);
|
|
50
|
+
if (!tokenData) {
|
|
51
|
+
console.error("No guardian token found for this assistant.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (subcommand === "get") {
|
|
56
|
+
console.log(tokenData.accessToken);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const gatewayUrl = entry.localUrl || entry.runtimeUrl;
|
|
61
|
+
if (!gatewayUrl) {
|
|
62
|
+
console.error("No gateway URL found for this assistant.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const refreshed = await refreshGuardianToken(gatewayUrl, entry.assistantId);
|
|
67
|
+
if (!refreshed) {
|
|
68
|
+
console.error("Failed to refresh guardian token.");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(refreshed.accessToken);
|
|
73
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { gatewayToken } from "./gateway/token.js";
|
|
2
|
+
|
|
3
|
+
function printUsage(): void {
|
|
4
|
+
console.log("Usage: vellum gateway <subcommand>");
|
|
5
|
+
console.log("");
|
|
6
|
+
console.log("Gateway management commands.");
|
|
7
|
+
console.log("");
|
|
8
|
+
console.log("Subcommands:");
|
|
9
|
+
console.log(" token Manage gateway authentication tokens");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function gateway(): Promise<void> {
|
|
13
|
+
const args = process.argv.slice(3);
|
|
14
|
+
const subcommand = args[0];
|
|
15
|
+
|
|
16
|
+
if (subcommand === "--help" || subcommand === "-h" || !subcommand) {
|
|
17
|
+
printUsage();
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (subcommand === "token") {
|
|
22
|
+
await gatewayToken();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
27
|
+
printUsage();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
package/src/commands/logs.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { createInterface } from "readline";
|
|
|
4
4
|
import { watch } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
extractHostFromUrl,
|
|
9
|
+
resolveAssistant,
|
|
10
|
+
resolveCloud,
|
|
11
|
+
type AssistantEntry,
|
|
12
|
+
} from "../lib/assistant-config";
|
|
9
13
|
import { dockerResourceNames } from "../lib/docker";
|
|
10
14
|
import { getLogDir } from "../lib/xdg-log";
|
|
11
15
|
import { execOutput } from "../lib/step-runner";
|
|
@@ -112,13 +116,6 @@ function parseArgs(): LogsArgs {
|
|
|
112
116
|
|
|
113
117
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
114
118
|
|
|
115
|
-
function resolveCloud(entry: AssistantEntry): string {
|
|
116
|
-
if (entry.cloud) return entry.cloud;
|
|
117
|
-
if (entry.project) return "gcp";
|
|
118
|
-
if (entry.sshUser) return "custom";
|
|
119
|
-
return "local";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
119
|
/**
|
|
123
120
|
* Parse a relative time string like "10m", "2h", "30s" into a Date.
|
|
124
121
|
* Returns null if the string doesn't look like a relative time.
|
|
@@ -494,15 +491,6 @@ async function showGcpLogs(
|
|
|
494
491
|
}
|
|
495
492
|
}
|
|
496
493
|
|
|
497
|
-
function extractHostFromUrl(url: string): string {
|
|
498
|
-
try {
|
|
499
|
-
const parsed = new URL(url);
|
|
500
|
-
return parsed.hostname;
|
|
501
|
-
} catch {
|
|
502
|
-
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
494
|
async function showCustomLogs(
|
|
507
495
|
entry: AssistantEntry,
|
|
508
496
|
opts: LogsArgs,
|
|
@@ -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
|
+
}
|