@vellumai/cli 0.8.9 → 0.8.10-dev.202606092334.09948c8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.8.9",
3
+ "version": "0.8.10-dev.202606092334.09948c8",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,85 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ import { parseConfirmArgs } from "../commands/confirm.js";
4
+
5
+ describe("parseConfirmArgs", () => {
6
+ test("parses a request id with the active assistant and defaults to allow", () => {
7
+ const r = parseConfirmArgs(["--request-id", "req-1"]);
8
+ expect(r).toEqual({
9
+ ok: true,
10
+ value: {
11
+ assistantId: undefined,
12
+ requestId: "req-1",
13
+ decision: "allow",
14
+ jsonOutput: false,
15
+ },
16
+ });
17
+ });
18
+
19
+ test("parses an explicit assistant plus request id", () => {
20
+ const r = parseConfirmArgs(["my-assistant", "--request-id", "req-1"]);
21
+ expect(r.ok).toBe(true);
22
+ if (!r.ok) return;
23
+ expect(r.value.assistantId).toBe("my-assistant");
24
+ expect(r.value.requestId).toBe("req-1");
25
+ });
26
+
27
+ test("honors an explicit deny decision", () => {
28
+ const r = parseConfirmArgs(["--request-id", "req-1", "--decision", "deny"]);
29
+ expect(r.ok).toBe(true);
30
+ if (!r.ok) return;
31
+ expect(r.value.decision).toBe("deny");
32
+ });
33
+
34
+ test("requires a request id", () => {
35
+ const r = parseConfirmArgs([]);
36
+ expect(r).toEqual({ ok: false, error: "--request-id is required." });
37
+ });
38
+
39
+ test("rejects an unknown decision", () => {
40
+ const r = parseConfirmArgs([
41
+ "--request-id",
42
+ "req-1",
43
+ "--decision",
44
+ "maybe",
45
+ ]);
46
+ expect(r).toEqual({
47
+ ok: false,
48
+ error: '--decision must be "allow" or "deny" (got "maybe").',
49
+ });
50
+ });
51
+
52
+ test("rejects a value-less --decision instead of defaulting to allow", () => {
53
+ // A trailing `--decision` with no value (e.g. an empty shell expansion)
54
+ // must not silently approve via the "allow" default.
55
+ const r = parseConfirmArgs(["--request-id", "req-1", "--decision"]);
56
+ expect(r).toEqual({
57
+ ok: false,
58
+ error: '--decision requires a value ("allow" or "deny").',
59
+ });
60
+ });
61
+
62
+ test("rejects an empty-string --decision value", () => {
63
+ const r = parseConfirmArgs(["--request-id", "req-1", "--decision", ""]);
64
+ expect(r).toEqual({
65
+ ok: false,
66
+ error: '--decision must be "allow" or "deny" (got "").',
67
+ });
68
+ });
69
+
70
+ test("rejects a value-less --request-id", () => {
71
+ const r = parseConfirmArgs(["--request-id"]);
72
+ expect(r).toEqual({
73
+ ok: false,
74
+ error: "--request-id requires a value.",
75
+ });
76
+ });
77
+
78
+ test("preserves --json alongside the request id", () => {
79
+ const r = parseConfirmArgs(["--json", "--request-id", "req-1"]);
80
+ expect(r.ok).toBe(true);
81
+ if (!r.ok) return;
82
+ expect(r.value.jsonOutput).toBe(true);
83
+ expect(r.value.requestId).toBe("req-1");
84
+ });
85
+ });
@@ -85,6 +85,7 @@ interface ParsedArgs {
85
85
  flagEnvVars: Record<string, string>;
86
86
  /** Parsed --flag overrides: kebab-case key -> typed value (for web injection). */
87
87
  parsedFlagOverrides: Record<string, boolean | string>;
88
+ disablePlatform: boolean;
88
89
  }
89
90
 
90
91
  function readAssistantName(entry: AssistantEntry | null): string | undefined {
@@ -99,6 +100,8 @@ export function parseArgs(): ParsedArgs {
99
100
  const { envVars: cliFlagVars, remaining: argsWithoutFlags } =
100
101
  parseFeatureFlagArgs(process.argv.slice(3));
101
102
  const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
103
+ const disablePlatformAmbient = process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
104
+ let disablePlatform = disablePlatformAmbient === "true" || disablePlatformAmbient === "1";
102
105
  const args = argsWithoutFlags;
103
106
 
104
107
  // Build parsedFlagOverrides from the extracted env vars:
@@ -133,6 +136,8 @@ export function parseArgs(): ParsedArgs {
133
136
  if (arg === "--help" || arg === "-h") {
134
137
  printUsage();
135
138
  process.exit(0);
139
+ } else if (arg === "--disable-platform") {
140
+ disablePlatform = true;
136
141
  } else if (
137
142
  (arg === "--url" ||
138
143
  arg === "-u" ||
@@ -252,6 +257,7 @@ export function parseArgs(): ParsedArgs {
252
257
  interfaceId,
253
258
  flagEnvVars,
254
259
  parsedFlagOverrides,
260
+ disablePlatform,
255
261
  };
256
262
  }
257
263
 
@@ -272,6 +278,7 @@ ${ANSI.bold}OPTIONS:${ANSI.reset}
272
278
  -a, --assistant-id <id> Assistant ID
273
279
  -i, --interface <id> Interface identifier: cli (default) or web
274
280
  --flag <key=value> Feature flag override (repeatable, kebab-case key)
281
+ --disable-platform Suppress all outbound platform API calls
275
282
  -h, --help Show this help message
276
283
 
277
284
  ${ANSI.bold}DEFAULTS:${ANSI.reset}
@@ -652,6 +659,7 @@ function getBaseDir(): string {
652
659
  async function runWebInterface(
653
660
  flagEnvVars: Record<string, string>,
654
661
  parsedFlagOverrides: Record<string, boolean | string>,
662
+ disablePlatform: boolean,
655
663
  ): Promise<void> {
656
664
  // Propagate flag env vars so child processes (e.g. hatch from the web UI) inherit them.
657
665
  Object.assign(process.env, flagEnvVars);
@@ -660,7 +668,7 @@ async function runWebInterface(
660
668
  // (HMR, __local endpoints, gateway proxy).
661
669
  const webSourceDir = findWebSourceDir();
662
670
  if (webSourceDir) {
663
- return runViteDevServer(webSourceDir, flagEnvVars);
671
+ return runViteDevServer(webSourceDir, flagEnvVars, disablePlatform);
664
672
  }
665
673
 
666
674
  const distDir = findWebDistDir();
@@ -679,7 +687,7 @@ async function runWebInterface(
679
687
  const webUrl = getWebUrl();
680
688
  const safeJson = (v: unknown) =>
681
689
  JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
682
- const configJson = safeJson({ webUrl, platformUrl });
690
+ const configJson = safeJson({ webUrl, platformUrl, disablePlatform });
683
691
  const hasOverrides = Object.keys(parsedFlagOverrides).length > 0;
684
692
  const flagOverridesSnippet = hasOverrides
685
693
  ? `;window.__VELLUM_FLAG_OVERRIDES__=${safeJson(parsedFlagOverrides)}`
@@ -814,6 +822,7 @@ async function runWebInterface(
814
822
  async function runViteDevServer(
815
823
  webSourceDir: string,
816
824
  flagEnvVars: Record<string, string>,
825
+ disablePlatform: boolean,
817
826
  ): Promise<void> {
818
827
  const platformUrl = getPlatformUrl();
819
828
 
@@ -830,6 +839,7 @@ async function runViteDevServer(
830
839
  ...process.env,
831
840
  ...flagEnvVars,
832
841
  ...viteFlagVars,
842
+ ...(disablePlatform ? { VITE_VELLUM_DISABLE_PLATFORM: "true" } : {}),
833
843
  VITE_PLATFORM_MODE: "false",
834
844
  API_PROXY_TARGET: platformUrl,
835
845
  VELLUM_WEB_URL: getWebUrl(),
@@ -909,10 +919,15 @@ export async function client(): Promise<void> {
909
919
  interfaceId,
910
920
  flagEnvVars,
911
921
  parsedFlagOverrides,
922
+ disablePlatform,
912
923
  } = parseArgs();
913
924
 
925
+ if (disablePlatform) {
926
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
927
+ }
928
+
914
929
  if (interfaceId === WEB_INTERFACE_ID) {
915
- await runWebInterface(flagEnvVars, parsedFlagOverrides);
930
+ await runWebInterface(flagEnvVars, parsedFlagOverrides, disablePlatform);
916
931
  return;
917
932
  }
918
933
 
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `vellum confirm <assistant> --request-id <id> --decision allow|deny`
3
+ *
4
+ * Resolve a pending tool confirmation on a running assistant via its
5
+ * runtime HTTP API. The assistant raises a `confirmation_request` event
6
+ * (with a `requestId`) when a tool exceeds the auto-approve risk
7
+ * threshold; this command answers it so the turn can proceed. Headless
8
+ * automation (e.g. the evals harness) uses it to approve requests that
9
+ * would otherwise hang waiting for an interactive user.
10
+ */
11
+
12
+ import { extractFlag } from "../lib/arg-utils.js";
13
+ import { AssistantClient } from "../lib/assistant-client.js";
14
+
15
+ function printUsage(): void {
16
+ console.log(`vellum confirm - Resolve a pending tool confirmation
17
+
18
+ USAGE:
19
+ vellum confirm [assistant] --request-id <id> [--decision allow|deny]
20
+
21
+ ARGUMENTS:
22
+ [assistant] Instance name (default: active assistant)
23
+
24
+ OPTIONS:
25
+ --request-id <id> The requestId from the confirmation_request event (required)
26
+ --decision <value> allow or deny (default: allow)
27
+ --json Output raw JSON response
28
+
29
+ EXAMPLES:
30
+ vellum confirm --request-id ede263d9-cc45-4d63-86f8-a656d17b3a3a
31
+ vellum confirm my-assistant --request-id req-1 --decision deny
32
+ vellum confirm --json --request-id req-1
33
+ `);
34
+ }
35
+
36
+ interface ParsedConfirmArgs {
37
+ assistantId?: string;
38
+ requestId: string;
39
+ decision: "allow" | "deny";
40
+ jsonOutput: boolean;
41
+ }
42
+
43
+ type ParseResult =
44
+ | { ok: true; value: ParsedConfirmArgs }
45
+ | { ok: false; error: string };
46
+
47
+ /**
48
+ * Parse `vellum confirm` arguments. Pure: does no I/O and never exits, so the
49
+ * positional/flag rules can be unit-tested. Defaults the decision to `allow`,
50
+ * which is the common automation case (approve and continue).
51
+ */
52
+ export function parseConfirmArgs(rawArgs: string[]): ParseResult {
53
+ const jsonOutput = rawArgs.includes("--json");
54
+ let args = rawArgs.filter((a) => a !== "--json");
55
+
56
+ const requestIdFlagPresent = args.includes("--request-id");
57
+ const [requestId, afterRequestId] = extractFlag(args, "--request-id");
58
+ args = afterRequestId;
59
+
60
+ const decisionFlagPresent = args.includes("--decision");
61
+ const [decisionRaw, afterDecision] = extractFlag(args, "--decision");
62
+ args = afterDecision;
63
+
64
+ // `extractFlag` strips a trailing value-less flag, which would otherwise let
65
+ // the next positional masquerade as the flag's value (or, for --decision,
66
+ // silently fall back to "allow" and approve a tool call the caller never
67
+ // meant to approve). Treat a flag supplied without a value as an error.
68
+ if (requestIdFlagPresent && requestId === undefined) {
69
+ return { ok: false, error: "--request-id requires a value." };
70
+ }
71
+ if (!requestId) {
72
+ return { ok: false, error: "--request-id is required." };
73
+ }
74
+
75
+ if (decisionFlagPresent && decisionRaw === undefined) {
76
+ return {
77
+ ok: false,
78
+ error: '--decision requires a value ("allow" or "deny").',
79
+ };
80
+ }
81
+ const decision = decisionRaw ?? "allow";
82
+ if (decision !== "allow" && decision !== "deny") {
83
+ return {
84
+ ok: false,
85
+ error: `--decision must be "allow" or "deny" (got "${decision}").`,
86
+ };
87
+ }
88
+
89
+ if (args.length >= 2) {
90
+ return { ok: false, error: "unexpected extra arguments." };
91
+ }
92
+
93
+ return {
94
+ ok: true,
95
+ value: { assistantId: args[0], requestId, decision, jsonOutput },
96
+ };
97
+ }
98
+
99
+ function exitWithUsage(error: string): never {
100
+ console.error(`Error: ${error}`);
101
+ console.error("");
102
+ printUsage();
103
+ process.exit(1);
104
+ }
105
+
106
+ export async function confirm(): Promise<void> {
107
+ const rawArgs = process.argv.slice(3);
108
+
109
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
110
+ printUsage();
111
+ return;
112
+ }
113
+
114
+ const parsed = parseConfirmArgs(rawArgs);
115
+ if (!parsed.ok) {
116
+ exitWithUsage(parsed.error);
117
+ }
118
+
119
+ const { assistantId, requestId, decision, jsonOutput } = parsed.value;
120
+
121
+ const client = new AssistantClient({ assistantId });
122
+
123
+ const response = await client.post("/confirm", { requestId, decision });
124
+
125
+ if (!response.ok) {
126
+ const body = await response.text().catch(() => "");
127
+ console.error(
128
+ `Error: HTTP ${response.status}: ${body || response.statusText}`,
129
+ );
130
+ process.exit(1);
131
+ }
132
+
133
+ const result = (await response.json()) as { accepted: boolean };
134
+
135
+ if (jsonOutput) {
136
+ console.log(JSON.stringify(result, null, 2));
137
+ } else {
138
+ console.log(
139
+ result.accepted
140
+ ? `Confirmation resolved (${decision})`
141
+ : `Confirmation not accepted`,
142
+ );
143
+ }
144
+ }
@@ -181,6 +181,7 @@ interface HatchArgs {
181
181
  configValues: Record<string, string>;
182
182
  flagEnvVars: Record<string, string>;
183
183
  analyze: boolean;
184
+ disablePlatform: boolean;
184
185
  }
185
186
 
186
187
  function parseArgs(): HatchArgs {
@@ -188,6 +189,8 @@ function parseArgs(): HatchArgs {
188
189
  process.argv.slice(3),
189
190
  );
190
191
  const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
192
+ const disablePlatformAmbient = process.env.VELLUM_DISABLE_PLATFORM?.trim().toLowerCase();
193
+ let disablePlatform = disablePlatformAmbient === "true" || disablePlatformAmbient === "1";
191
194
  let species: Species = DEFAULT_SPECIES;
192
195
  let detached = false;
193
196
  let keepAlive = false;
@@ -233,6 +236,9 @@ function parseArgs(): HatchArgs {
233
236
  console.log(
234
237
  " --analyze Emit a structured hatch-timing log line on stdout",
235
238
  );
239
+ console.log(
240
+ " --disable-platform Suppress all outbound platform API calls",
241
+ );
236
242
  process.exit(0);
237
243
  } else if (arg === "-d") {
238
244
  detached = true;
@@ -293,11 +299,13 @@ function parseArgs(): HatchArgs {
293
299
  const value = next.slice(eqIndex + 1);
294
300
  configValues[key] = value;
295
301
  i++;
302
+ } else if (arg === "--disable-platform") {
303
+ disablePlatform = true;
296
304
  } else if (VALID_SPECIES.includes(arg as Species)) {
297
305
  species = arg as Species;
298
306
  } else {
299
307
  console.error(
300
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze`,
308
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --source <path>, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>, --flag <key=value>, --analyze, --disable-platform`,
301
309
  );
302
310
  process.exit(1);
303
311
  }
@@ -314,6 +322,7 @@ function parseArgs(): HatchArgs {
314
322
  configValues,
315
323
  flagEnvVars,
316
324
  analyze,
325
+ disablePlatform,
317
326
  };
318
327
  }
319
328
 
@@ -549,8 +558,14 @@ export async function hatch(): Promise<void> {
549
558
  configValues,
550
559
  flagEnvVars,
551
560
  analyze,
561
+ disablePlatform,
552
562
  } = parseArgs();
553
563
 
564
+ if (disablePlatform) {
565
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
566
+ flagEnvVars.VELLUM_DISABLE_PLATFORM = "true";
567
+ }
568
+
554
569
  if (watch && remote !== "local" && remote !== "docker") {
555
570
  console.error(
556
571
  "Error: --watch is only supported for local and docker hatch targets.",
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import cliPkg from "../package.json";
4
4
  import { backup } from "./commands/backup";
5
5
  import { clean } from "./commands/clean";
6
6
  import { client } from "./commands/client";
7
+ import { confirm } from "./commands/confirm";
7
8
  import { connect } from "./commands/connect";
8
9
  import { devices } from "./commands/devices";
9
10
  import { env } from "./commands/env";
@@ -40,6 +41,7 @@ const commands = {
40
41
  backup,
41
42
  clean,
42
43
  client,
44
+ confirm,
43
45
  connect,
44
46
  devices,
45
47
  env,
@@ -81,6 +83,7 @@ function printHelp(): void {
81
83
  console.log(" backup Export a backup of a running assistant");
82
84
  console.log(" clean Kill orphaned vellum processes");
83
85
  console.log(" client Connect to a hatched assistant");
86
+ console.log(" confirm Resolve a pending tool confirmation on an assistant");
84
87
  console.log(" connect Import an assistant paired from another machine");
85
88
  console.log(" devices List or revoke devices paired to a local assistant");
86
89
  console.log(" env Manage the default CLI environment");
package/src/lib/docker.ts CHANGED
@@ -1323,6 +1323,10 @@ export async function hatchDocker(
1323
1323
  : ownSecret;
1324
1324
 
1325
1325
  emitProgress(4, 6, "Starting containers...");
1326
+ if (flagEnvVars.VELLUM_DISABLE_PLATFORM) {
1327
+ extraAssistantEnv.VELLUM_DISABLE_PLATFORM =
1328
+ flagEnvVars.VELLUM_DISABLE_PLATFORM;
1329
+ }
1326
1330
  const extraGatewayEnv =
1327
1331
  Object.keys(flagEnvVars).length > 0 ? flagEnvVars : undefined;
1328
1332
  await startContainers(
package/src/lib/local.ts CHANGED
@@ -961,6 +961,7 @@ export async function startLocalDaemon(
961
961
  "VELLUM_DEBUG",
962
962
  "VELLUM_DEV",
963
963
  "VELLUM_DESKTOP_APP",
964
+ "VELLUM_DISABLE_PLATFORM",
964
965
  "VELLUM_WORKSPACE_DIR",
965
966
  ]) {
966
967
  if (process.env[key]) {