@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.
- package/package.json +1 -1
- package/src/__tests__/backup.test.ts +13 -3
- package/src/__tests__/input-history.test.ts +102 -0
- package/src/__tests__/orphan-detection.test.ts +287 -0
- package/src/__tests__/preload.ts +5 -1
- package/src/__tests__/provider-secrets.test.ts +290 -0
- package/src/__tests__/ps-platform-status.test.ts +182 -0
- package/src/__tests__/search-provider-env-var-parity.test.ts +48 -0
- package/src/__tests__/setup.test.ts +296 -0
- package/src/__tests__/sync-events.test.ts +54 -0
- package/src/__tests__/teleport.test.ts +190 -163
- package/src/commands/client.ts +128 -10
- package/src/commands/events.ts +13 -1
- package/src/commands/login.ts +3 -2
- package/src/commands/ps.ts +28 -17
- package/src/commands/setup.ts +101 -96
- package/src/components/DefaultMainScreen.tsx +80 -128
- package/src/lib/__tests__/docker.test.ts +11 -0
- package/src/lib/assistant-config.ts +69 -2
- package/src/lib/client-identity.ts +1 -0
- package/src/lib/environments/paths.ts +21 -0
- package/src/lib/input-history.ts +5 -8
- package/src/lib/orphan-detection.ts +66 -1
- package/src/lib/platform-client.ts +8 -7
- package/src/lib/provider-secrets.ts +413 -0
- package/src/lib/statefulset.ts +12 -0
- package/src/lib/sync-cloud-assistants.ts +39 -18
- package/src/lib/upgrade-lifecycle.ts +9 -73
- package/src/shared/provider-env-vars.ts +15 -8
- package/src/lib/doctor-client.ts +0 -153
|
@@ -20,7 +20,19 @@ const testDir = mkdtempSync(join(tmpdir(), "cli-teleport-test-"));
|
|
|
20
20
|
process.env.VELLUM_LOCKFILE_DIR = testDir;
|
|
21
21
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
|
-
// Mocks — must be set up before importing the module under test
|
|
23
|
+
// Mocks — must be set up before importing the module under test.
|
|
24
|
+
//
|
|
25
|
+
// We use Bun's `mock.module()` API (rather than `spyOn` on the imported
|
|
26
|
+
// module namespace). `spyOn` mutates the shared module namespace, and
|
|
27
|
+
// `mockRestore()` in `afterAll` was not reliably restoring on CI — the
|
|
28
|
+
// mutations leaked into sibling test files (notably `guardian-token.test.ts`)
|
|
29
|
+
// and caused flaky failures when the runtime function for that file returned
|
|
30
|
+
// teleport's mock value instead of the real implementation.
|
|
31
|
+
//
|
|
32
|
+
// `mock.module()` registers a per-module factory in Bun's loader. To prevent
|
|
33
|
+
// the mock from leaking across files in the same `bun test` run, we capture
|
|
34
|
+
// the real exports up front and re-register them in `afterAll` to restore
|
|
35
|
+
// the original module bindings.
|
|
24
36
|
// ---------------------------------------------------------------------------
|
|
25
37
|
|
|
26
38
|
import * as assistantConfig from "../lib/assistant-config.js";
|
|
@@ -28,83 +40,95 @@ import * as guardianToken from "../lib/guardian-token.js";
|
|
|
28
40
|
import * as platformClient from "../lib/platform-client.js";
|
|
29
41
|
import * as localRuntimeClient from "../lib/local-runtime-client.js";
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} as unknown as
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
43
|
+
// Snapshot the real exports before any `mock.module()` call so we can
|
|
44
|
+
// reliably restore them after this file's tests complete, regardless of
|
|
45
|
+
// how Bun's loader treats namespace bindings post-mock.
|
|
46
|
+
const realAssistantConfig = { ...assistantConfig };
|
|
47
|
+
const realGuardianToken = { ...guardianToken };
|
|
48
|
+
const realPlatformClient = { ...platformClient };
|
|
49
|
+
const realLocalRuntimeClient = { ...localRuntimeClient };
|
|
50
|
+
|
|
51
|
+
const findAssistantByNameMock = mock<
|
|
52
|
+
typeof assistantConfig.findAssistantByName
|
|
53
|
+
>(() => null);
|
|
54
|
+
const saveAssistantEntryMock = mock<typeof assistantConfig.saveAssistantEntry>(
|
|
55
|
+
() => {},
|
|
56
|
+
);
|
|
57
|
+
const loadAllAssistantsMock = mock<typeof assistantConfig.loadAllAssistants>(
|
|
58
|
+
() => [],
|
|
59
|
+
);
|
|
60
|
+
const removeAssistantEntryMock = mock<
|
|
61
|
+
typeof assistantConfig.removeAssistantEntry
|
|
62
|
+
>(() => {});
|
|
63
|
+
|
|
64
|
+
mock.module("../lib/assistant-config.js", () => ({
|
|
65
|
+
...realAssistantConfig,
|
|
66
|
+
findAssistantByName: findAssistantByNameMock,
|
|
67
|
+
saveAssistantEntry: saveAssistantEntryMock,
|
|
68
|
+
loadAllAssistants: loadAllAssistantsMock,
|
|
69
|
+
removeAssistantEntry: removeAssistantEntryMock,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const loadGuardianTokenMock = mock<typeof guardianToken.loadGuardianToken>(
|
|
73
|
+
() =>
|
|
74
|
+
({
|
|
75
|
+
accessToken: "local-token",
|
|
76
|
+
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
77
|
+
}) as unknown as ReturnType<typeof guardianToken.loadGuardianToken>,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const leaseGuardianTokenMock = mock<typeof guardianToken.leaseGuardianToken>(
|
|
81
|
+
async () =>
|
|
82
|
+
({
|
|
83
|
+
accessToken: "leased-token",
|
|
84
|
+
accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
85
|
+
}) as unknown as Awaited<
|
|
86
|
+
ReturnType<typeof guardianToken.leaseGuardianToken>
|
|
87
|
+
>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const computeDeviceIdMock = mock<typeof guardianToken.computeDeviceId>(
|
|
91
|
+
() => "device-id-123",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
mock.module("../lib/guardian-token.js", () => ({
|
|
95
|
+
...realGuardianToken,
|
|
96
|
+
loadGuardianToken: loadGuardianTokenMock,
|
|
97
|
+
leaseGuardianToken: leaseGuardianTokenMock,
|
|
98
|
+
computeDeviceId: computeDeviceIdMock,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const readPlatformTokenMock = mock<typeof platformClient.readPlatformToken>(
|
|
102
|
+
() => "platform-token",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const getPlatformUrlMock = mock<typeof platformClient.getPlatformUrl>(
|
|
106
|
+
() => "https://platform.vellum.ai",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const hatchAssistantMock = mock<typeof platformClient.hatchAssistant>(
|
|
110
|
+
async () => ({
|
|
111
|
+
assistant: {
|
|
112
|
+
id: "platform-new-id",
|
|
113
|
+
name: "platform-new",
|
|
114
|
+
status: "active",
|
|
115
|
+
},
|
|
116
|
+
reusedExisting: false,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
93
119
|
|
|
94
|
-
const platformPollJobStatusMock =
|
|
95
|
-
platformClient
|
|
96
|
-
|
|
97
|
-
).mockResolvedValue({
|
|
120
|
+
const platformPollJobStatusMock = mock<
|
|
121
|
+
typeof platformClient.platformPollJobStatus
|
|
122
|
+
>(async () => ({
|
|
98
123
|
jobId: "platform-job-1",
|
|
99
124
|
type: "export",
|
|
100
125
|
status: "complete",
|
|
101
126
|
bundleKey: "platform-bundle-key-abc",
|
|
102
|
-
});
|
|
127
|
+
}));
|
|
103
128
|
|
|
104
|
-
const platformRequestSignedUrlMock =
|
|
105
|
-
platformClient
|
|
106
|
-
|
|
107
|
-
).mockImplementation(async (params) => ({
|
|
129
|
+
const platformRequestSignedUrlMock = mock<
|
|
130
|
+
typeof platformClient.platformRequestSignedUrl
|
|
131
|
+
>(async (params) => ({
|
|
108
132
|
url:
|
|
109
133
|
params.operation === "upload"
|
|
110
134
|
? "https://storage.googleapis.com/bucket/signed-upload"
|
|
@@ -113,10 +137,9 @@ const platformRequestSignedUrlMock = spyOn(
|
|
|
113
137
|
expiresAt: new Date(Date.now() + 3600_000).toISOString(),
|
|
114
138
|
}));
|
|
115
139
|
|
|
116
|
-
const platformImportBundleFromGcsMock =
|
|
117
|
-
platformClient
|
|
118
|
-
|
|
119
|
-
).mockResolvedValue({
|
|
140
|
+
const platformImportBundleFromGcsMock = mock<
|
|
141
|
+
typeof platformClient.platformImportBundleFromGcs
|
|
142
|
+
>(async () => ({
|
|
120
143
|
statusCode: 200,
|
|
121
144
|
body: {
|
|
122
145
|
success: true,
|
|
@@ -128,12 +151,11 @@ const platformImportBundleFromGcsMock = spyOn(
|
|
|
128
151
|
backups_created: 1,
|
|
129
152
|
},
|
|
130
153
|
} as Record<string, unknown>,
|
|
131
|
-
});
|
|
154
|
+
}));
|
|
132
155
|
|
|
133
|
-
const platformImportPreflightFromGcsMock =
|
|
134
|
-
platformClient
|
|
135
|
-
|
|
136
|
-
).mockResolvedValue({
|
|
156
|
+
const platformImportPreflightFromGcsMock = mock<
|
|
157
|
+
typeof platformClient.platformImportPreflightFromGcs
|
|
158
|
+
>(async () => ({
|
|
137
159
|
statusCode: 200,
|
|
138
160
|
body: {
|
|
139
161
|
can_import: true,
|
|
@@ -144,71 +166,84 @@ const platformImportPreflightFromGcsMock = spyOn(
|
|
|
144
166
|
total_files: 3,
|
|
145
167
|
},
|
|
146
168
|
} as Record<string, unknown>,
|
|
147
|
-
});
|
|
169
|
+
}));
|
|
148
170
|
|
|
149
|
-
const checkExistingPlatformAssistantMock =
|
|
150
|
-
platformClient
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
} as unknown as Awaited<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
const fetchCurrentUserMock =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
171
|
+
const checkExistingPlatformAssistantMock = mock<
|
|
172
|
+
typeof platformClient.checkExistingPlatformAssistant
|
|
173
|
+
>(async () => null);
|
|
174
|
+
|
|
175
|
+
const ensureSelfHostedLocalRegistrationMock = mock<
|
|
176
|
+
typeof platformClient.ensureSelfHostedLocalRegistration
|
|
177
|
+
>(
|
|
178
|
+
async () =>
|
|
179
|
+
({
|
|
180
|
+
assistant: { id: "platform-assistant-1", name: "my-assistant" },
|
|
181
|
+
registration: {
|
|
182
|
+
client_installation_id: "device-id-123",
|
|
183
|
+
runtime_assistant_id: "target-local",
|
|
184
|
+
client_platform: "cli",
|
|
185
|
+
},
|
|
186
|
+
assistant_api_key: "api-key-123",
|
|
187
|
+
webhook_secret: "webhook-secret-123",
|
|
188
|
+
}) as unknown as Awaited<
|
|
189
|
+
ReturnType<typeof platformClient.ensureSelfHostedLocalRegistration>
|
|
190
|
+
>,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const injectCredentialsIntoAssistantMock = mock<
|
|
194
|
+
typeof platformClient.injectCredentialsIntoAssistant
|
|
195
|
+
>(async () => true);
|
|
196
|
+
|
|
197
|
+
const fetchCurrentUserMock = mock<typeof platformClient.fetchCurrentUser>(
|
|
198
|
+
async () =>
|
|
199
|
+
({
|
|
200
|
+
id: "user-1",
|
|
201
|
+
email: "test@example.com",
|
|
202
|
+
display: "Test",
|
|
203
|
+
}) as unknown as Awaited<
|
|
204
|
+
ReturnType<typeof platformClient.fetchCurrentUser>
|
|
205
|
+
>,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const fetchOrganizationIdMock = mock<typeof platformClient.fetchOrganizationId>(
|
|
209
|
+
async () => "org-1",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
mock.module("../lib/platform-client.js", () => ({
|
|
213
|
+
...realPlatformClient,
|
|
214
|
+
readPlatformToken: readPlatformTokenMock,
|
|
215
|
+
getPlatformUrl: getPlatformUrlMock,
|
|
216
|
+
hatchAssistant: hatchAssistantMock,
|
|
217
|
+
platformPollJobStatus: platformPollJobStatusMock,
|
|
218
|
+
platformRequestSignedUrl: platformRequestSignedUrlMock,
|
|
219
|
+
platformImportBundleFromGcs: platformImportBundleFromGcsMock,
|
|
220
|
+
platformImportPreflightFromGcs: platformImportPreflightFromGcsMock,
|
|
221
|
+
checkExistingPlatformAssistant: checkExistingPlatformAssistantMock,
|
|
222
|
+
ensureSelfHostedLocalRegistration: ensureSelfHostedLocalRegistrationMock,
|
|
223
|
+
injectCredentialsIntoAssistant: injectCredentialsIntoAssistantMock,
|
|
224
|
+
fetchCurrentUser: fetchCurrentUserMock,
|
|
225
|
+
fetchOrganizationId: fetchOrganizationIdMock,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const localRuntimeExportToGcsMock = mock<
|
|
229
|
+
typeof localRuntimeClient.localRuntimeExportToGcs
|
|
230
|
+
>(async () => ({ jobId: "local-export-job-1" }));
|
|
231
|
+
|
|
232
|
+
const localRuntimeImportFromGcsMock = mock<
|
|
233
|
+
typeof localRuntimeClient.localRuntimeImportFromGcs
|
|
234
|
+
>(async () => ({ jobId: "local-import-job-1" }));
|
|
198
235
|
|
|
199
236
|
// Default to a fixed version string. Tests that exercise the version-gate
|
|
200
237
|
// surface override this mock per-case to assert the value flows from the
|
|
201
238
|
// target runtime's `/v1/identity` (NOT from `cliPkg.version`) into the
|
|
202
239
|
// download signed-URL request.
|
|
203
|
-
const localRuntimeIdentityMock =
|
|
204
|
-
localRuntimeClient
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"localRuntimePollJobStatus",
|
|
211
|
-
).mockImplementation(async (_runtimeUrl, _token, jobId) => ({
|
|
240
|
+
const localRuntimeIdentityMock = mock<
|
|
241
|
+
typeof localRuntimeClient.localRuntimeIdentity
|
|
242
|
+
>(async () => ({ version: "0.6.5" }));
|
|
243
|
+
|
|
244
|
+
const localRuntimePollJobStatusMock = mock<
|
|
245
|
+
typeof localRuntimeClient.localRuntimePollJobStatus
|
|
246
|
+
>(async (_runtimeUrl, _token, jobId) => ({
|
|
212
247
|
jobId,
|
|
213
248
|
type: jobId.includes("import") ? "import" : "export",
|
|
214
249
|
status: "complete",
|
|
@@ -224,6 +259,14 @@ const localRuntimePollJobStatusMock = spyOn(
|
|
|
224
259
|
},
|
|
225
260
|
}));
|
|
226
261
|
|
|
262
|
+
mock.module("../lib/local-runtime-client.js", () => ({
|
|
263
|
+
...realLocalRuntimeClient,
|
|
264
|
+
localRuntimeExportToGcs: localRuntimeExportToGcsMock,
|
|
265
|
+
localRuntimeImportFromGcs: localRuntimeImportFromGcsMock,
|
|
266
|
+
localRuntimeIdentity: localRuntimeIdentityMock,
|
|
267
|
+
localRuntimePollJobStatus: localRuntimePollJobStatusMock,
|
|
268
|
+
}));
|
|
269
|
+
|
|
227
270
|
const hatchLocalMock = mock(async () => {});
|
|
228
271
|
|
|
229
272
|
mock.module("../lib/hatch-local.js", () => ({
|
|
@@ -285,29 +328,13 @@ import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
|
285
328
|
// ---------------------------------------------------------------------------
|
|
286
329
|
|
|
287
330
|
afterAll(() => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
getPlatformUrlMock.mockRestore();
|
|
296
|
-
hatchAssistantMock.mockRestore();
|
|
297
|
-
checkExistingPlatformAssistantMock.mockRestore();
|
|
298
|
-
platformPollJobStatusMock.mockRestore();
|
|
299
|
-
platformRequestSignedUrlMock.mockRestore();
|
|
300
|
-
platformImportBundleFromGcsMock.mockRestore();
|
|
301
|
-
platformImportPreflightFromGcsMock.mockRestore();
|
|
302
|
-
ensureSelfHostedLocalRegistrationMock.mockRestore();
|
|
303
|
-
injectCredentialsIntoAssistantMock.mockRestore();
|
|
304
|
-
fetchCurrentUserMock.mockRestore();
|
|
305
|
-
fetchOrganizationIdMock.mockRestore();
|
|
306
|
-
computeDeviceIdMock.mockRestore();
|
|
307
|
-
localRuntimeExportToGcsMock.mockRestore();
|
|
308
|
-
localRuntimeImportFromGcsMock.mockRestore();
|
|
309
|
-
localRuntimeIdentityMock.mockRestore();
|
|
310
|
-
localRuntimePollJobStatusMock.mockRestore();
|
|
331
|
+
// Restore the real module exports so the mocks do not leak into sibling
|
|
332
|
+
// test files (e.g. guardian-token.test.ts, platform-client tests) that
|
|
333
|
+
// run in the same `bun test` invocation.
|
|
334
|
+
mock.module("../lib/assistant-config.js", () => realAssistantConfig);
|
|
335
|
+
mock.module("../lib/guardian-token.js", () => realGuardianToken);
|
|
336
|
+
mock.module("../lib/platform-client.js", () => realPlatformClient);
|
|
337
|
+
mock.module("../lib/local-runtime-client.js", () => realLocalRuntimeClient);
|
|
311
338
|
rmSync(testDir, { recursive: true, force: true });
|
|
312
339
|
delete process.env.VELLUM_LOCKFILE_DIR;
|
|
313
340
|
});
|
package/src/commands/client.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
findAssistantByName,
|
|
5
7
|
getActiveAssistant,
|
|
6
8
|
resolveAssistant,
|
|
9
|
+
saveAssistantEntry,
|
|
7
10
|
} from "../lib/assistant-config";
|
|
8
11
|
import {
|
|
9
12
|
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
@@ -14,15 +17,17 @@ import { loadGuardianToken } from "../lib/guardian-token";
|
|
|
14
17
|
import { getLocalLanIPv4 } from "../lib/local";
|
|
15
18
|
import {
|
|
16
19
|
CLI_INTERFACE_ID,
|
|
20
|
+
WEB_INTERFACE_ID,
|
|
17
21
|
getClientRegistrationHeaders,
|
|
18
22
|
} from "../lib/client-identity";
|
|
19
23
|
import {
|
|
20
24
|
fetchOrganizationId,
|
|
25
|
+
fetchPlatformAssistants,
|
|
21
26
|
readPlatformToken,
|
|
22
27
|
} from "../lib/platform-client";
|
|
23
28
|
import { tuiLog } from "../lib/tui-log";
|
|
24
29
|
|
|
25
|
-
const SUPPORTED_INTERFACES = ["cli"] as const;
|
|
30
|
+
const SUPPORTED_INTERFACES = ["cli", "web"] as const;
|
|
26
31
|
type SupportedInterface = (typeof SUPPORTED_INTERFACES)[number];
|
|
27
32
|
|
|
28
33
|
const ANSI = {
|
|
@@ -36,6 +41,7 @@ const FALLBACK_RUNTIME_URL = `http://127.0.0.1:${GATEWAY_PORT}`;
|
|
|
36
41
|
interface ParsedArgs {
|
|
37
42
|
runtimeUrl: string;
|
|
38
43
|
assistantId: string;
|
|
44
|
+
assistantName?: string;
|
|
39
45
|
species: Species;
|
|
40
46
|
/** "vellum" for platform-hosted assistants, undefined for local. */
|
|
41
47
|
cloud?: string;
|
|
@@ -49,6 +55,15 @@ interface ParsedArgs {
|
|
|
49
55
|
zone?: string;
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
function readAssistantName(
|
|
59
|
+
entry: ReturnType<typeof findAssistantByName>,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
const rawName = entry?.name ?? entry?.assistantName;
|
|
62
|
+
return typeof rawName === "string" && rawName.trim()
|
|
63
|
+
? rawName.trim()
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
function parseArgs(): ParsedArgs {
|
|
53
68
|
const args = process.argv.slice(3);
|
|
54
69
|
|
|
@@ -109,6 +124,7 @@ function parseArgs(): ParsedArgs {
|
|
|
109
124
|
|
|
110
125
|
let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
111
126
|
let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
|
|
127
|
+
let assistantName = readAssistantName(entry);
|
|
112
128
|
const cloud = entry?.cloud;
|
|
113
129
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
114
130
|
|
|
@@ -131,14 +147,9 @@ function parseArgs(): ParsedArgs {
|
|
|
131
147
|
flagArgs[i + 1]
|
|
132
148
|
) {
|
|
133
149
|
assistantId = flagArgs[++i];
|
|
150
|
+
assistantName = undefined;
|
|
134
151
|
} else if ((flag === "--interface" || flag === "-i") && flagArgs[i + 1]) {
|
|
135
152
|
const value = flagArgs[++i];
|
|
136
|
-
if (value === "web") {
|
|
137
|
-
console.error(
|
|
138
|
-
`--interface web is not yet supported. Coming soon.`,
|
|
139
|
-
);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
153
|
if (!(SUPPORTED_INTERFACES as readonly string[]).includes(value)) {
|
|
143
154
|
console.error(
|
|
144
155
|
`Unknown interface '${value}'. Supported: ${SUPPORTED_INTERFACES.join(", ")}.`,
|
|
@@ -152,6 +163,7 @@ function parseArgs(): ParsedArgs {
|
|
|
152
163
|
return {
|
|
153
164
|
runtimeUrl: maybeSwapToLocalhost(runtimeUrl.replace(/\/+$/, "")),
|
|
154
165
|
assistantId,
|
|
166
|
+
assistantName,
|
|
155
167
|
species,
|
|
156
168
|
cloud,
|
|
157
169
|
platformToken,
|
|
@@ -213,7 +225,7 @@ ${ANSI.bold}ARGUMENTS:${ANSI.reset}
|
|
|
213
225
|
${ANSI.bold}OPTIONS:${ANSI.reset}
|
|
214
226
|
-u, --url <url> Runtime URL
|
|
215
227
|
-a, --assistant-id <id> Assistant ID
|
|
216
|
-
-i, --interface <id> Interface identifier (default
|
|
228
|
+
-i, --interface <id> Interface identifier: cli (default) or web
|
|
217
229
|
-h, --help Show this help message
|
|
218
230
|
|
|
219
231
|
${ANSI.bold}DEFAULTS:${ANSI.reset}
|
|
@@ -228,10 +240,104 @@ ${ANSI.bold}EXAMPLES:${ANSI.reset}
|
|
|
228
240
|
`);
|
|
229
241
|
}
|
|
230
242
|
|
|
243
|
+
async function maybeHydratePlatformAssistantName(
|
|
244
|
+
assistantId: string,
|
|
245
|
+
assistantName: string | undefined,
|
|
246
|
+
cloud: string | undefined,
|
|
247
|
+
platformToken: string | undefined,
|
|
248
|
+
): Promise<string | undefined> {
|
|
249
|
+
if (cloud !== "vellum" || assistantName || !platformToken) {
|
|
250
|
+
return assistantName;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const matchedAssistant = (
|
|
255
|
+
await fetchPlatformAssistants(platformToken)
|
|
256
|
+
).find((assistant) => assistant.id === assistantId);
|
|
257
|
+
const hydratedName = matchedAssistant?.name.trim();
|
|
258
|
+
if (!hydratedName) {
|
|
259
|
+
return assistantName;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const entry = findAssistantByName(assistantId);
|
|
263
|
+
if (entry && entry.name !== hydratedName) {
|
|
264
|
+
saveAssistantEntry({
|
|
265
|
+
...entry,
|
|
266
|
+
name: hydratedName,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return hydratedName;
|
|
271
|
+
} catch {
|
|
272
|
+
return assistantName;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Walk up from this file's location to find a sibling `clients/web` package.
|
|
278
|
+
*
|
|
279
|
+
* Returns the absolute path to its directory, or null when not found —
|
|
280
|
+
* e.g. when the CLI is installed via npm/bunx, where the `clients/web`
|
|
281
|
+
* source isn't shipped alongside `@vellumai/cli`. For now we treat the
|
|
282
|
+
* `--interface web` path as source-checkout-only.
|
|
283
|
+
*/
|
|
284
|
+
function findClientsWebDir(): string | null {
|
|
285
|
+
let dir = import.meta.dir;
|
|
286
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
287
|
+
const candidate = path.join(dir, "clients", "web", "package.json");
|
|
288
|
+
if (existsSync(candidate)) {
|
|
289
|
+
return path.dirname(candidate);
|
|
290
|
+
}
|
|
291
|
+
const parent = path.dirname(dir);
|
|
292
|
+
if (parent === dir) break;
|
|
293
|
+
dir = parent;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Spawn the `clients/web` package's `local` script and proxy its lifecycle.
|
|
300
|
+
*
|
|
301
|
+
* The web client is deliberately not declared as a dependency of `@vellumai/cli`:
|
|
302
|
+
* the CLI is published, the web package is not. Locating it on disk and
|
|
303
|
+
* shelling out keeps the two packages independent.
|
|
304
|
+
*/
|
|
305
|
+
async function runWebInterface(): Promise<void> {
|
|
306
|
+
const webDir = findClientsWebDir();
|
|
307
|
+
if (!webDir) {
|
|
308
|
+
console.error(
|
|
309
|
+
`${ANSI.bold}--interface web${ANSI.reset}: unable to locate ` +
|
|
310
|
+
`clients/web. This interface currently requires running ` +
|
|
311
|
+
`vellum from a source checkout of vellum-assistant.`,
|
|
312
|
+
);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const child = Bun.spawn({
|
|
317
|
+
cmd: ["bun", "run", "local"],
|
|
318
|
+
cwd: webDir,
|
|
319
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const forward = (signal: "SIGINT" | "SIGTERM"): void => {
|
|
323
|
+
try {
|
|
324
|
+
child.kill(signal);
|
|
325
|
+
} catch {
|
|
326
|
+
// Child already exited; nothing to forward.
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
process.on("SIGINT", () => forward("SIGINT"));
|
|
330
|
+
process.on("SIGTERM", () => forward("SIGTERM"));
|
|
331
|
+
|
|
332
|
+
const exitCode = await child.exited;
|
|
333
|
+
process.exit(typeof exitCode === "number" ? exitCode : 0);
|
|
334
|
+
}
|
|
335
|
+
|
|
231
336
|
export async function client(): Promise<void> {
|
|
232
337
|
const {
|
|
233
338
|
runtimeUrl,
|
|
234
339
|
assistantId,
|
|
340
|
+
assistantName: parsedAssistantName,
|
|
235
341
|
species,
|
|
236
342
|
cloud,
|
|
237
343
|
platformToken,
|
|
@@ -241,6 +347,11 @@ export async function client(): Promise<void> {
|
|
|
241
347
|
zone,
|
|
242
348
|
} = parseArgs();
|
|
243
349
|
|
|
350
|
+
if (interfaceId === WEB_INTERFACE_ID) {
|
|
351
|
+
await runWebInterface();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
244
355
|
tuiLog.init();
|
|
245
356
|
tuiLog.info("session start", {
|
|
246
357
|
runtimeUrl,
|
|
@@ -250,6 +361,13 @@ export async function client(): Promise<void> {
|
|
|
250
361
|
interfaceId,
|
|
251
362
|
});
|
|
252
363
|
|
|
364
|
+
const assistantName = await maybeHydratePlatformAssistantName(
|
|
365
|
+
assistantId,
|
|
366
|
+
parsedAssistantName,
|
|
367
|
+
cloud,
|
|
368
|
+
platformToken,
|
|
369
|
+
);
|
|
370
|
+
|
|
253
371
|
// Build pre-constructed request headers merged from auth + client registration.
|
|
254
372
|
// Spreading into every fetch site ensures consistency across REST and SSE endpoints.
|
|
255
373
|
let auth: Record<string, string> | undefined;
|
|
@@ -286,6 +404,6 @@ export async function client(): Promise<void> {
|
|
|
286
404
|
console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
|
|
287
405
|
process.exit(0);
|
|
288
406
|
},
|
|
289
|
-
{ auth, project, zone },
|
|
407
|
+
{ auth, project, zone, assistantName },
|
|
290
408
|
);
|
|
291
409
|
}
|
package/src/commands/events.ts
CHANGED
|
@@ -49,13 +49,14 @@ interface AssistantEvent {
|
|
|
49
49
|
content?: string;
|
|
50
50
|
message?: string;
|
|
51
51
|
chunk?: string;
|
|
52
|
+
tags?: unknown;
|
|
52
53
|
conversationId?: string;
|
|
53
54
|
[key: string]: unknown;
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/** Render an event as human-readable markdown to stdout. */
|
|
58
|
-
function renderMarkdown(event: AssistantEvent): void {
|
|
59
|
+
export function renderMarkdown(event: AssistantEvent): void {
|
|
59
60
|
const msg = event.message;
|
|
60
61
|
switch (msg.type) {
|
|
61
62
|
case "assistant_text_delta":
|
|
@@ -94,6 +95,17 @@ function renderMarkdown(event: AssistantEvent): void {
|
|
|
94
95
|
case "user_message_echo":
|
|
95
96
|
console.log(`\n**You:** ${msg.text}`);
|
|
96
97
|
break;
|
|
98
|
+
case "sync_changed": {
|
|
99
|
+
const tags = Array.isArray(msg.tags)
|
|
100
|
+
? msg.tags.filter((tag): tag is string => typeof tag === "string")
|
|
101
|
+
: [];
|
|
102
|
+
const renderedTags =
|
|
103
|
+
tags.length > 0
|
|
104
|
+
? tags.map((tag) => `\`${tag}\``).join(", ")
|
|
105
|
+
: "(no tags)";
|
|
106
|
+
console.log(`\n> **Sync changed:** ${renderedTags}`);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
97
109
|
default:
|
|
98
110
|
// Silently skip events that don't have a markdown representation
|
|
99
111
|
// (e.g. heartbeat comments, activity states, etc.)
|