@vellumai/cli 0.8.12-staging.2 → 0.9.0-staging.1
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/bun.lock +49 -56
- package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +224 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +19 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +8 -1
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +0 -15
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +8 -4
- package/node_modules/@vellumai/local-mode/src/sleep.ts +80 -0
- package/node_modules/@vellumai/local-mode/src/status.ts +342 -0
- package/node_modules/@vellumai/local-mode/src/wake.ts +12 -1
- package/package.json +3 -3
- package/src/__tests__/assistant-config.test.ts +1 -2
- package/src/__tests__/device-id.test.ts +6 -14
- package/src/__tests__/helpers/os-mock.ts +27 -0
- package/src/__tests__/login-loopback.test.ts +71 -0
- package/src/__tests__/multi-local.test.ts +2 -10
- package/src/__tests__/nginx-ingress-command.test.ts +69 -0
- package/src/__tests__/nginx-ingress.test.ts +401 -0
- package/src/__tests__/sleep.test.ts +4 -0
- package/src/__tests__/teleport.test.ts +6 -9
- package/src/__tests__/tunnel.test.ts +164 -0
- package/src/__tests__/wake.test.ts +15 -4
- package/src/__tests__/workos-pkce.test.ts +314 -0
- package/src/commands/flags.ts +1 -22
- package/src/commands/hatch.ts +90 -9
- package/src/commands/login.ts +123 -59
- package/src/commands/nginx-ingress.ts +291 -0
- package/src/commands/rollback.ts +0 -6
- package/src/commands/sleep.ts +17 -0
- package/src/commands/teleport.ts +23 -36
- package/src/commands/tunnel.ts +69 -11
- package/src/commands/upgrade.ts +0 -2
- package/src/commands/wake.ts +7 -5
- package/src/commands/workflows.ts +301 -0
- package/src/index.ts +8 -0
- package/src/lib/arg-utils.ts +48 -0
- package/src/lib/assistant-client.ts +2 -0
- package/src/lib/assistant-config.ts +0 -7
- package/src/lib/cloudflare-tunnel.ts +15 -2
- package/src/lib/docker.ts +103 -49
- package/src/lib/feature-flags.test.ts +157 -0
- package/src/lib/feature-flags.ts +38 -0
- package/src/lib/hatch-local.ts +0 -1
- package/src/lib/local.ts +5 -0
- package/src/lib/nginx-ingress.ts +574 -0
- package/src/lib/ngrok.ts +26 -4
- package/src/lib/platform-client.ts +0 -1
- package/src/lib/retire-local.ts +5 -0
- package/src/lib/statefulset.ts +73 -21
- package/src/lib/sync-cloud-assistants.ts +4 -17
- package/src/lib/upgrade-lifecycle.ts +1 -2
- package/src/lib/workos-pkce.ts +160 -0
package/src/commands/teleport.ts
CHANGED
|
@@ -391,9 +391,8 @@ async function exportFromAssistant(
|
|
|
391
391
|
// daemon, the CLI is updated separately).
|
|
392
392
|
let sourceRuntimeVersion: string;
|
|
393
393
|
try {
|
|
394
|
-
const identity = await callRuntimeWithAuthRetry(
|
|
395
|
-
entry,
|
|
396
|
-
async (token) => localRuntimeIdentity(entry, token),
|
|
394
|
+
const identity = await callRuntimeWithAuthRetry(entry, async (token) =>
|
|
395
|
+
localRuntimeIdentity(entry, token),
|
|
397
396
|
);
|
|
398
397
|
sourceRuntimeVersion = identity.version;
|
|
399
398
|
} catch (err) {
|
|
@@ -427,16 +426,13 @@ async function exportFromAssistant(
|
|
|
427
426
|
let jobId: string;
|
|
428
427
|
let accessToken: string;
|
|
429
428
|
try {
|
|
430
|
-
const result = await callRuntimeWithAuthRetry(
|
|
431
|
-
entry,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return { jobId: r.jobId, token };
|
|
438
|
-
},
|
|
439
|
-
);
|
|
429
|
+
const result = await callRuntimeWithAuthRetry(entry, async (token) => {
|
|
430
|
+
const r = await localRuntimeExportToGcs(entry, token, {
|
|
431
|
+
uploadUrl,
|
|
432
|
+
description: "teleport export",
|
|
433
|
+
});
|
|
434
|
+
return { jobId: r.jobId, token };
|
|
435
|
+
});
|
|
440
436
|
jobId = result.jobId;
|
|
441
437
|
accessToken = result.token;
|
|
442
438
|
} catch (err) {
|
|
@@ -734,9 +730,8 @@ async function importToAssistant(
|
|
|
734
730
|
// target can't actually load) whenever the two drift apart.
|
|
735
731
|
let targetRuntimeVersion: string;
|
|
736
732
|
try {
|
|
737
|
-
const identity = await callRuntimeWithAuthRetry(
|
|
738
|
-
entry,
|
|
739
|
-
(token) => localRuntimeIdentity(entry, token),
|
|
733
|
+
const identity = await callRuntimeWithAuthRetry(entry, (token) =>
|
|
734
|
+
localRuntimeIdentity(entry, token),
|
|
740
735
|
);
|
|
741
736
|
targetRuntimeVersion = identity.version;
|
|
742
737
|
} catch (err) {
|
|
@@ -779,15 +774,12 @@ async function importToAssistant(
|
|
|
779
774
|
let jobId: string;
|
|
780
775
|
let accessToken: string;
|
|
781
776
|
try {
|
|
782
|
-
const result = await callRuntimeWithAuthRetry(
|
|
783
|
-
entry,
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
return { jobId: r.jobId, token };
|
|
789
|
-
},
|
|
790
|
-
);
|
|
777
|
+
const result = await callRuntimeWithAuthRetry(entry, async (token) => {
|
|
778
|
+
const r = await localRuntimeImportFromGcs(entry, token, {
|
|
779
|
+
bundleUrl,
|
|
780
|
+
});
|
|
781
|
+
return { jobId: r.jobId, token };
|
|
782
|
+
});
|
|
791
783
|
jobId = result.jobId;
|
|
792
784
|
accessToken = result.token;
|
|
793
785
|
} catch (err) {
|
|
@@ -910,17 +902,12 @@ export async function resolveOrHatchTarget(
|
|
|
910
902
|
|
|
911
903
|
if (targetEnv === "docker") {
|
|
912
904
|
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
913
|
-
await hatchDocker(
|
|
914
|
-
"vellum",
|
|
915
|
-
false,
|
|
916
|
-
targetName ?? null,
|
|
917
|
-
false,
|
|
918
|
-
|
|
919
|
-
{},
|
|
920
|
-
{
|
|
921
|
-
setupProviderCredentials: false,
|
|
922
|
-
},
|
|
923
|
-
);
|
|
905
|
+
await hatchDocker({
|
|
906
|
+
species: "vellum",
|
|
907
|
+
detached: false,
|
|
908
|
+
name: targetName ?? null,
|
|
909
|
+
setupProviderCredentials: false,
|
|
910
|
+
});
|
|
924
911
|
const entry = targetName
|
|
925
912
|
? findAssistantByName(targetName)
|
|
926
913
|
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
|
|
3
|
-
import { resolveAssistant } from "../lib/assistant-config";
|
|
3
|
+
import { resolveAssistant, type AssistantEntry } from "../lib/assistant-config";
|
|
4
4
|
import { runCloudflareTunnel } from "../lib/cloudflare-tunnel.js";
|
|
5
|
+
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
6
|
+
import {
|
|
7
|
+
isAssistantFeatureFlagEnabled,
|
|
8
|
+
WEB_REMOTE_INGRESS_FLAG,
|
|
9
|
+
} from "../lib/feature-flags.js";
|
|
5
10
|
import { runNgrokTunnel } from "../lib/ngrok";
|
|
6
11
|
|
|
7
12
|
const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
|
|
@@ -88,6 +93,46 @@ function parseArgs(): TunnelArgs {
|
|
|
88
93
|
return { assistantName, provider };
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
function parsePortFromUrl(url: unknown): number | undefined {
|
|
97
|
+
if (typeof url !== "string" || !url.trim()) return undefined;
|
|
98
|
+
try {
|
|
99
|
+
const port = Number(new URL(url).port);
|
|
100
|
+
return Number.isInteger(port) && port > 0 && port <= 65535
|
|
101
|
+
? port
|
|
102
|
+
: undefined;
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveEntryGatewayPort(entry: AssistantEntry): number {
|
|
109
|
+
return (
|
|
110
|
+
entry.resources?.gatewayPort ??
|
|
111
|
+
parsePortFromUrl(entry.localUrl) ??
|
|
112
|
+
parsePortFromUrl(entry.runtimeUrl) ??
|
|
113
|
+
GATEWAY_PORT
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function shouldPreferNginxIngress(
|
|
118
|
+
assistantId: string,
|
|
119
|
+
gatewayPort: number,
|
|
120
|
+
): Promise<boolean> {
|
|
121
|
+
try {
|
|
122
|
+
return await isAssistantFeatureFlagEnabled(
|
|
123
|
+
assistantId,
|
|
124
|
+
WEB_REMOTE_INGRESS_FLAG,
|
|
125
|
+
{ runtimeUrl: `http://127.0.0.1:${gatewayPort}` },
|
|
126
|
+
);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Could not verify the \`${WEB_REMOTE_INGRESS_FLAG}\` feature flag before starting the tunnel. Is the assistant running? Try \`vellum wake\` and retry. ${
|
|
130
|
+
err instanceof Error ? err.message : String(err)
|
|
131
|
+
}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
export async function tunnel(): Promise<void> {
|
|
92
137
|
const { assistantName, provider } = parseArgs();
|
|
93
138
|
|
|
@@ -104,21 +149,34 @@ export async function tunnel(): Promise<void> {
|
|
|
104
149
|
process.exit(1);
|
|
105
150
|
}
|
|
106
151
|
|
|
152
|
+
const resources = entry.resources;
|
|
153
|
+
const gatewayPort = resolveEntryGatewayPort(entry);
|
|
154
|
+
const baseTunnelOpts = {
|
|
155
|
+
port: gatewayPort,
|
|
156
|
+
...(resources
|
|
157
|
+
? { workspaceDir: join(resources.instanceDir, ".vellum", "workspace") }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
|
|
107
161
|
if (provider === "ngrok") {
|
|
108
|
-
await runNgrokTunnel(
|
|
162
|
+
await runNgrokTunnel({
|
|
163
|
+
...baseTunnelOpts,
|
|
164
|
+
preferNginxIngress: await shouldPreferNginxIngress(
|
|
165
|
+
entry.assistantId,
|
|
166
|
+
gatewayPort,
|
|
167
|
+
),
|
|
168
|
+
});
|
|
109
169
|
return;
|
|
110
170
|
}
|
|
111
171
|
|
|
112
172
|
if (provider === "cloudflare") {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
: {},
|
|
121
|
-
);
|
|
173
|
+
await runCloudflareTunnel({
|
|
174
|
+
...baseTunnelOpts,
|
|
175
|
+
preferNginxIngress: await shouldPreferNginxIngress(
|
|
176
|
+
entry.assistantId,
|
|
177
|
+
gatewayPort,
|
|
178
|
+
),
|
|
179
|
+
});
|
|
122
180
|
return;
|
|
123
181
|
}
|
|
124
182
|
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
findAssistantByName,
|
|
7
7
|
getActiveAssistant,
|
|
8
8
|
loadAllAssistants,
|
|
9
|
-
normalizeVersion,
|
|
10
9
|
resolveCloud,
|
|
11
10
|
saveAssistantEntry,
|
|
12
11
|
type AssistantEntry,
|
|
@@ -496,7 +495,6 @@ async function upgradeDocker(
|
|
|
496
495
|
previousContainerInfo: entry.containerInfo,
|
|
497
496
|
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
498
497
|
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
499
|
-
version: normalizeVersion(versionTag),
|
|
500
498
|
// Preserve the backup path so `vellum rollback` can restore it later
|
|
501
499
|
preUpgradeBackupPath: backupPath ?? undefined,
|
|
502
500
|
};
|
package/src/commands/wake.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import { dockerResourceNames, wakeContainers } from "../lib/docker.js";
|
|
10
10
|
import {
|
|
11
11
|
leaseGuardianToken,
|
|
12
|
-
loadGuardianToken,
|
|
13
12
|
resetGuardianBootstrap,
|
|
14
13
|
seedGuardianTokenFromSiblingEnv,
|
|
15
14
|
} from "../lib/guardian-token.js";
|
|
@@ -43,7 +42,7 @@ export async function wake(): Promise<void> {
|
|
|
43
42
|
" --foreground Run assistant in foreground with logs printed to terminal",
|
|
44
43
|
);
|
|
45
44
|
console.log(
|
|
46
|
-
" --repair-guardian
|
|
45
|
+
" --repair-guardian Force-re-provision the guardian token (resets the\n" +
|
|
47
46
|
" gateway bootstrap and re-leases — REVOKES other device-bound\n" +
|
|
48
47
|
" tokens, so only use deliberately, never from auto-repair)",
|
|
49
48
|
);
|
|
@@ -238,8 +237,11 @@ export async function wake(): Promise<void> {
|
|
|
238
237
|
console.log(" Seeded guardian token from sibling environment.");
|
|
239
238
|
}
|
|
240
239
|
|
|
241
|
-
// Last-resort recovery (explicit `--repair-guardian` only):
|
|
242
|
-
//
|
|
240
|
+
// Last-resort recovery (explicit `--repair-guardian` only): force a
|
|
241
|
+
// re-provision. Token health can't be judged locally — a connect can 401
|
|
242
|
+
// off a token whose local expiry looks fine (revoked, mis-seeded, wrong
|
|
243
|
+
// principal) — and the user explicitly confirmed the destructive repair,
|
|
244
|
+
// so guessing "looks healthy, skip" just recreates the no-op loop. The
|
|
243
245
|
// single-use bootstrap secret may already be spent — a prior connect can
|
|
244
246
|
// lease a token that's then lost, or the gateway marks the secret consumed
|
|
245
247
|
// before the client persists it — which otherwise bricks connect into a
|
|
@@ -248,7 +250,7 @@ export async function wake(): Promise<void> {
|
|
|
248
250
|
// by the lockfile secret — mirrors the macOS client's forceReBootstrap), then
|
|
249
251
|
// re-lease. Gated behind the flag because the re-lease revokes other
|
|
250
252
|
// device-bound tokens; it must never run from the automatic repair path.
|
|
251
|
-
if (repairGuardian
|
|
253
|
+
if (repairGuardian) {
|
|
252
254
|
const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
253
255
|
const maxAttempts = 3;
|
|
254
256
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { extractAssistantFlag, extractValueFlag } from "../lib/arg-utils.js";
|
|
2
|
+
import { AssistantClient } from "../lib/assistant-client.js";
|
|
3
|
+
import {
|
|
4
|
+
formatAssistantLookupError,
|
|
5
|
+
lookupAssistantByIdentifier,
|
|
6
|
+
} from "../lib/assistant-config.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Client-side mirror of the server's wire-run projection
|
|
10
|
+
* (`WorkflowRunWire` from `assistant/src/runtime/routes/workflow-routes.ts`).
|
|
11
|
+
* The CLI is an independent build unit and deliberately does NOT import from
|
|
12
|
+
* `assistant/` (see `cli/src/shared/provider-env-vars.ts`), so the shape is
|
|
13
|
+
* mirrored here. Only the fields the CLI renders are declared — the server may
|
|
14
|
+
* send a superset. Keep in sync with `workflowRunSchema`.
|
|
15
|
+
*/
|
|
16
|
+
type WorkflowRun = {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string | null;
|
|
19
|
+
status: string;
|
|
20
|
+
agentsSpawned: number;
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
error: string | null;
|
|
24
|
+
createdAt: number | null;
|
|
25
|
+
finishedAt: number | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SavedWorkflow = {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
path: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function pad(s: string, w: number): string {
|
|
35
|
+
return s + " ".repeat(Math.max(0, w - s.length));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fmtTime(ms: number | null): string {
|
|
39
|
+
return ms == null ? "-" : new Date(ms).toISOString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printRunsTable(runs: WorkflowRun[]): void {
|
|
43
|
+
const headers = {
|
|
44
|
+
id: "ID",
|
|
45
|
+
name: "NAME",
|
|
46
|
+
status: "STATUS",
|
|
47
|
+
agents: "AGENTS",
|
|
48
|
+
created: "CREATED",
|
|
49
|
+
};
|
|
50
|
+
const rows = runs.map((r) => ({
|
|
51
|
+
id: r.id,
|
|
52
|
+
name: r.name ?? "-",
|
|
53
|
+
status: r.status,
|
|
54
|
+
agents: String(r.agentsSpawned),
|
|
55
|
+
created: fmtTime(r.createdAt),
|
|
56
|
+
}));
|
|
57
|
+
const all = [headers, ...rows];
|
|
58
|
+
const w = {
|
|
59
|
+
id: Math.max(...all.map((r) => r.id.length)),
|
|
60
|
+
name: Math.max(...all.map((r) => r.name.length)),
|
|
61
|
+
status: Math.max(...all.map((r) => r.status.length)),
|
|
62
|
+
agents: Math.max(...all.map((r) => r.agents.length)),
|
|
63
|
+
created: Math.max(...all.map((r) => r.created.length)),
|
|
64
|
+
};
|
|
65
|
+
const formatRow = (r: typeof headers) =>
|
|
66
|
+
`${pad(r.id, w.id)} ${pad(r.name, w.name)} ${pad(r.status, w.status)} ${pad(r.agents, w.agents)} ${r.created}`;
|
|
67
|
+
console.log(formatRow(headers));
|
|
68
|
+
console.log(
|
|
69
|
+
`${"-".repeat(w.id)} ${"-".repeat(w.name)} ${"-".repeat(w.status)} ${"-".repeat(w.agents)} ${"-".repeat(w.created)}`,
|
|
70
|
+
);
|
|
71
|
+
for (const row of rows) console.log(formatRow(row));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function printSavedTable(workflows: SavedWorkflow[]): void {
|
|
75
|
+
const headers = { name: "NAME", description: "DESCRIPTION" };
|
|
76
|
+
const rows = workflows.map((w) => ({
|
|
77
|
+
name: w.name,
|
|
78
|
+
description: w.description,
|
|
79
|
+
}));
|
|
80
|
+
const all = [headers, ...rows];
|
|
81
|
+
const w = {
|
|
82
|
+
name: Math.max(...all.map((r) => r.name.length)),
|
|
83
|
+
description: Math.max(...all.map((r) => r.description.length)),
|
|
84
|
+
};
|
|
85
|
+
const formatRow = (r: typeof headers) =>
|
|
86
|
+
`${pad(r.name, w.name)} ${r.description}`;
|
|
87
|
+
console.log(formatRow(headers));
|
|
88
|
+
console.log(`${"-".repeat(w.name)} ${"-".repeat(w.description)}`);
|
|
89
|
+
for (const row of rows) console.log(formatRow(row));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function printHelp(): void {
|
|
93
|
+
console.log("Usage: vellum workflows <subcommand> [options]");
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("Inspect and control workflow runs on the active assistant.");
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log("Subcommands:");
|
|
98
|
+
console.log(" list List saved (named) workflows");
|
|
99
|
+
console.log(" runs List recent workflow runs");
|
|
100
|
+
console.log(" show <run-id> Show details for a single run");
|
|
101
|
+
console.log(" abort <run-id> Abort an in-flight run");
|
|
102
|
+
console.log(
|
|
103
|
+
" resume <run-id> Resume an interrupted run (orphaned by a restart)",
|
|
104
|
+
);
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log("Options:");
|
|
107
|
+
console.log(
|
|
108
|
+
" --assistant <name> Target a specific assistant (display name or ID)",
|
|
109
|
+
);
|
|
110
|
+
console.log(" --limit <n> (runs) Max runs to list");
|
|
111
|
+
console.log(" --status <status> (runs) Filter by run status");
|
|
112
|
+
console.log(" --help, -h Show this help");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createClient(assistantName?: string): AssistantClient {
|
|
116
|
+
let assistantId: string | undefined;
|
|
117
|
+
if (assistantName) {
|
|
118
|
+
const result = lookupAssistantByIdentifier(assistantName);
|
|
119
|
+
if (result.status !== "found") {
|
|
120
|
+
throw new Error(formatAssistantLookupError(assistantName, result));
|
|
121
|
+
}
|
|
122
|
+
assistantId = result.entry.assistantId;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return new AssistantClient(assistantId ? { assistantId } : undefined);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error(
|
|
128
|
+
assistantName
|
|
129
|
+
? `No assistant found matching '${assistantName}'.`
|
|
130
|
+
: "No assistant found. Hatch one with 'vellum hatch' first.",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function rethrowFetchError(err: unknown): never {
|
|
136
|
+
if (
|
|
137
|
+
err instanceof TypeError &&
|
|
138
|
+
(err.message.includes("fetch") || err.message.includes("connect"))
|
|
139
|
+
) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Could not reach the assistant. Is it running? Try 'vellum wake'.",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function requestJson<T>(
|
|
148
|
+
client: AssistantClient,
|
|
149
|
+
method: "get" | "post",
|
|
150
|
+
path: string,
|
|
151
|
+
query?: Record<string, string>,
|
|
152
|
+
): Promise<T> {
|
|
153
|
+
let res: Response;
|
|
154
|
+
try {
|
|
155
|
+
res =
|
|
156
|
+
method === "get"
|
|
157
|
+
? await client.get(path, query ? { query } : undefined)
|
|
158
|
+
: await client.post(path, undefined);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
rethrowFetchError(err);
|
|
161
|
+
}
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const body = await res.text().catch(() => "");
|
|
164
|
+
throw new Error(`Request failed: HTTP ${res.status} ${body}`.trim());
|
|
165
|
+
}
|
|
166
|
+
return (await res.json()) as T;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function listSaved(assistantName?: string): Promise<void> {
|
|
170
|
+
const client = createClient(assistantName);
|
|
171
|
+
const data = await requestJson<{ workflows: SavedWorkflow[] }>(
|
|
172
|
+
client,
|
|
173
|
+
"get",
|
|
174
|
+
"/workflows",
|
|
175
|
+
);
|
|
176
|
+
if (data.workflows.length === 0) {
|
|
177
|
+
console.log("No saved workflows found.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
printSavedTable(data.workflows);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function listRuns(
|
|
184
|
+
opts: { limit?: string; status?: string },
|
|
185
|
+
assistantName?: string,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const client = createClient(assistantName);
|
|
188
|
+
const query: Record<string, string> = {};
|
|
189
|
+
if (opts.limit) query.limit = opts.limit;
|
|
190
|
+
if (opts.status) query.status = opts.status;
|
|
191
|
+
const data = await requestJson<{ runs: WorkflowRun[] }>(
|
|
192
|
+
client,
|
|
193
|
+
"get",
|
|
194
|
+
"/workflows/runs",
|
|
195
|
+
Object.keys(query).length ? query : undefined,
|
|
196
|
+
);
|
|
197
|
+
if (data.runs.length === 0) {
|
|
198
|
+
console.log("No workflow runs found.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
printRunsTable(data.runs);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function showRun(runId: string, assistantName?: string): Promise<void> {
|
|
205
|
+
const client = createClient(assistantName);
|
|
206
|
+
const run = await requestJson<WorkflowRun>(
|
|
207
|
+
client,
|
|
208
|
+
"get",
|
|
209
|
+
`/workflows/runs/${runId}`,
|
|
210
|
+
);
|
|
211
|
+
console.log(`ID: ${run.id}`);
|
|
212
|
+
console.log(`Name: ${run.name ?? "(unnamed)"}`);
|
|
213
|
+
console.log(`Status: ${run.status}`);
|
|
214
|
+
console.log(`Agents spawned: ${run.agentsSpawned}`);
|
|
215
|
+
console.log(
|
|
216
|
+
`Tokens: ${run.inputTokens} in / ${run.outputTokens} out`,
|
|
217
|
+
);
|
|
218
|
+
console.log(`Created: ${fmtTime(run.createdAt)}`);
|
|
219
|
+
console.log(`Finished: ${fmtTime(run.finishedAt)}`);
|
|
220
|
+
if (run.error) console.log(`Error: ${run.error}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function abortRun(runId: string, assistantName?: string): Promise<void> {
|
|
224
|
+
const client = createClient(assistantName);
|
|
225
|
+
await requestJson<{ ok: boolean; runId: string }>(
|
|
226
|
+
client,
|
|
227
|
+
"post",
|
|
228
|
+
`/workflows/runs/${runId}/abort`,
|
|
229
|
+
);
|
|
230
|
+
console.log(`Abort signalled for workflow run ${runId}.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function resumeRun(runId: string, assistantName?: string): Promise<void> {
|
|
234
|
+
const client = createClient(assistantName);
|
|
235
|
+
await requestJson<{ ok: boolean; runId: string }>(
|
|
236
|
+
client,
|
|
237
|
+
"post",
|
|
238
|
+
`/workflows/runs/${runId}/resume`,
|
|
239
|
+
);
|
|
240
|
+
console.log(
|
|
241
|
+
`Resumed workflow run ${runId}. It replays its completed steps and continues from where it was interrupted.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function workflows(): Promise<void> {
|
|
246
|
+
const args = process.argv.slice(3);
|
|
247
|
+
|
|
248
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
249
|
+
printHelp();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const assistantName = extractAssistantFlag(args);
|
|
254
|
+
const limit = extractValueFlag(args, "limit");
|
|
255
|
+
const status = extractValueFlag(args, "status");
|
|
256
|
+
const subcommand = args[0];
|
|
257
|
+
|
|
258
|
+
if (!subcommand || subcommand === "list") {
|
|
259
|
+
await listSaved(assistantName);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (subcommand === "runs") {
|
|
264
|
+
await listRuns({ limit, status }, assistantName);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (subcommand === "show") {
|
|
269
|
+
const runId = args[1];
|
|
270
|
+
if (!runId) {
|
|
271
|
+
console.error("Usage: vellum workflows show <run-id>");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
await showRun(runId, assistantName);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (subcommand === "abort") {
|
|
279
|
+
const runId = args[1];
|
|
280
|
+
if (!runId) {
|
|
281
|
+
console.error("Usage: vellum workflows abort <run-id>");
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
await abortRun(runId, assistantName);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (subcommand === "resume") {
|
|
289
|
+
const runId = args[1];
|
|
290
|
+
if (!runId) {
|
|
291
|
+
console.error("Usage: vellum workflows resume <run-id>");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
await resumeRun(runId, assistantName);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
299
|
+
printHelp();
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { hatch } from "./commands/hatch";
|
|
|
16
16
|
import { login, logout, whoami } from "./commands/login";
|
|
17
17
|
import { logs } from "./commands/logs";
|
|
18
18
|
import { message } from "./commands/message";
|
|
19
|
+
import { nginxIngress } from "./commands/nginx-ingress";
|
|
19
20
|
import { pair } from "./commands/pair";
|
|
20
21
|
import { ps } from "./commands/ps";
|
|
21
22
|
import { recover } from "./commands/recover";
|
|
@@ -33,6 +34,7 @@ import { unpair } from "./commands/unpair";
|
|
|
33
34
|
import { upgrade } from "./commands/upgrade";
|
|
34
35
|
import { use } from "./commands/use";
|
|
35
36
|
import { wake } from "./commands/wake";
|
|
37
|
+
import { workflows } from "./commands/workflows";
|
|
36
38
|
import { resolveAssistant, setActiveAssistant } from "./lib/assistant-config";
|
|
37
39
|
import { loadGuardianToken } from "./lib/guardian-token";
|
|
38
40
|
import { checkHealth } from "./lib/health-check";
|
|
@@ -54,6 +56,7 @@ const commands = {
|
|
|
54
56
|
logout,
|
|
55
57
|
logs,
|
|
56
58
|
message,
|
|
59
|
+
"nginx-ingress": nginxIngress,
|
|
57
60
|
pair,
|
|
58
61
|
ps,
|
|
59
62
|
recover,
|
|
@@ -72,6 +75,7 @@ const commands = {
|
|
|
72
75
|
use,
|
|
73
76
|
wake,
|
|
74
77
|
whoami,
|
|
78
|
+
workflows,
|
|
75
79
|
} as const;
|
|
76
80
|
|
|
77
81
|
type CommandName = keyof typeof commands;
|
|
@@ -96,6 +100,9 @@ function printHelp(): void {
|
|
|
96
100
|
console.log(" flags Show and toggle feature flags");
|
|
97
101
|
console.log(" gateway Gateway management commands");
|
|
98
102
|
console.log(" hatch Create a new assistant instance");
|
|
103
|
+
console.log(
|
|
104
|
+
" nginx-ingress Manage the nginx proxy fronting the gateway for web access [beta]",
|
|
105
|
+
);
|
|
99
106
|
console.log(" logs View logs from an assistant instance");
|
|
100
107
|
console.log(" login Log in to the Vellum platform");
|
|
101
108
|
console.log(" logout Log out of the Vellum platform");
|
|
@@ -126,6 +133,7 @@ function printHelp(): void {
|
|
|
126
133
|
console.log(" use Set the active assistant for commands");
|
|
127
134
|
console.log(" wake Start the assistant and gateway");
|
|
128
135
|
console.log(" whoami Show current logged-in user");
|
|
136
|
+
console.log(" workflows Inspect and control workflow runs");
|
|
129
137
|
console.log("");
|
|
130
138
|
console.log("Options:");
|
|
131
139
|
console.log(
|
package/src/lib/arg-utils.ts
CHANGED
|
@@ -11,3 +11,51 @@ export function extractFlag(
|
|
|
11
11
|
const remaining = [...args.slice(0, idx), ...args.slice(idx + 2)];
|
|
12
12
|
return [value, remaining];
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Strip `--<name> <value>` from argv and return the captured value.
|
|
17
|
+
*
|
|
18
|
+
* Mutates the input array so positional parsing downstream sees a clean shape.
|
|
19
|
+
* Returns `undefined` if the flag is absent. Error-reports a missing value (and
|
|
20
|
+
* exits) so the user gets a clear message rather than the flag being silently
|
|
21
|
+
* swallowed as a positional.
|
|
22
|
+
*/
|
|
23
|
+
export function extractValueFlag(
|
|
24
|
+
args: string[],
|
|
25
|
+
name: string,
|
|
26
|
+
): string | undefined {
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (args[i] !== `--${name}`) continue;
|
|
29
|
+
const value = args[i + 1];
|
|
30
|
+
if (!value || value.startsWith("-")) {
|
|
31
|
+
console.error(`Missing value for --${name} <value>`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
args.splice(i, 2);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Strip `--assistant <name>` from argv and return the captured value.
|
|
42
|
+
*
|
|
43
|
+
* Mutates the input array so positional parsing downstream sees a clean shape
|
|
44
|
+
* (subcommand + key + value). Returns `undefined` if the flag is absent.
|
|
45
|
+
* Error-reports a missing value so the user gets a clear message rather than
|
|
46
|
+
* the flag being silently swallowed as a positional. (Kept distinct from
|
|
47
|
+
* {@link extractValueFlag} only for its `<name>` wording in the error string.)
|
|
48
|
+
*/
|
|
49
|
+
export function extractAssistantFlag(args: string[]): string | undefined {
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
if (args[i] !== "--assistant") continue;
|
|
52
|
+
const value = args[i + 1];
|
|
53
|
+
if (!value || value.startsWith("-")) {
|
|
54
|
+
console.error("Missing value for --assistant <name>");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
args.splice(i, 2);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
@@ -26,6 +26,7 @@ const FALLBACK_RUNTIME_URL = `http://127.0.0.1:${GATEWAY_PORT}`;
|
|
|
26
26
|
|
|
27
27
|
export interface AssistantClientOpts {
|
|
28
28
|
assistantId?: string;
|
|
29
|
+
runtimeUrl?: string;
|
|
29
30
|
/**
|
|
30
31
|
* When provided alongside `orgId`, the client authenticates with a
|
|
31
32
|
* session token instead of a guardian token. The session token is
|
|
@@ -73,6 +74,7 @@ export class AssistantClient {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
this.runtimeUrl = (
|
|
77
|
+
opts?.runtimeUrl ||
|
|
76
78
|
entry.localUrl ||
|
|
77
79
|
entry.runtimeUrl ||
|
|
78
80
|
FALLBACK_RUNTIME_URL
|
|
@@ -97,8 +97,6 @@ export interface AssistantEntry {
|
|
|
97
97
|
sshUser?: string;
|
|
98
98
|
zone?: string;
|
|
99
99
|
hatchedAt?: string;
|
|
100
|
-
/** Installed service-group release version (no `v` prefix), written at hatch/upgrade/rollback. */
|
|
101
|
-
version?: string;
|
|
102
100
|
/** Per-instance resource config. Present for local entries in multi-instance setups. */
|
|
103
101
|
resources?: LocalInstanceResources;
|
|
104
102
|
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
@@ -586,11 +584,6 @@ export function extractHostFromUrl(url: string): string {
|
|
|
586
584
|
}
|
|
587
585
|
}
|
|
588
586
|
|
|
589
|
-
/** Strip a leading `v` so stored versions match the healthz `version` format. */
|
|
590
|
-
export function normalizeVersion(version: string): string {
|
|
591
|
-
return version.replace(/^v/, "");
|
|
592
|
-
}
|
|
593
|
-
|
|
594
587
|
export function saveAssistantEntry(entry: AssistantEntry): void {
|
|
595
588
|
const entries = readAssistants().filter(
|
|
596
589
|
(e) => e.assistantId !== entry.assistantId,
|