@vellumai/assistant 0.5.12 → 0.5.13

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.
Files changed (31) hide show
  1. package/Dockerfile +41 -9
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  3. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  4. package/openapi.yaml +1 -1
  5. package/package.json +1 -1
  6. package/src/__tests__/first-greeting.test.ts +7 -0
  7. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  8. package/src/__tests__/platform.test.ts +3 -168
  9. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  10. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  11. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  12. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  13. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  14. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  15. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  16. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  17. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  18. package/src/config/feature-flag-registry.json +1 -1
  19. package/src/credential-execution/client.ts +14 -2
  20. package/src/daemon/first-greeting.ts +6 -1
  21. package/src/daemon/lifecycle.ts +13 -8
  22. package/src/index.ts +0 -12
  23. package/src/memory/conversation-queries.ts +6 -6
  24. package/src/memory/journal-memory.ts +8 -2
  25. package/src/prompts/journal-context.ts +4 -1
  26. package/src/prompts/system-prompt.ts +11 -0
  27. package/src/runtime/http-server.ts +7 -15
  28. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  29. package/src/runtime/routes/secret-routes.ts +9 -2
  30. package/src/tools/browser/browser-manager.ts +2 -2
  31. package/src/util/platform.ts +1 -91
@@ -0,0 +1,224 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = mkdtempSync(join(tmpdir(), "platform-connect-test-"));
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Mock state
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let mockGetSecureKeyViaDaemon: (
13
+ account: string,
14
+ ) => Promise<string | undefined> = async () => undefined;
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Mocks
18
+ // ---------------------------------------------------------------------------
19
+
20
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
21
+ getSecureKeyViaDaemon: (account: string) =>
22
+ mockGetSecureKeyViaDaemon(account),
23
+ deleteSecureKeyViaDaemon: async () => "not-found" as const,
24
+ setSecureKeyViaDaemon: async () => false,
25
+ getProviderKeyViaDaemon: async () => undefined,
26
+ getSecureKeyResultViaDaemon: async () => ({
27
+ value: undefined,
28
+ unreachable: false,
29
+ }),
30
+ }));
31
+
32
+ mock.module("../../../../inbound/platform-callback-registration.js", () => ({
33
+ resolvePlatformCallbackRegistrationContext: async () => ({
34
+ containerized: false,
35
+ platformBaseUrl: "",
36
+ assistantId: "",
37
+ hasInternalApiKey: false,
38
+ hasAssistantApiKey: false,
39
+ authHeader: null,
40
+ enabled: false,
41
+ }),
42
+ registerCallbackRoute: async () => "",
43
+ shouldUsePlatformCallbacks: () => false,
44
+ resolveCallbackUrl: async () => "",
45
+ }));
46
+
47
+ mock.module("../../../../util/logger.js", () => ({
48
+ getLogger: () => ({
49
+ info: () => {},
50
+ warn: () => {},
51
+ error: () => {},
52
+ debug: () => {},
53
+ }),
54
+ getCliLogger: () => ({
55
+ info: () => {},
56
+ warn: () => {},
57
+ error: () => {},
58
+ debug: () => {},
59
+ }),
60
+ initLogger: () => {},
61
+ truncateForLog: (value: string, maxLen = 500) =>
62
+ value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
63
+ pruneOldLogFiles: () => 0,
64
+ }));
65
+
66
+ mock.module("../../../../util/platform.js", () => ({
67
+ getRootDir: () => testDir,
68
+ getDataDir: () => join(testDir, "data"),
69
+ getWorkspaceSkillsDir: () => join(testDir, "skills"),
70
+ getWorkspaceDir: () => join(testDir, "workspace"),
71
+ getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
72
+ getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
73
+ getHooksDir: () => join(testDir, "hooks"),
74
+ getSignalsDir: () => join(testDir, "signals"),
75
+ getConversationsDir: () => join(testDir, "conversations"),
76
+ getEmbeddingModelsDir: () => join(testDir, "models"),
77
+ getSandboxRootDir: () => join(testDir, "sandbox"),
78
+ getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
79
+ getInterfacesDir: () => join(testDir, "interfaces"),
80
+ getSoundsDir: () => join(testDir, "sounds"),
81
+ getHistoryPath: () => join(testDir, "history"),
82
+ isMacOS: () => process.platform === "darwin",
83
+ isLinux: () => process.platform === "linux",
84
+ isWindows: () => process.platform === "win32",
85
+ getPlatformName: () => "linux",
86
+ getClipboardCommand: () => null,
87
+ resolveInstanceDataDir: () => undefined,
88
+ normalizeAssistantId: (id: string) => id,
89
+ getTCPPort: () => 0,
90
+ isTCPEnabled: () => false,
91
+ getTCPHost: () => "127.0.0.1",
92
+ isIOSPairingEnabled: () => false,
93
+ getPlatformTokenPath: () => join(testDir, "token"),
94
+ readPlatformToken: () => null,
95
+ getPidPath: () => join(testDir, "test.pid"),
96
+ getDbPath: () => join(testDir, "test.db"),
97
+ getLogPath: () => join(testDir, "test.log"),
98
+ getWorkspaceDirDisplay: () => testDir,
99
+ getWorkspacePromptPath: (file: string) => join(testDir, file),
100
+ ensureDataDir: () => {},
101
+ }));
102
+
103
+ mock.module("../../../../config/loader.js", () => ({
104
+ API_KEY_PROVIDERS: [] as const,
105
+ getConfig: () => ({
106
+ permissions: { mode: "workspace" },
107
+ skills: { load: { extraDirs: [] } },
108
+ sandbox: { enabled: true },
109
+ }),
110
+ loadConfig: () => ({}),
111
+ invalidateConfigCache: () => {},
112
+ saveConfig: () => {},
113
+ loadRawConfig: () => ({}),
114
+ saveRawConfig: () => {},
115
+ getNestedValue: () => undefined,
116
+ setNestedValue: () => {},
117
+ applyNestedDefaults: (config: unknown) => config,
118
+ deepMergeMissing: () => false,
119
+ deepMergeOverwrite: () => {},
120
+ mergeDefaultWorkspaceConfig: () => {},
121
+ }));
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Import module under test (after mocks are registered)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const { buildCliProgram } = await import("../../../program.js");
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Test helper
131
+ // ---------------------------------------------------------------------------
132
+
133
+ async function runCommand(
134
+ args: string[],
135
+ ): Promise<{ stdout: string; exitCode: number }> {
136
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
137
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
138
+ const stdoutChunks: string[] = [];
139
+
140
+ process.stdout.write = ((chunk: unknown) => {
141
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
142
+ return true;
143
+ }) as typeof process.stdout.write;
144
+
145
+ process.stderr.write = (() => true) as typeof process.stderr.write;
146
+
147
+ process.exitCode = 0;
148
+
149
+ try {
150
+ const program = buildCliProgram();
151
+ program.exitOverride();
152
+ program.configureOutput({
153
+ writeErr: () => {},
154
+ writeOut: (str: string) => stdoutChunks.push(str),
155
+ });
156
+ await program.parseAsync(["node", "assistant", ...args]);
157
+ } catch {
158
+ if (process.exitCode === 0) process.exitCode = 1;
159
+ } finally {
160
+ process.stdout.write = originalStdoutWrite;
161
+ process.stderr.write = originalStderrWrite;
162
+ }
163
+
164
+ const exitCode = process.exitCode ?? 0;
165
+ process.exitCode = 0;
166
+
167
+ return { exitCode, stdout: stdoutChunks.join("") };
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Tests
172
+ // ---------------------------------------------------------------------------
173
+
174
+ describe("assistant platform connect", () => {
175
+ beforeEach(() => {
176
+ mockGetSecureKeyViaDaemon = async () => undefined;
177
+ process.exitCode = 0;
178
+ });
179
+
180
+ test.todo(
181
+ "already connected returns success with existing base URL",
182
+ async () => {
183
+ /**
184
+ * When the assistant already has stored platform credentials (base
185
+ * URL and API key), the connect command should short-circuit and
186
+ * report that it is already connected, returning the existing base
187
+ * URL.
188
+ *
189
+ * NOTE: The connect command is currently stubbed — the full
190
+ * credential-collection flow via a secure UI component is not yet
191
+ * implemented. This test validates the already-connected early-exit
192
+ * path, which IS implemented. It is skipped because the stub path
193
+ * still sets a non-zero exit code before reaching this branch
194
+ * under certain conditions. Unskip once the full connect flow
195
+ * lands.
196
+ */
197
+
198
+ // GIVEN stored platform credentials already exist
199
+ mockGetSecureKeyViaDaemon = async (account: string) => {
200
+ if (account === "credential/vellum/platform_base_url")
201
+ return "https://platform.vellum.ai";
202
+ if (account === "credential/vellum/assistant_api_key")
203
+ return "sk-existing-key";
204
+ return undefined;
205
+ };
206
+
207
+ // WHEN the connect command is run with --json
208
+ const { exitCode, stdout } = await runCommand([
209
+ "platform",
210
+ "connect",
211
+ "--json",
212
+ ]);
213
+
214
+ // THEN the command succeeds
215
+ expect(exitCode).toBe(0);
216
+
217
+ // AND the output indicates already connected with the base URL
218
+ const parsed = JSON.parse(stdout);
219
+ expect(parsed.ok).toBe(true);
220
+ expect(parsed.alreadyConnected).toBe(true);
221
+ expect(parsed.baseUrl).toBe("https://platform.vellum.ai");
222
+ },
223
+ );
224
+ });
@@ -0,0 +1,237 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = mkdtempSync(join(tmpdir(), "platform-disconnect-test-"));
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Mock state
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let mockGetSecureKeyViaDaemon: (
13
+ account: string,
14
+ ) => Promise<string | undefined> = async () => undefined;
15
+
16
+ let mockDeleteSecureKeyViaDaemonCalls: Array<{
17
+ type: string;
18
+ name: string;
19
+ }> = [];
20
+
21
+ let mockDeleteSecureKeyViaDaemonResult: "deleted" | "not-found" | "error" =
22
+ "deleted";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Mocks
26
+ // ---------------------------------------------------------------------------
27
+
28
+ mock.module("../../../../inbound/platform-callback-registration.js", () => ({
29
+ resolvePlatformCallbackRegistrationContext: async () => ({
30
+ containerized: false,
31
+ platformBaseUrl: "",
32
+ assistantId: "",
33
+ hasInternalApiKey: false,
34
+ hasAssistantApiKey: false,
35
+ authHeader: null,
36
+ enabled: false,
37
+ }),
38
+ registerCallbackRoute: async () => "",
39
+ shouldUsePlatformCallbacks: () => false,
40
+ resolveCallbackUrl: async () => "",
41
+ }));
42
+
43
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
44
+ getSecureKeyViaDaemon: (account: string) =>
45
+ mockGetSecureKeyViaDaemon(account),
46
+ deleteSecureKeyViaDaemon: async (type: string, name: string) => {
47
+ mockDeleteSecureKeyViaDaemonCalls.push({ type, name });
48
+ return mockDeleteSecureKeyViaDaemonResult;
49
+ },
50
+ setSecureKeyViaDaemon: async () => false,
51
+ getProviderKeyViaDaemon: async () => undefined,
52
+ getSecureKeyResultViaDaemon: async () => ({
53
+ value: undefined,
54
+ unreachable: false,
55
+ }),
56
+ }));
57
+
58
+ mock.module("../../../../util/logger.js", () => ({
59
+ getLogger: () => ({
60
+ info: () => {},
61
+ warn: () => {},
62
+ error: () => {},
63
+ debug: () => {},
64
+ }),
65
+ getCliLogger: () => ({
66
+ info: () => {},
67
+ warn: () => {},
68
+ error: () => {},
69
+ debug: () => {},
70
+ }),
71
+ initLogger: () => {},
72
+ truncateForLog: (value: string, maxLen = 500) =>
73
+ value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
74
+ pruneOldLogFiles: () => 0,
75
+ }));
76
+
77
+ mock.module("../../../../util/platform.js", () => ({
78
+ getRootDir: () => testDir,
79
+ getDataDir: () => join(testDir, "data"),
80
+ getWorkspaceSkillsDir: () => join(testDir, "skills"),
81
+ getWorkspaceDir: () => join(testDir, "workspace"),
82
+ getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
83
+ getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
84
+ getHooksDir: () => join(testDir, "hooks"),
85
+ getSignalsDir: () => join(testDir, "signals"),
86
+ getConversationsDir: () => join(testDir, "conversations"),
87
+ getEmbeddingModelsDir: () => join(testDir, "models"),
88
+ getSandboxRootDir: () => join(testDir, "sandbox"),
89
+ getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
90
+ getInterfacesDir: () => join(testDir, "interfaces"),
91
+ getSoundsDir: () => join(testDir, "sounds"),
92
+ getHistoryPath: () => join(testDir, "history"),
93
+ isMacOS: () => process.platform === "darwin",
94
+ isLinux: () => process.platform === "linux",
95
+ isWindows: () => process.platform === "win32",
96
+ getPlatformName: () => "linux",
97
+ getClipboardCommand: () => null,
98
+ resolveInstanceDataDir: () => undefined,
99
+ normalizeAssistantId: (id: string) => id,
100
+ getTCPPort: () => 0,
101
+ isTCPEnabled: () => false,
102
+ getTCPHost: () => "127.0.0.1",
103
+ isIOSPairingEnabled: () => false,
104
+ getPlatformTokenPath: () => join(testDir, "token"),
105
+ readPlatformToken: () => null,
106
+ getPidPath: () => join(testDir, "test.pid"),
107
+ getDbPath: () => join(testDir, "test.db"),
108
+ getLogPath: () => join(testDir, "test.log"),
109
+ getWorkspaceDirDisplay: () => testDir,
110
+ getWorkspacePromptPath: (file: string) => join(testDir, file),
111
+ ensureDataDir: () => {},
112
+ }));
113
+
114
+ mock.module("../../../../config/loader.js", () => ({
115
+ API_KEY_PROVIDERS: [] as const,
116
+ getConfig: () => ({
117
+ permissions: { mode: "workspace" },
118
+ skills: { load: { extraDirs: [] } },
119
+ sandbox: { enabled: true },
120
+ }),
121
+ loadConfig: () => ({}),
122
+ invalidateConfigCache: () => {},
123
+ saveConfig: () => {},
124
+ loadRawConfig: () => ({}),
125
+ saveRawConfig: () => {},
126
+ getNestedValue: () => undefined,
127
+ setNestedValue: () => {},
128
+ applyNestedDefaults: (config: unknown) => config,
129
+ deepMergeMissing: () => false,
130
+ deepMergeOverwrite: () => {},
131
+ mergeDefaultWorkspaceConfig: () => {},
132
+ }));
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Import module under test (after mocks are registered)
136
+ // ---------------------------------------------------------------------------
137
+
138
+ const { buildCliProgram } = await import("../../../program.js");
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Test helper
142
+ // ---------------------------------------------------------------------------
143
+
144
+ async function runCommand(
145
+ args: string[],
146
+ ): Promise<{ stdout: string; exitCode: number }> {
147
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
148
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
149
+ const stdoutChunks: string[] = [];
150
+
151
+ process.stdout.write = ((chunk: unknown) => {
152
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
153
+ return true;
154
+ }) as typeof process.stdout.write;
155
+
156
+ process.stderr.write = (() => true) as typeof process.stderr.write;
157
+
158
+ process.exitCode = 0;
159
+
160
+ try {
161
+ const program = buildCliProgram();
162
+ program.exitOverride();
163
+ program.configureOutput({
164
+ writeErr: () => {},
165
+ writeOut: (str: string) => stdoutChunks.push(str),
166
+ });
167
+ await program.parseAsync(["node", "assistant", ...args]);
168
+ } catch {
169
+ if (process.exitCode === 0) process.exitCode = 1;
170
+ } finally {
171
+ process.stdout.write = originalStdoutWrite;
172
+ process.stderr.write = originalStderrWrite;
173
+ }
174
+
175
+ const exitCode = process.exitCode ?? 0;
176
+ process.exitCode = 0;
177
+
178
+ return { exitCode, stdout: stdoutChunks.join("") };
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Tests
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe("assistant platform disconnect", () => {
186
+ beforeEach(() => {
187
+ mockGetSecureKeyViaDaemon = async () => undefined;
188
+ mockDeleteSecureKeyViaDaemonCalls = [];
189
+ mockDeleteSecureKeyViaDaemonResult = "deleted";
190
+ process.exitCode = 0;
191
+ });
192
+
193
+ test("successfully removes all stored platform credentials", async () => {
194
+ /**
195
+ * When a connected platform has stored credentials, the disconnect
196
+ * command should delete all credential keys and report success with
197
+ * the previous base URL.
198
+ */
199
+
200
+ // GIVEN stored platform credentials exist
201
+ mockGetSecureKeyViaDaemon = async (account: string) => {
202
+ if (account === "credential/vellum/platform_base_url")
203
+ return "https://platform.vellum.ai";
204
+ if (account === "credential/vellum/assistant_api_key")
205
+ return "sk-test-key";
206
+ return undefined;
207
+ };
208
+
209
+ // AND credential deletion succeeds
210
+ mockDeleteSecureKeyViaDaemonResult = "deleted";
211
+
212
+ // WHEN the disconnect command is run with --json
213
+ const { exitCode, stdout } = await runCommand([
214
+ "platform",
215
+ "disconnect",
216
+ "--json",
217
+ ]);
218
+
219
+ // THEN the command succeeds
220
+ expect(exitCode).toBe(0);
221
+
222
+ // AND the output confirms disconnection with the previous base URL
223
+ const parsed = JSON.parse(stdout);
224
+ expect(parsed.ok).toBe(true);
225
+ expect(parsed.disconnected).toBe(true);
226
+ expect(parsed.previousBaseUrl).toBe("https://platform.vellum.ai");
227
+
228
+ // AND all five credential keys were deleted
229
+ expect(mockDeleteSecureKeyViaDaemonCalls).toHaveLength(5);
230
+ const deletedNames = mockDeleteSecureKeyViaDaemonCalls.map((c) => c.name);
231
+ expect(deletedNames).toContain("vellum:platform_base_url");
232
+ expect(deletedNames).toContain("vellum:assistant_api_key");
233
+ expect(deletedNames).toContain("vellum:platform_assistant_id");
234
+ expect(deletedNames).toContain("vellum:platform_organization_id");
235
+ expect(deletedNames).toContain("vellum:platform_user_id");
236
+ });
237
+ });