@vellumai/cli 0.8.0 → 0.8.2

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.
@@ -18,7 +18,6 @@ import {
18
18
  import { getStateDir } from "./environments/paths.js";
19
19
  import { getCurrentEnvironment } from "./environments/resolve.js";
20
20
  import { loadGuardianToken } from "./guardian-token.js";
21
- import { getPlatformUrl } from "./platform-client.js";
22
21
  import { resolveImageRefs } from "./platform-releases.js";
23
22
  import { exec, execOutput } from "./step-runner.js";
24
23
  import { compareVersions } from "./version-compat.js";
@@ -481,42 +480,6 @@ export async function performDockerRollback(
481
480
  console.log("🔍 Resolving image references...");
482
481
  const { imageTags: targetImageTags } = await resolveImageRefs(targetVersion);
483
482
 
484
- // Fetch target migration ceiling from releases API
485
- let targetMigrationCeiling: {
486
- dbVersion?: number;
487
- workspaceMigrationId?: string;
488
- } = {};
489
- try {
490
- const platformUrl = getPlatformUrl();
491
- const releasesResp = await fetch(
492
- `${platformUrl}/v1/releases/?stable=true`,
493
- { signal: AbortSignal.timeout(10000) },
494
- );
495
- if (releasesResp.ok) {
496
- const releases = (await releasesResp.json()) as Array<{
497
- version: string;
498
- db_migration_version?: number | null;
499
- last_workspace_migration_id?: string;
500
- }>;
501
- const normalizedTag = targetVersion.replace(/^v/, "");
502
- const targetRelease = releases.find(
503
- (r) => r.version?.replace(/^v/, "") === normalizedTag,
504
- );
505
- if (
506
- targetRelease?.db_migration_version != null ||
507
- targetRelease?.last_workspace_migration_id
508
- ) {
509
- targetMigrationCeiling = {
510
- dbVersion: targetRelease.db_migration_version ?? undefined,
511
- workspaceMigrationId:
512
- targetRelease.last_workspace_migration_id || undefined,
513
- };
514
- }
515
- }
516
- } catch {
517
- // Best-effort — fall back to rollbackToRegistryCeiling post-swap
518
- }
519
-
520
483
  // Capture current image digests for auto-rollback on failure
521
484
  console.log("📸 Capturing current image references for rollback...");
522
485
  const currentImageRefs = await captureImageRefs(res);
@@ -702,26 +665,6 @@ export async function performDockerRollback(
702
665
  }
703
666
  console.log("✅ Docker images pulled\n");
704
667
 
705
- // Pre-swap migration rollback to target ceiling on the CURRENT (newer) daemon
706
- let preSwapRollbackOk = true;
707
- if (
708
- targetMigrationCeiling.dbVersion !== undefined ||
709
- targetMigrationCeiling.workspaceMigrationId !== undefined
710
- ) {
711
- console.log("🔄 Reverting database changes...");
712
- await broadcastUpgradeEvent(
713
- entry.runtimeUrl,
714
- entry.assistantId,
715
- buildProgressEvent(UPGRADE_PROGRESS.REVERTING_MIGRATIONS),
716
- );
717
- preSwapRollbackOk = await rollbackMigrations(
718
- entry.runtimeUrl,
719
- entry.assistantId,
720
- targetMigrationCeiling.dbVersion,
721
- targetMigrationCeiling.workspaceMigrationId,
722
- );
723
- }
724
-
725
668
  // Progress: switching version
726
669
  await broadcastUpgradeEvent(
727
670
  entry.runtimeUrl,
@@ -757,22 +700,15 @@ export async function performDockerRollback(
757
700
  if (ready) {
758
701
  // Success path
759
702
 
760
- // Post-swap migration rollback fallback: if pre-swap rollback failed
761
- // or no ceiling metadata was available, ask the now-running old daemon
762
- // to roll back migrations above its own registry ceiling.
763
- if (
764
- !preSwapRollbackOk ||
765
- (targetMigrationCeiling.dbVersion === undefined &&
766
- targetMigrationCeiling.workspaceMigrationId === undefined)
767
- ) {
768
- await rollbackMigrations(
769
- entry.runtimeUrl,
770
- entry.assistantId,
771
- undefined,
772
- undefined,
773
- true,
774
- );
775
- }
703
+ // Post-swap migration rollback: ask the now-running old daemon to roll
704
+ // back any migrations above its own registry ceiling.
705
+ await rollbackMigrations(
706
+ entry.runtimeUrl,
707
+ entry.assistantId,
708
+ undefined,
709
+ undefined,
710
+ true,
711
+ );
776
712
 
777
713
  // Capture new digests from the rolled-back containers
778
714
  const newDigests = await captureImageRefs(res);
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * Provider API key environment variable names, keyed by provider ID.
3
3
  *
4
- * Two sources are merged into a single combined map:
4
+ * Two sources are merged into a single combined map. Both are locally-
5
+ * maintained mirrors of canonical catalogs in `assistant/src/providers/`
6
+ * — the CLI does not import from `assistant/src/`, so drift is caught by
7
+ * dedicated parity tests:
5
8
  *
6
- * 1. Search-provider env vars — hardcoded below (Brave, Perplexity).
7
- * 2. LLM-provider env vars — sourced from `PROVIDER_CATALOG` in
8
- * `assistant/src/providers/model-catalog.ts` via a locally-maintained
9
- * mirror (the CLI does not import from `assistant/src/`; drift is caught
10
- * by `cli/src/__tests__/llm-provider-env-var-parity.test.ts`).
9
+ * 1. LLM-provider env vars — mirrors `PROVIDER_CATALOG` entries with an
10
+ * `envVar`. Drift guard: `cli/src/__tests__/llm-provider-env-var-parity.test.ts`.
11
+ * 2. Search-provider env vars — mirrors `SEARCH_PROVIDER_CATALOG`
12
+ * entries with an `envVar`. Drift guard:
13
+ * `cli/src/__tests__/search-provider-env-var-parity.test.ts`.
11
14
  *
12
15
  * The combined map is what cloud-infra code (docker.ts, aws.ts, gcp.ts)
13
16
  * iterates to forward provider API keys from the caller's environment into
@@ -23,12 +26,16 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
23
26
  gemini: "GEMINI_API_KEY",
24
27
  fireworks: "FIREWORKS_API_KEY",
25
28
  openrouter: "OPENROUTER_API_KEY",
29
+ zai: "ZAI_API_KEY",
30
+ deepseek: "DEEPSEEK_API_KEY",
31
+ minimax: "MINIMAX_API_KEY",
26
32
  };
27
33
 
28
- /** Search-provider env var names. */
34
+ /** Search-provider env var names. Mirrors `SEARCH_PROVIDER_CATALOG` BYOK entries. */
29
35
  export const SEARCH_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
30
- brave: "BRAVE_API_KEY",
31
36
  perplexity: "PERPLEXITY_API_KEY",
37
+ brave: "BRAVE_API_KEY",
38
+ tavily: "TAVILY_API_KEY",
32
39
  };
33
40
 
34
41
  /**
@@ -1,153 +0,0 @@
1
- const DOCTOR_URL = process.env.DOCTOR_SERVICE_URL?.trim() || "";
2
-
3
- export type ProgressPhase =
4
- | "invoking_prompt"
5
- | "calling_tool"
6
- | "processing_tool_result";
7
-
8
- export interface ProgressEvent {
9
- phase: ProgressPhase;
10
- toolName?: string;
11
- }
12
-
13
- interface DoctorResult {
14
- assistantId: string;
15
- diagnostics: string | null;
16
- recommendation: string | null;
17
- error: string | null;
18
- }
19
-
20
- export interface ChatLogEntry {
21
- role: "user" | "assistant" | "error";
22
- content: string;
23
- }
24
-
25
- type DoctorProgressCallback = (event: ProgressEvent) => void;
26
- type DoctorLogCallback = (message: string) => void;
27
-
28
- async function streamDoctorResponse(
29
- response: globalThis.Response,
30
- onProgress?: DoctorProgressCallback,
31
- onLog?: DoctorLogCallback,
32
- ): Promise<DoctorResult> {
33
- if (!response.body) {
34
- throw new Error(
35
- `No response body from doctor (HTTP ${response.status} ${response.statusText})`,
36
- );
37
- }
38
-
39
- let result: DoctorResult | null = null;
40
- const decoder = new TextDecoder();
41
- let buffer = "";
42
- let chunkCount = 0;
43
- const receivedEventTypes: string[] = [];
44
-
45
- try {
46
- for await (const chunk of response.body) {
47
- chunkCount++;
48
- buffer += decoder.decode(chunk, { stream: true });
49
- const lines = buffer.split("\n");
50
- buffer = lines.pop() ?? "";
51
-
52
- for (const line of lines) {
53
- if (!line.trim()) continue;
54
- const parsed = JSON.parse(line) as { type: string } & Record<
55
- string,
56
- unknown
57
- >;
58
- receivedEventTypes.push(parsed.type);
59
- if (parsed.type === "progress") {
60
- onProgress?.(parsed as unknown as ProgressEvent);
61
- } else if (parsed.type === "log") {
62
- onLog?.((parsed as unknown as { message: string }).message);
63
- } else if (parsed.type === "result") {
64
- result = parsed as unknown as DoctorResult;
65
- }
66
- }
67
- }
68
- } catch (streamErr) {
69
- const detail =
70
- streamErr instanceof Error ? streamErr.message : String(streamErr);
71
- throw new Error(
72
- `Doctor stream interrupted after ${chunkCount} chunks ` +
73
- `(received events: [${receivedEventTypes.join(", ")}]): ${detail}`,
74
- );
75
- }
76
-
77
- if (buffer.trim()) {
78
- const parsed = JSON.parse(buffer) as { type: string } & Record<
79
- string,
80
- unknown
81
- >;
82
- receivedEventTypes.push(parsed.type);
83
- if (parsed.type === "result") {
84
- result = parsed as unknown as DoctorResult;
85
- }
86
- }
87
-
88
- if (!result) {
89
- throw new Error(
90
- `No result received from doctor. ` +
91
- `HTTP ${response.status}, ${chunkCount} chunks read, ` +
92
- `events received: [${receivedEventTypes.join(", ")}], ` +
93
- `trailing buffer: ${buffer.trim() ? JSON.stringify(buffer.trim().slice(0, 200)) : "(empty)"}`,
94
- );
95
- }
96
-
97
- return result;
98
- }
99
-
100
- async function callDoctorDaemon(
101
- assistantId: string,
102
- project?: string,
103
- zone?: string,
104
- userPrompt?: string,
105
- onProgress?: DoctorProgressCallback,
106
- sessionId?: string,
107
- chatContext?: ChatLogEntry[],
108
- onLog?: DoctorLogCallback,
109
- ): Promise<DoctorResult> {
110
- if (!DOCTOR_URL) {
111
- onLog?.("Doctor service not configured (DOCTOR_SERVICE_URL is not set)");
112
- return {
113
- assistantId,
114
- diagnostics: null,
115
- recommendation: null,
116
- error: "Doctor service not configured",
117
- };
118
- }
119
-
120
- const MAX_RETRIES = 2;
121
- let lastError: unknown;
122
-
123
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
124
- try {
125
- const response = await fetch(`${DOCTOR_URL}/doctor`, {
126
- method: "POST",
127
- headers: { "Content-Type": "application/json" },
128
- body: JSON.stringify({
129
- assistantId,
130
- project,
131
- zone,
132
- userPrompt,
133
- sessionId,
134
- chatContext,
135
- }),
136
- });
137
- return await streamDoctorResponse(response, onProgress, onLog);
138
- } catch (err) {
139
- lastError = err;
140
- const errMsg = err instanceof Error ? err.message : String(err);
141
- const logMsg = `[doctor-client] Attempt ${attempt + 1}/${MAX_RETRIES} failed: ${errMsg}`;
142
- onLog?.(logMsg);
143
- if (attempt < MAX_RETRIES - 1) {
144
- await new Promise((resolve) => setTimeout(resolve, 500));
145
- }
146
- }
147
- }
148
-
149
- throw lastError;
150
- }
151
-
152
- export { callDoctorDaemon };
153
- export type { DoctorProgressCallback, DoctorResult };