@vellumai/cli 0.8.1 → 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.
@@ -110,6 +110,21 @@ export function getStateDir(env: EnvironmentDefinition): string {
110
110
  return join(xdgDataHome(), `vellum-${env.name}`);
111
111
  }
112
112
 
113
+ /**
114
+ * Path to the interactive CLI's input history file.
115
+ *
116
+ * Follows the XDG Base Directory spec: history files are state data
117
+ * (persistent across runs but not portable / user-owned content), so they
118
+ * belong under `$XDG_STATE_HOME`, mirroring `bash`, `zsh`, `psql`, and `gh`.
119
+ * Defaults to `~/.local/state/vellum/input-history`.
120
+ *
121
+ * Not environment-scoped: terminal input history is per-user, not per-assistant,
122
+ * so dev and prod CLIs share the same history file.
123
+ */
124
+ export function getInputHistoryPath(): string {
125
+ return join(xdgStateHome(), "vellum", "input-history");
126
+ }
127
+
113
128
  /**
114
129
  * Named port constants derived from `DEFAULT_PORTS`.
115
130
  * These are the ports the assistant and gateway services bind to *inside*
@@ -127,3 +142,9 @@ function xdgDataHome(): string {
127
142
  function xdgConfigHome(): string {
128
143
  return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
129
144
  }
145
+
146
+ function xdgStateHome(): string {
147
+ return (
148
+ process.env.XDG_STATE_HOME?.trim() || join(homedir(), ".local", "state")
149
+ );
150
+ }
@@ -1,16 +1,13 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { homedir } from "os";
3
- import { dirname, join } from "path";
2
+ import { dirname } from "path";
4
3
 
5
- const MAX_ENTRIES = 1000;
4
+ import { getInputHistoryPath } from "./environments/paths.js";
6
5
 
7
- function historyPath(): string {
8
- return join(homedir(), ".vellum", "input-history");
9
- }
6
+ const MAX_ENTRIES = 1000;
10
7
 
11
8
  export function loadHistory(): string[] {
12
9
  try {
13
- const path = historyPath();
10
+ const path = getInputHistoryPath();
14
11
  if (!existsSync(path)) return [];
15
12
  const content = readFileSync(path, "utf-8");
16
13
  return content
@@ -26,7 +23,7 @@ export function appendHistory(entry: string): void {
26
23
  const trimmed = entry.trim();
27
24
  if (!trimmed || trimmed.startsWith("/")) return;
28
25
  try {
29
- const path = historyPath();
26
+ const path = getInputHistoryPath();
30
27
  const dir = dirname(path);
31
28
  if (!existsSync(dir)) {
32
29
  mkdirSync(dir, { recursive: true });
@@ -0,0 +1,413 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ import { LLM_PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
4
+
5
+ export type LlmProviderId = keyof typeof LLM_PROVIDER_ENV_VAR_NAMES;
6
+
7
+ export type ProviderApiKeySource = "env" | "prompt";
8
+ export type ProviderSecretFetch = (
9
+ input: Parameters<typeof fetch>[0],
10
+ init?: Parameters<typeof fetch>[1],
11
+ ) => ReturnType<typeof fetch>;
12
+
13
+ export type EnsureProviderApiKeyResult =
14
+ | {
15
+ status: "already_configured";
16
+ provider: LlmProviderId;
17
+ }
18
+ | {
19
+ status: "configured";
20
+ provider: LlmProviderId;
21
+ source: ProviderApiKeySource;
22
+ }
23
+ | {
24
+ status: "missing";
25
+ provider: LlmProviderId;
26
+ message: string;
27
+ }
28
+ | {
29
+ status: "failed";
30
+ provider: LlmProviderId;
31
+ message: string;
32
+ }
33
+ | {
34
+ status: "skipped";
35
+ message: string;
36
+ };
37
+
38
+ export interface EnsureProviderApiKeyOptions {
39
+ gatewayUrl: string;
40
+ provider: string | null;
41
+ bearerToken?: string;
42
+ env?: NodeJS.ProcessEnv;
43
+ fetchImpl?: ProviderSecretFetch;
44
+ prompt?: (prompt: string) => Promise<string>;
45
+ stdinIsTTY?: boolean;
46
+ input?: NodeJS.ReadStream;
47
+ output?: NodeJS.WriteStream;
48
+ }
49
+
50
+ export interface GatewayApiKeyReadResult {
51
+ found: boolean;
52
+ unreachable: boolean;
53
+ }
54
+
55
+ const PROVIDER_LABELS: Record<LlmProviderId, string> = {
56
+ anthropic: "Anthropic",
57
+ openai: "OpenAI",
58
+ gemini: "Gemini",
59
+ fireworks: "Fireworks",
60
+ openrouter: "OpenRouter",
61
+ };
62
+
63
+ export function formatProviderName(provider: LlmProviderId): string {
64
+ return PROVIDER_LABELS[provider];
65
+ }
66
+
67
+ export function isSupportedLlmProvider(
68
+ provider: string,
69
+ ): provider is LlmProviderId {
70
+ return Object.hasOwn(LLM_PROVIDER_ENV_VAR_NAMES, provider);
71
+ }
72
+
73
+ function gatewayUrlWithPath(gatewayUrl: string, path: string): string {
74
+ return `${gatewayUrl.replace(/\/+$/, "")}${path}`;
75
+ }
76
+
77
+ function secretHeaders(bearerToken?: string): Record<string, string> {
78
+ const headers: Record<string, string> = {
79
+ "Content-Type": "application/json",
80
+ Accept: "application/json",
81
+ };
82
+ if (bearerToken) {
83
+ headers.Authorization = `Bearer ${bearerToken}`;
84
+ }
85
+ return headers;
86
+ }
87
+
88
+ async function parseErrorMessage(response: Response): Promise<string> {
89
+ let text = "";
90
+ try {
91
+ text = await response.text();
92
+ } catch {
93
+ // Fall through to status text.
94
+ }
95
+
96
+ try {
97
+ const body = JSON.parse(text) as {
98
+ error?: unknown;
99
+ };
100
+ const message = extractErrorMessage(body.error);
101
+ if (message) return message;
102
+ } catch {
103
+ // Fall back to raw text below.
104
+ }
105
+
106
+ if (text.trim().length > 0) {
107
+ return text.trim();
108
+ }
109
+
110
+ return response.statusText || `HTTP ${response.status}`;
111
+ }
112
+
113
+ function extractErrorMessage(error: unknown): string | null {
114
+ if (typeof error === "string" && error.trim().length > 0) {
115
+ return error.trim();
116
+ }
117
+ if (!error || typeof error !== "object") {
118
+ return null;
119
+ }
120
+
121
+ const maybeMessage = (error as { message?: unknown }).message;
122
+ if (typeof maybeMessage === "string" && maybeMessage.trim().length > 0) {
123
+ return maybeMessage.trim();
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ export async function readGatewayApiKey(
130
+ gatewayUrl: string,
131
+ provider: LlmProviderId,
132
+ bearerToken?: string,
133
+ fetchImpl: ProviderSecretFetch = fetch,
134
+ ): Promise<GatewayApiKeyReadResult> {
135
+ const response = await fetchImpl(
136
+ gatewayUrlWithPath(gatewayUrl, "/v1/secrets/read"),
137
+ {
138
+ method: "POST",
139
+ headers: secretHeaders(bearerToken),
140
+ body: JSON.stringify({
141
+ type: "api_key",
142
+ name: provider,
143
+ reveal: false,
144
+ }),
145
+ signal: AbortSignal.timeout(10_000),
146
+ },
147
+ );
148
+
149
+ if (!response.ok) {
150
+ const message = await parseErrorMessage(response);
151
+ if (response.status === 404) {
152
+ throw new Error(
153
+ `Active assistant at ${gatewayUrl} does not expose /v1/secrets/read (${message}). Run \`vellum ps\` to confirm the active assistant, then select a self-hosted assistant with \`vellum use <assistant>\` or wake a current assistant before running setup.`,
154
+ );
155
+ }
156
+ throw new Error(
157
+ `Failed to check ${formatProviderName(provider)} API key: ${message}`,
158
+ );
159
+ }
160
+
161
+ const body = (await response.json()) as {
162
+ found?: unknown;
163
+ unreachable?: unknown;
164
+ };
165
+ return {
166
+ found: body.found === true,
167
+ unreachable: body.unreachable === true,
168
+ };
169
+ }
170
+
171
+ export async function injectGatewayApiKey(
172
+ gatewayUrl: string,
173
+ provider: LlmProviderId,
174
+ value: string,
175
+ bearerToken?: string,
176
+ fetchImpl: ProviderSecretFetch = fetch,
177
+ ): Promise<void> {
178
+ const response = await fetchImpl(
179
+ gatewayUrlWithPath(gatewayUrl, "/v1/secrets"),
180
+ {
181
+ method: "POST",
182
+ headers: secretHeaders(bearerToken),
183
+ body: JSON.stringify({
184
+ type: "api_key",
185
+ name: provider,
186
+ value,
187
+ }),
188
+ signal: AbortSignal.timeout(10_000),
189
+ },
190
+ );
191
+
192
+ if (!response.ok) {
193
+ const message = await parseErrorMessage(response);
194
+ throw new Error(
195
+ `Failed to store ${formatProviderName(provider)} API key: ${message}`,
196
+ );
197
+ }
198
+
199
+ const body = (await response.json().catch(() => ({}))) as {
200
+ success?: unknown;
201
+ error?: unknown;
202
+ };
203
+ if (body.success === false) {
204
+ const message =
205
+ typeof body.error === "string" && body.error.trim().length > 0
206
+ ? body.error
207
+ : "Assistant rejected the API key.";
208
+ throw new Error(message);
209
+ }
210
+ }
211
+
212
+ export async function promptSecret(
213
+ prompt: string,
214
+ streams: {
215
+ input?: NodeJS.ReadStream;
216
+ output?: NodeJS.WriteStream;
217
+ } = {},
218
+ ): Promise<string> {
219
+ const input = streams.input ?? process.stdin;
220
+ const output = streams.output ?? process.stdout;
221
+
222
+ const restoreEcho = disableTerminalEcho(input);
223
+ output.write(prompt);
224
+
225
+ return new Promise((resolve, reject) => {
226
+ const wasRaw = input.isRaw;
227
+ if (input.isTTY) {
228
+ input.setRawMode(true);
229
+ }
230
+ input.resume();
231
+
232
+ let value = "";
233
+
234
+ const cleanup = (): void => {
235
+ input.removeListener("data", onData);
236
+ if (input.isTTY) {
237
+ input.setRawMode(wasRaw ?? false);
238
+ }
239
+ restoreEcho();
240
+ input.pause();
241
+ };
242
+
243
+ const finish = (): void => {
244
+ cleanup();
245
+ output.write("\n");
246
+ resolve(value);
247
+ };
248
+
249
+ const cancel = (): void => {
250
+ cleanup();
251
+ output.write("\n");
252
+ reject(new Error("Input cancelled."));
253
+ };
254
+
255
+ const onData = (chunk: Buffer | string): void => {
256
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
257
+ if (bytes[0] === 27) {
258
+ return;
259
+ }
260
+
261
+ for (const byte of bytes) {
262
+ if (byte === 3) {
263
+ cancel();
264
+ return;
265
+ }
266
+ if (byte === 10 || byte === 13) {
267
+ finish();
268
+ return;
269
+ }
270
+ if (byte === 8 || byte === 127) {
271
+ value = value.slice(0, -1);
272
+ continue;
273
+ }
274
+ if (byte >= 32 && byte <= 126) {
275
+ value += String.fromCharCode(byte);
276
+ }
277
+ }
278
+ };
279
+
280
+ input.on("data", onData);
281
+ });
282
+ }
283
+
284
+ function disableTerminalEcho(input: NodeJS.ReadStream): () => void {
285
+ if (input !== process.stdin || !input.isTTY || process.platform === "win32") {
286
+ return () => {};
287
+ }
288
+
289
+ const currentState = spawnSync("stty", ["-g"], {
290
+ encoding: "utf8",
291
+ stdio: ["inherit", "pipe", "ignore"],
292
+ });
293
+ const state = currentState.stdout.trim();
294
+ if (currentState.status !== 0 || state.length === 0) {
295
+ return () => {};
296
+ }
297
+
298
+ const disabled = spawnSync("stty", ["-echo"], {
299
+ stdio: ["inherit", "ignore", "ignore"],
300
+ });
301
+ if (disabled.status !== 0) {
302
+ return () => {};
303
+ }
304
+
305
+ let restored = false;
306
+ return () => {
307
+ if (restored) {
308
+ return;
309
+ }
310
+ restored = true;
311
+ spawnSync("stty", [state], {
312
+ stdio: ["inherit", "ignore", "ignore"],
313
+ });
314
+ };
315
+ }
316
+
317
+ export async function ensureProviderApiKey(
318
+ options: EnsureProviderApiKeyOptions,
319
+ ): Promise<EnsureProviderApiKeyResult> {
320
+ if (options.provider === null) {
321
+ return {
322
+ status: "skipped",
323
+ message: "Selected provider does not require an API key.",
324
+ };
325
+ }
326
+
327
+ const normalizedProvider = options.provider.trim().toLowerCase();
328
+ if (!isSupportedLlmProvider(normalizedProvider)) {
329
+ throw new Error(
330
+ `Provider '${options.provider}' does not have a supported API-key setup flow.`,
331
+ );
332
+ }
333
+ const provider = normalizedProvider;
334
+ const providerName = formatProviderName(provider);
335
+ const envVarName = LLM_PROVIDER_ENV_VAR_NAMES[provider];
336
+ const fetchImpl = options.fetchImpl ?? fetch;
337
+
338
+ const existing = await readGatewayApiKey(
339
+ options.gatewayUrl,
340
+ provider,
341
+ options.bearerToken,
342
+ fetchImpl,
343
+ );
344
+ if (existing.unreachable) {
345
+ return {
346
+ status: "failed",
347
+ provider,
348
+ message:
349
+ "Assistant credential store is unavailable. Try again after the assistant finishes starting.",
350
+ };
351
+ }
352
+ if (existing.found) {
353
+ return {
354
+ status: "already_configured",
355
+ provider,
356
+ };
357
+ }
358
+
359
+ const envValue = options.env?.[envVarName]?.trim();
360
+ let apiKey = envValue;
361
+ let source: ProviderApiKeySource = "env";
362
+
363
+ if (!apiKey) {
364
+ source = "prompt";
365
+ if (options.prompt) {
366
+ apiKey = (
367
+ await options.prompt(
368
+ `Enter your ${providerName} API key (${envVarName}): `,
369
+ )
370
+ ).trim();
371
+ } else {
372
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY;
373
+ if (!stdinIsTTY) {
374
+ return {
375
+ status: "missing",
376
+ provider,
377
+ message: `Missing ${envVarName}. Set it in the environment or run vellum setup from an interactive terminal.`,
378
+ };
379
+ }
380
+ apiKey = (
381
+ await promptSecret(
382
+ `Enter your ${providerName} API key (${envVarName}): `,
383
+ {
384
+ input: options.input,
385
+ output: options.output,
386
+ },
387
+ )
388
+ ).trim();
389
+ }
390
+ }
391
+
392
+ if (!apiKey) {
393
+ return {
394
+ status: "missing",
395
+ provider,
396
+ message: "API key cannot be empty.",
397
+ };
398
+ }
399
+
400
+ await injectGatewayApiKey(
401
+ options.gatewayUrl,
402
+ provider,
403
+ apiKey,
404
+ options.bearerToken,
405
+ fetchImpl,
406
+ );
407
+
408
+ return {
409
+ status: "configured",
410
+ provider,
411
+ source,
412
+ };
413
+ }
@@ -56,9 +56,7 @@ export async function syncCloudAssistants(
56
56
  const log = options?.log;
57
57
  const platformUrl = getPlatformUrl();
58
58
  log?.(`Platform URL: ${platformUrl}`);
59
- log?.(
60
- `Token found (${token.length} chars, prefix: ${token.slice(0, 6)}…)`,
61
- );
59
+ log?.(`Token found (${token.length} chars, prefix: ${token.slice(0, 6)}…)`);
62
60
 
63
61
  // Fetch user info for the login status line
64
62
  let email: string | undefined;
@@ -94,27 +92,41 @@ export async function syncCloudAssistants(
94
92
  const platformIds = new Set(platformAssistants.map((a) => a.id));
95
93
 
96
94
  // Add new platform assistants not yet in the lockfile
97
- const existingCloudIds = new Set(
98
- loadAllAssistants()
99
- .filter((a) => a.cloud === "vellum")
100
- .map((a) => a.assistantId),
95
+ const existingCloudEntries = loadAllAssistants().filter(
96
+ (a) => a.cloud === "vellum",
101
97
  );
98
+ const existingCloudById = new Map(
99
+ existingCloudEntries.map((a) => [a.assistantId, a]),
100
+ );
101
+ const existingCloudIds = new Set(existingCloudById.keys());
102
102
  log?.(
103
103
  `Lockfile has ${existingCloudIds.size} cloud assistant(s): ${[...existingCloudIds].join(", ") || "(none)"}`,
104
104
  );
105
105
 
106
106
  let added = 0;
107
+ let updated = 0;
107
108
  for (const pa of platformAssistants) {
108
- if (!existingCloudIds.has(pa.id)) {
109
+ const existing = existingCloudById.get(pa.id);
110
+ const assistantName = pa.name.trim();
111
+ const nameFields = assistantName ? { name: assistantName } : {};
112
+ if (!existing) {
109
113
  log?.(`Adding ${pa.name || pa.id} to lockfile`);
110
114
  saveAssistantEntry({
111
115
  assistantId: pa.id,
116
+ ...nameFields,
112
117
  runtimeUrl: getPlatformUrl(),
113
118
  cloud: "vellum",
114
119
  species: "vellum",
115
120
  hatchedAt: new Date().toISOString(),
116
121
  });
117
122
  added++;
123
+ } else if (assistantName && existing.name !== assistantName) {
124
+ log?.(`Updating ${pa.id} name to ${assistantName}`);
125
+ saveAssistantEntry({
126
+ ...existing,
127
+ name: assistantName,
128
+ });
129
+ updated++;
118
130
  }
119
131
  }
120
132
 
@@ -128,6 +140,8 @@ export async function syncCloudAssistants(
128
140
  }
129
141
  }
130
142
 
131
- log?.(`Sync complete: ${added} added, ${removed} removed`);
143
+ log?.(
144
+ `Sync complete: ${added} added, ${updated} updated, ${removed} removed`,
145
+ );
132
146
  return { added, removed, email };
133
147
  }
@@ -26,6 +26,9 @@ export const LLM_PROVIDER_ENV_VAR_NAMES: Record<string, string> = {
26
26
  gemini: "GEMINI_API_KEY",
27
27
  fireworks: "FIREWORKS_API_KEY",
28
28
  openrouter: "OPENROUTER_API_KEY",
29
+ zai: "ZAI_API_KEY",
30
+ deepseek: "DEEPSEEK_API_KEY",
31
+ minimax: "MINIMAX_API_KEY",
29
32
  };
30
33
 
31
34
  /** Search-provider env var names. Mirrors `SEARCH_PROVIDER_CATALOG` BYOK entries. */
@@ -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 };