@vellumai/cli 0.8.12-dev.202606152248.70317d3 → 0.8.12-dev.202606160320.367ef7c

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.
@@ -0,0 +1,224 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
3
+ import { createServer, type Server } from "node:http";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { getLocalAssistantStatus } from "../status";
8
+
9
+ let tempDir: string;
10
+ let lockfilePath: string;
11
+ let instanceDir: string;
12
+
13
+ function writeLockfile(entry: Record<string, unknown>): void {
14
+ writeFileSync(
15
+ lockfilePath,
16
+ JSON.stringify({
17
+ assistants: [entry],
18
+ activeAssistant: entry.assistantId,
19
+ }),
20
+ );
21
+ }
22
+
23
+ function writeLocalLockfile(overrides: Record<string, unknown> = {}): void {
24
+ writeLockfile({
25
+ assistantId: "local-1",
26
+ cloud: "local",
27
+ resources: {
28
+ instanceDir,
29
+ daemonPort: 30101,
30
+ gatewayPort: 30102,
31
+ qdrantPort: 30103,
32
+ },
33
+ ...overrides,
34
+ });
35
+ }
36
+
37
+ function writeAssistantPid(value: string): string {
38
+ const pidDir = path.join(instanceDir, ".vellum", "workspace");
39
+ mkdirSync(pidDir, { recursive: true });
40
+ const pidPath = path.join(pidDir, "vellum.pid");
41
+ writeFileSync(pidPath, value);
42
+ return pidPath;
43
+ }
44
+
45
+ function markStale(filePath: string): void {
46
+ const stale = new Date(Date.now() - 120_000);
47
+ utimesSync(filePath, stale, stale);
48
+ }
49
+
50
+ function listen(server: Server, port = 0): Promise<number> {
51
+ return new Promise((resolve) => {
52
+ server.listen(port, "127.0.0.1", () => {
53
+ const address = server.address();
54
+ if (!address || typeof address === "string") {
55
+ throw new Error("expected TCP server address");
56
+ }
57
+ resolve(address.port);
58
+ });
59
+ });
60
+ }
61
+
62
+ function close(server: Server): Promise<void> {
63
+ return new Promise((resolve, reject) => {
64
+ server.close((err) => (err ? reject(err) : resolve()));
65
+ });
66
+ }
67
+
68
+ async function unusedPort(): Promise<number> {
69
+ const server = createServer();
70
+ const port = await listen(server);
71
+ await close(server);
72
+ return port;
73
+ }
74
+
75
+ beforeEach(() => {
76
+ tempDir = path.join(
77
+ tmpdir(),
78
+ `vellum-local-status-test-${Date.now()}-${Math.random()}`,
79
+ );
80
+ mkdirSync(tempDir, { recursive: true });
81
+ lockfilePath = path.join(tempDir, "lockfile.json");
82
+ instanceDir = path.join(tempDir, "instance");
83
+ });
84
+
85
+ afterEach(() => {
86
+ rmSync(tempDir, { recursive: true, force: true });
87
+ });
88
+
89
+ describe("getLocalAssistantStatus", () => {
90
+ test("returns sleeping when the assistant PID file is absent", async () => {
91
+ writeLocalLockfile();
92
+
93
+ expect(await getLocalAssistantStatus([lockfilePath], "local-1")).toEqual({
94
+ ok: true,
95
+ state: "sleeping",
96
+ });
97
+ });
98
+
99
+ test("returns sleeping for legacy local entries without cloud/resources", async () => {
100
+ writeLockfile({
101
+ assistantId: "legacy-local",
102
+ baseDataDir: instanceDir,
103
+ runtimeUrl: "http://127.0.0.1:30102",
104
+ });
105
+
106
+ expect(
107
+ await getLocalAssistantStatus([lockfilePath], "legacy-local"),
108
+ ).toEqual({
109
+ ok: true,
110
+ state: "sleeping",
111
+ });
112
+ });
113
+
114
+ test("returns sleeping when the assistant PID file points at a dead process", async () => {
115
+ writeLocalLockfile();
116
+ writeAssistantPid("999999999");
117
+
118
+ const result = await getLocalAssistantStatus([lockfilePath], "local-1");
119
+
120
+ expect(result.ok).toBe(true);
121
+ if (result.ok) {
122
+ expect(result.state).toBe("sleeping");
123
+ }
124
+ });
125
+
126
+ test("returns crashed when the assistant PID file is invalid", async () => {
127
+ writeLocalLockfile();
128
+ writeAssistantPid("not-a-pid");
129
+
130
+ const result = await getLocalAssistantStatus([lockfilePath], "local-1");
131
+
132
+ expect(result.ok).toBe(true);
133
+ if (result.ok) {
134
+ expect(result.state).toBe("crashed");
135
+ expect(result.detail).toContain("PID file");
136
+ }
137
+ });
138
+
139
+ test("returns starting when a fresh assistant PID is alive but health is not ready yet", async () => {
140
+ writeLocalLockfile({
141
+ resources: {
142
+ instanceDir,
143
+ daemonPort: await unusedPort(),
144
+ gatewayPort: 30102,
145
+ qdrantPort: 30103,
146
+ },
147
+ });
148
+ writeAssistantPid(String(process.pid));
149
+
150
+ const result = await getLocalAssistantStatus([lockfilePath], "local-1");
151
+
152
+ expect(result.ok).toBe(true);
153
+ if (result.ok) {
154
+ expect(result.state).toBe("starting");
155
+ expect(result.pid).toBe(process.pid);
156
+ }
157
+ });
158
+
159
+ test("returns crashed when an old live assistant PID is still not responding", async () => {
160
+ writeLocalLockfile({
161
+ resources: {
162
+ instanceDir,
163
+ daemonPort: await unusedPort(),
164
+ gatewayPort: 30102,
165
+ qdrantPort: 30103,
166
+ },
167
+ });
168
+ const pidPath = writeAssistantPid(String(process.pid));
169
+ markStale(pidPath);
170
+
171
+ const result = await getLocalAssistantStatus([lockfilePath], "local-1");
172
+
173
+ expect(result.ok).toBe(true);
174
+ if (result.ok) {
175
+ expect(result.state).toBe("crashed");
176
+ expect(result.detail).toContain("not responding");
177
+ }
178
+ });
179
+
180
+ test("returns starting while the gateway is coming up for a freshly started assistant", async () => {
181
+ const server = createServer((_req, res) => {
182
+ res.writeHead(200, { "Content-Type": "application/json" });
183
+ res.end(JSON.stringify({ status: "healthy" }));
184
+ });
185
+ const daemonPort = await listen(server);
186
+ try {
187
+ writeLocalLockfile({
188
+ resources: {
189
+ instanceDir,
190
+ daemonPort,
191
+ gatewayPort: await unusedPort(),
192
+ qdrantPort: 30103,
193
+ },
194
+ });
195
+ writeAssistantPid(String(process.pid));
196
+
197
+ const result = await getLocalAssistantStatus([lockfilePath], "local-1");
198
+
199
+ expect(result.ok).toBe(true);
200
+ if (result.ok) {
201
+ expect(result.state).toBe("starting");
202
+ expect(result.pid).toBe(process.pid);
203
+ }
204
+ } finally {
205
+ await close(server);
206
+ }
207
+ });
208
+
209
+ test("rejects non-local assistants", async () => {
210
+ writeLockfile({
211
+ assistantId: "platform-1",
212
+ cloud: "vellum",
213
+ runtimeUrl: "https://example.com",
214
+ });
215
+
216
+ expect(await getLocalAssistantStatus([lockfilePath], "platform-1")).toEqual(
217
+ {
218
+ ok: false,
219
+ status: 404,
220
+ error: "Local assistant not found",
221
+ },
222
+ );
223
+ });
224
+ });
@@ -35,8 +35,15 @@ export { runHatch } from "./hatch";
35
35
  export type { HatchResult } from "./hatch";
36
36
  export { runRetire } from "./retire";
37
37
  export type { RetireResult } from "./retire";
38
+ export { runSleep } from "./sleep";
39
+ export type { SleepResult } from "./sleep";
38
40
  export { runWake } from "./wake";
39
41
  export type { WakeOptions, WakeResult } from "./wake";
42
+ export { getLocalAssistantStatus } from "./status";
43
+ export type {
44
+ LocalAssistantRuntimeState,
45
+ LocalAssistantStatusResult,
46
+ } from "./status";
40
47
  export { getGuardianAccessToken } from "./guardian-token";
41
48
  export type { TokenResult } from "./guardian-token";
42
49
  export {
@@ -31,6 +31,7 @@
31
31
  */
32
32
 
33
33
  export interface LocalAssistantResources {
34
+ instanceDir?: string;
34
35
  gatewayPort: number;
35
36
  daemonPort: number;
36
37
  }
@@ -69,7 +70,13 @@ function parseResources(value: unknown): LocalAssistantResources | undefined {
69
70
  if (!isRecord(value)) return undefined;
70
71
  if (typeof value.gatewayPort !== "number") return undefined;
71
72
  if (typeof value.daemonPort !== "number") return undefined;
72
- return { gatewayPort: value.gatewayPort, daemonPort: value.daemonPort };
73
+ return {
74
+ ...(typeof value.instanceDir === "string"
75
+ ? { instanceDir: value.instanceDir }
76
+ : {}),
77
+ gatewayPort: value.gatewayPort,
78
+ daemonPort: value.daemonPort,
79
+ };
73
80
  }
74
81
 
75
82
  /**
@@ -0,0 +1,80 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import type { CliInvocation } from "./util";
4
+
5
+ // The CLI's `sleep` command uses a 120s SIGKILL ceiling for the assistant
6
+ // daemon (WAL checkpoint can be slow on large databases) plus 7s for the
7
+ // gateway drain window. The wrapper timeout must sit above that total so a
8
+ // slow-but-succeeding sleep isn't killed and misreported as a timeout.
9
+ const SLEEP_TIMEOUT_MS = 150_000;
10
+
11
+ export type SleepResult =
12
+ | { ok: true }
13
+ | { ok: false; status: number; error: string };
14
+
15
+ /**
16
+ * Stop a local assistant's daemon and gateway via the CLI's `sleep --force`.
17
+ *
18
+ * Uses `--force` to bypass the active-call-lease guard — the restart flow
19
+ * immediately follows with a `wake`, so the brief interruption is expected
20
+ * and user-confirmed at the UI level.
21
+ *
22
+ * Mirrors {@link runRetire}'s never-reject contract so each host wires
23
+ * transport once and surfaces a structured failure rather than a thrown error.
24
+ */
25
+ export function runSleep(
26
+ invocation: CliInvocation,
27
+ assistantId: string,
28
+ ): Promise<SleepResult> {
29
+ return new Promise((resolve) => {
30
+ const child = spawn(
31
+ invocation.command,
32
+ [...invocation.baseArgs, "sleep", assistantId, "--force"],
33
+ { stdio: ["ignore", "pipe", "pipe"] },
34
+ );
35
+
36
+ let stdout = "";
37
+ let stderr = "";
38
+ let done = false;
39
+
40
+ const finish = (result: SleepResult) => {
41
+ if (done) return;
42
+ done = true;
43
+ clearTimeout(timeout);
44
+ resolve(result);
45
+ };
46
+
47
+ const timeout = setTimeout(() => {
48
+ child.kill("SIGTERM");
49
+ finish({
50
+ ok: false,
51
+ status: 500,
52
+ error: `Sleep timed out after ${SLEEP_TIMEOUT_MS / 1000} seconds`,
53
+ });
54
+ }, SLEEP_TIMEOUT_MS);
55
+
56
+ child.stdout.on("data", (data: Buffer) => {
57
+ stdout += data.toString();
58
+ });
59
+
60
+ child.stderr.on("data", (data: Buffer) => {
61
+ stderr += data.toString();
62
+ });
63
+
64
+ child.on("close", (code) => {
65
+ if (code === 0) {
66
+ finish({ ok: true });
67
+ } else {
68
+ finish({ ok: false, status: 500, error: stderr || stdout });
69
+ }
70
+ });
71
+
72
+ child.on("error", (err) => {
73
+ finish({
74
+ ok: false,
75
+ status: 500,
76
+ error: `Failed to spawn CLI: ${err.message}`,
77
+ });
78
+ });
79
+ });
80
+ }
@@ -0,0 +1,342 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import http from "node:http";
4
+ import path from "node:path";
5
+
6
+ import { SEEDS } from "@vellumai/environments";
7
+
8
+ import type {
9
+ LockfileAssistant,
10
+ } from "./lockfile-contract";
11
+ import { getLockfileData } from "./lockfile";
12
+
13
+ const HEALTH_TIMEOUT_MS = 1_500;
14
+ const STARTING_GRACE_MS = 60_000;
15
+ const PRODUCTION_ENVIRONMENT_NAME = "production";
16
+ const DEFAULT_PORTS = {
17
+ daemon: 7821,
18
+ gateway: 7830,
19
+ };
20
+
21
+ export type LocalAssistantRuntimeState =
22
+ | "healthy"
23
+ | "sleeping"
24
+ | "starting"
25
+ | "crashed"
26
+ | "unknown";
27
+
28
+ export type LocalAssistantStatusResult =
29
+ | {
30
+ ok: true;
31
+ state: LocalAssistantRuntimeState;
32
+ detail?: string;
33
+ pid?: number;
34
+ }
35
+ | { ok: false; status: number; error: string };
36
+
37
+ type PidState =
38
+ | { state: "missing" }
39
+ | { state: "starting"; updatedAtMs: number }
40
+ | { state: "alive"; pid: number; updatedAtMs: number }
41
+ | { state: "dead"; pid: number; updatedAtMs: number }
42
+ | { state: "invalid"; value: string; updatedAtMs: number };
43
+
44
+ interface StatusResources {
45
+ instanceDir: string;
46
+ gatewayPort: number;
47
+ daemonPort: number;
48
+ }
49
+
50
+ function getDaemonPidPath(instanceDir: string): string {
51
+ return path.join(instanceDir, ".vellum", "workspace", "vellum.pid");
52
+ }
53
+
54
+ function getGatewayPidPath(instanceDir: string): string {
55
+ return path.join(instanceDir, ".vellum", "gateway.pid");
56
+ }
57
+
58
+ function readPidState(pidFile: string): PidState {
59
+ if (!existsSync(pidFile)) return { state: "missing" };
60
+
61
+ const updatedAtMs = statSync(pidFile).mtimeMs;
62
+ const value = readFileSync(pidFile, "utf-8").trim();
63
+ if (!value) return { state: "missing" };
64
+ if (value === "starting") return { state: "starting", updatedAtMs };
65
+
66
+ const pid = Number(value);
67
+ if (!Number.isInteger(pid) || pid <= 0) {
68
+ return { state: "invalid", value, updatedAtMs };
69
+ }
70
+
71
+ try {
72
+ process.kill(pid, 0);
73
+ return { state: "alive", pid, updatedAtMs };
74
+ } catch {
75
+ return { state: "dead", pid, updatedAtMs };
76
+ }
77
+ }
78
+
79
+ function isFreshPidState(
80
+ pidState: PidState,
81
+ observedAtMs: number,
82
+ ): boolean {
83
+ return (
84
+ "updatedAtMs" in pidState &&
85
+ observedAtMs - pidState.updatedAtMs <= STARTING_GRACE_MS
86
+ );
87
+ }
88
+
89
+ function httpHealthCheck(port: number): Promise<boolean> {
90
+ return new Promise((resolve) => {
91
+ const req = http.get(
92
+ {
93
+ hostname: "127.0.0.1",
94
+ port,
95
+ path: "/healthz",
96
+ timeout: HEALTH_TIMEOUT_MS,
97
+ },
98
+ (res) => {
99
+ const chunks: Buffer[] = [];
100
+ res.on("data", (chunk: Buffer) => chunks.push(chunk));
101
+ res.on("end", () => {
102
+ if (res.statusCode !== 200) {
103
+ resolve(false);
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const body = JSON.parse(Buffer.concat(chunks).toString()) as {
109
+ status?: string;
110
+ };
111
+ resolve(
112
+ body.status === undefined ||
113
+ body.status === "healthy" ||
114
+ body.status === "ok",
115
+ );
116
+ } catch {
117
+ resolve(true);
118
+ }
119
+ });
120
+ },
121
+ );
122
+
123
+ req.on("timeout", () => {
124
+ req.destroy();
125
+ resolve(false);
126
+ });
127
+ req.on("error", () => resolve(false));
128
+ });
129
+ }
130
+
131
+ function localOnlyEntry(
132
+ entry: LockfileAssistant | undefined,
133
+ ): LockfileAssistant | null {
134
+ if (!entry || (entry.cloud != null && entry.cloud !== "local")) return null;
135
+ return entry;
136
+ }
137
+
138
+ function isRecord(value: unknown): value is Record<string, unknown> {
139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
140
+ }
141
+
142
+ function parsePortFromUrl(url: unknown): number | undefined {
143
+ if (typeof url !== "string") return undefined;
144
+ try {
145
+ const parsed = new URL(url);
146
+ const port = Number(parsed.port);
147
+ return Number.isInteger(port) && port > 0 ? port : undefined;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ function defaultPorts(env: Record<string, string | undefined>): {
154
+ daemon: number;
155
+ gateway: number;
156
+ } {
157
+ const envName = env.VELLUM_ENVIRONMENT?.trim() || PRODUCTION_ENVIRONMENT_NAME;
158
+ const seed = SEEDS[envName] ?? SEEDS[PRODUCTION_ENVIRONMENT_NAME];
159
+ return {
160
+ daemon: seed?.portsOverride?.daemon ?? DEFAULT_PORTS.daemon,
161
+ gateway: seed?.portsOverride?.gateway ?? DEFAULT_PORTS.gateway,
162
+ };
163
+ }
164
+
165
+ function defaultInstanceDir(
166
+ env: Record<string, string | undefined>,
167
+ assistantId: string,
168
+ ): string {
169
+ const envName = env.VELLUM_ENVIRONMENT?.trim() || PRODUCTION_ENVIRONMENT_NAME;
170
+ const xdgDataHome =
171
+ env.XDG_DATA_HOME?.trim() || path.join(os.homedir(), ".local", "share");
172
+ const dataRoot =
173
+ envName === PRODUCTION_ENVIRONMENT_NAME ? "vellum" : `vellum-${envName}`;
174
+ return path.join(xdgDataHome, dataRoot, "assistants", assistantId);
175
+ }
176
+
177
+ function firstString(...values: unknown[]): string | undefined {
178
+ for (const value of values) {
179
+ if (typeof value === "string" && value.length > 0) return value;
180
+ }
181
+ return undefined;
182
+ }
183
+
184
+ function firstNumber(...values: unknown[]): number | undefined {
185
+ for (const value of values) {
186
+ if (typeof value === "number" && Number.isFinite(value)) return value;
187
+ }
188
+ return undefined;
189
+ }
190
+
191
+ function findRawAssistant(
192
+ lockfilePaths: string[],
193
+ assistantId: string,
194
+ ): Record<string, unknown> | null {
195
+ for (const candidate of lockfilePaths) {
196
+ let data: unknown;
197
+ try {
198
+ data = JSON.parse(readFileSync(candidate, "utf-8"));
199
+ } catch {
200
+ continue;
201
+ }
202
+ if (!isRecord(data) || !Array.isArray(data.assistants)) return null;
203
+ const entry = data.assistants.find(
204
+ (assistant) =>
205
+ isRecord(assistant) && assistant.assistantId === assistantId,
206
+ );
207
+ return isRecord(entry) ? entry : null;
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function resolveStatusResources(
213
+ entry: LockfileAssistant,
214
+ rawEntry: Record<string, unknown> | null,
215
+ env: Record<string, string | undefined>,
216
+ ): StatusResources {
217
+ const rawResources = isRecord(rawEntry?.resources)
218
+ ? rawEntry.resources
219
+ : undefined;
220
+ const ports = defaultPorts(env);
221
+ const instanceDir =
222
+ firstString(
223
+ entry.resources?.instanceDir,
224
+ rawResources?.instanceDir,
225
+ rawEntry?.baseDataDir,
226
+ ) ?? defaultInstanceDir(env, entry.assistantId);
227
+ return {
228
+ instanceDir,
229
+ daemonPort:
230
+ firstNumber(entry.resources?.daemonPort, rawResources?.daemonPort) ??
231
+ ports.daemon,
232
+ gatewayPort:
233
+ firstNumber(entry.resources?.gatewayPort, rawResources?.gatewayPort) ??
234
+ parsePortFromUrl(rawEntry?.localUrl) ??
235
+ parsePortFromUrl(rawEntry?.runtimeUrl ?? entry.runtimeUrl) ??
236
+ ports.gateway,
237
+ };
238
+ }
239
+
240
+ async function runtimeStatusForEntry(
241
+ entry: LockfileAssistant,
242
+ rawEntry: Record<string, unknown> | null,
243
+ env: Record<string, string | undefined>,
244
+ ): Promise<LocalAssistantStatusResult> {
245
+ const resources = resolveStatusResources(entry, rawEntry, env);
246
+ const observedAtMs = Date.now();
247
+
248
+ const assistantPid = readPidState(getDaemonPidPath(resources.instanceDir));
249
+ if (assistantPid.state === "missing") {
250
+ return { ok: true, state: "sleeping" };
251
+ }
252
+ if (assistantPid.state === "starting") {
253
+ return { ok: true, state: "starting" };
254
+ }
255
+ if (assistantPid.state === "dead") {
256
+ return { ok: true, state: "sleeping", pid: assistantPid.pid };
257
+ }
258
+ if (assistantPid.state === "invalid") {
259
+ return {
260
+ ok: true,
261
+ state: "crashed",
262
+ detail: "assistant PID file is invalid",
263
+ };
264
+ }
265
+
266
+ const assistantHealthy = await httpHealthCheck(resources.daemonPort);
267
+ if (!assistantHealthy) {
268
+ if (isFreshPidState(assistantPid, observedAtMs)) {
269
+ return { ok: true, state: "starting", pid: assistantPid.pid };
270
+ }
271
+ return {
272
+ ok: true,
273
+ state: "crashed",
274
+ pid: assistantPid.pid,
275
+ detail: "assistant process is not responding",
276
+ };
277
+ }
278
+
279
+ const gatewayPid = readPidState(getGatewayPidPath(resources.instanceDir));
280
+ if (gatewayPid.state === "starting") {
281
+ return { ok: true, state: "starting", pid: assistantPid.pid };
282
+ }
283
+ if (gatewayPid.state !== "alive") {
284
+ if (
285
+ isFreshPidState(assistantPid, observedAtMs) ||
286
+ isFreshPidState(gatewayPid, observedAtMs)
287
+ ) {
288
+ return { ok: true, state: "starting", pid: assistantPid.pid };
289
+ }
290
+ return {
291
+ ok: true,
292
+ state: "crashed",
293
+ pid: assistantPid.pid,
294
+ detail: "gateway process is not running",
295
+ };
296
+ }
297
+
298
+ const gatewayHealthy = await httpHealthCheck(resources.gatewayPort);
299
+ if (!gatewayHealthy) {
300
+ if (isFreshPidState(gatewayPid, observedAtMs)) {
301
+ return { ok: true, state: "starting", pid: gatewayPid.pid };
302
+ }
303
+ return {
304
+ ok: true,
305
+ state: "crashed",
306
+ pid: gatewayPid.pid,
307
+ detail: "gateway process is not responding",
308
+ };
309
+ }
310
+
311
+ return { ok: true, state: "healthy", pid: assistantPid.pid };
312
+ }
313
+
314
+ export async function getLocalAssistantStatus(
315
+ lockfilePaths: string[],
316
+ assistantId: string,
317
+ env: Record<string, string | undefined> = process.env,
318
+ ): Promise<LocalAssistantStatusResult> {
319
+ const result = getLockfileData(lockfilePaths);
320
+ if (!result.ok) {
321
+ return {
322
+ ok: false,
323
+ status: result.status,
324
+ error: result.error ?? "Failed to read lockfile",
325
+ };
326
+ }
327
+
328
+ const entry = localOnlyEntry(
329
+ result.data.assistants.find(
330
+ (assistant) => assistant.assistantId === assistantId,
331
+ ),
332
+ );
333
+ if (!entry) {
334
+ return { ok: false, status: 404, error: "Local assistant not found" };
335
+ }
336
+
337
+ return runtimeStatusForEntry(
338
+ entry,
339
+ findRawAssistant(lockfilePaths, assistantId),
340
+ env,
341
+ );
342
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.8.12-dev.202606152248.70317d3",
3
+ "version": "0.8.12-dev.202606160320.367ef7c",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {