@vellumai/assistant 0.5.11 → 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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -117,12 +117,12 @@ describe("Slack messaging token resolution", () => {
117
117
  expect(await slackProvider.isConnected!()).toBe(true);
118
118
  });
119
119
 
120
- test("returns true when only integration:slack has active OAuth connection (backwards compat)", async () => {
120
+ test("returns true when only slack has active OAuth connection (backwards compat)", async () => {
121
121
  // No bot token
122
122
  getSecureKeyAsyncMock.mockImplementation(async () => null);
123
123
  // But OAuth provider is connected
124
124
  isProviderConnectedMock.mockImplementation(async (service: string) =>
125
- service === "integration:slack" ? true : false,
125
+ service === "slack" ? true : false,
126
126
  );
127
127
 
128
128
  expect(await slackProvider.isConnected!()).toBe(true);
@@ -139,7 +139,7 @@ describe("Slack messaging token resolution", () => {
139
139
  // ── slackProvider.resolveConnection() ───────────────────────────────────
140
140
 
141
141
  describe("slackProvider.resolveConnection()", () => {
142
- test("returns bot token string when Socket Mode credentials exist", async () => {
142
+ test("returns undefined when Socket Mode credentials exist (token cached internally)", async () => {
143
143
  getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
144
144
  key === "credential/slack_channel/bot_token"
145
145
  ? "xoxb-socket-token"
@@ -147,20 +147,20 @@ describe("Slack messaging token resolution", () => {
147
147
  );
148
148
 
149
149
  const result = await slackProvider.resolveConnection!();
150
- expect(result).toBe("xoxb-socket-token");
150
+ expect(result).toBeUndefined();
151
151
  });
152
152
 
153
- test("returns bot token string even without a slack_channel connection row (token-only resilience)", async () => {
153
+ test("returns undefined even without a slack_channel connection row (token-only resilience)", async () => {
154
154
  getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
155
155
  key === "credential/slack_channel/bot_token" ? "xoxb-token-only" : null,
156
156
  );
157
- // No connection row — resolveConnection should still return the token
157
+ // No connection row — resolveConnection should still return undefined (token cached internally)
158
158
 
159
159
  const result = await slackProvider.resolveConnection!();
160
- expect(result).toBe("xoxb-token-only");
160
+ expect(result).toBeUndefined();
161
161
  });
162
162
 
163
- test("returns OAuthConnection when only OAuth integration:slack credentials exist (backwards compat)", async () => {
163
+ test("returns OAuthConnection when only OAuth slack credentials exist (backwards compat)", async () => {
164
164
  getSecureKeyAsyncMock.mockImplementation(async () => null);
165
165
  const oauthConn = {
166
166
  accessToken: "xoxp-oauth-token",
@@ -169,16 +169,15 @@ describe("Slack messaging token resolution", () => {
169
169
 
170
170
  const result = await slackProvider.resolveConnection!();
171
171
  expect(result).toBe(oauthConn);
172
- expect(resolveOAuthConnectionMock).toHaveBeenCalledWith(
173
- "integration:slack",
174
- { account: undefined },
175
- );
172
+ expect(resolveOAuthConnectionMock).toHaveBeenCalledWith("slack", {
173
+ account: undefined,
174
+ });
176
175
  });
177
176
 
178
177
  test("throws when no credentials exist at all (no Socket Mode, no OAuth)", async () => {
179
178
  getSecureKeyAsyncMock.mockImplementation(async () => null);
180
179
  resolveOAuthConnectionMock.mockImplementation(async () => {
181
- throw new Error("No OAuth connection found for integration:slack");
180
+ throw new Error("No OAuth connection found for slack");
182
181
  });
183
182
 
184
183
  await expect(slackProvider.resolveConnection!()).rejects.toThrow(
@@ -190,13 +189,13 @@ describe("Slack messaging token resolution", () => {
190
189
  // ── getProviderConnection() integration ─────────────────────────────────
191
190
 
192
191
  describe("getProviderConnection()", () => {
193
- test("returns bot token string for Slack when Socket Mode credentials exist", async () => {
192
+ test("returns undefined for Slack when Socket Mode credentials exist (token cached internally)", async () => {
194
193
  getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
195
194
  key === "credential/slack_channel/bot_token" ? "xoxb-conn-token" : null,
196
195
  );
197
196
 
198
197
  const result = await getProviderConnection(slackProvider);
199
- expect(result).toBe("xoxb-conn-token");
198
+ expect(result).toBeUndefined();
200
199
  });
201
200
 
202
201
  test("returns OAuthConnection for Slack when only OAuth credentials exist (backwards compat)", async () => {
@@ -221,9 +220,9 @@ describe("Slack messaging token resolution", () => {
221
220
  );
222
221
  });
223
222
 
224
- test('Telegram still returns "" (no resolveConnection, uses isConnected path — regression check)', async () => {
223
+ test("Telegram returns undefined (no resolveConnection, uses isConnected path — regression check)", async () => {
225
224
  // Telegram has isConnected but no resolveConnection.
226
- // When isConnected returns true, getProviderConnection returns ""
225
+ // When isConnected returns true, getProviderConnection returns undefined
227
226
  getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
228
227
  if (key === "credential/telegram/bot_token") return "bot-token";
229
228
  if (key === "credential/telegram/webhook_secret") return "secret";
@@ -234,7 +233,7 @@ describe("Slack messaging token resolution", () => {
234
233
  );
235
234
 
236
235
  const result = await getProviderConnection(telegramBotMessagingProvider);
237
- expect(result).toBe("");
236
+ expect(result).toBeUndefined();
238
237
  });
239
238
 
240
239
  test("Gmail still calls resolveOAuthConnection (no resolveConnection, no isConnected — regression check)", async () => {
@@ -247,10 +246,9 @@ describe("Slack messaging token resolution", () => {
247
246
 
248
247
  const result = await getProviderConnection(gmailMessagingProvider);
249
248
  expect(result).toBe(oauthConn);
250
- expect(resolveOAuthConnectionMock).toHaveBeenCalledWith(
251
- "integration:google",
252
- { account: undefined },
253
- );
249
+ expect(resolveOAuthConnectionMock).toHaveBeenCalledWith("google", {
250
+ account: undefined,
251
+ });
254
252
  });
255
253
  });
256
254
 
@@ -264,7 +262,7 @@ describe("Slack messaging token resolution", () => {
264
262
  );
265
263
  // Gmail connected via OAuth
266
264
  isProviderConnectedMock.mockImplementation(async (service: string) =>
267
- service === "integration:google" ? true : false,
265
+ service === "google" ? true : false,
268
266
  );
269
267
 
270
268
  await expect(resolveProvider()).rejects.toThrow(
@@ -285,7 +283,7 @@ describe("Slack messaging token resolution", () => {
285
283
  test("auto-selects Gmail when it is the only connected provider (no Slack credentials)", async () => {
286
284
  getSecureKeyAsyncMock.mockImplementation(async () => null);
287
285
  isProviderConnectedMock.mockImplementation(async (service: string) =>
288
- service === "integration:google" ? true : false,
286
+ service === "google" ? true : false,
289
287
  );
290
288
 
291
289
  const provider = await resolveProvider();
@@ -111,7 +111,7 @@ describe("handleListSlackChannels", () => {
111
111
  });
112
112
 
113
113
  test("returns channels sorted by type then name", async () => {
114
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
114
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
115
115
  secureKeyValues.set(
116
116
  "oauth_connection/conn-slack-1/access_token",
117
117
  "xoxb-test",
@@ -192,7 +192,7 @@ describe("handleShareToSlackChannel", () => {
192
192
  });
193
193
 
194
194
  test("returns 400 for malformed JSON", async () => {
195
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
195
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
196
196
  secureKeyValues.set(
197
197
  "oauth_connection/conn-slack-1/access_token",
198
198
  "xoxb-test",
@@ -207,7 +207,7 @@ describe("handleShareToSlackChannel", () => {
207
207
  });
208
208
 
209
209
  test("returns 400 when missing required fields", async () => {
210
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
210
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
211
211
  secureKeyValues.set(
212
212
  "oauth_connection/conn-slack-1/access_token",
213
213
  "xoxb-test",
@@ -220,7 +220,7 @@ describe("handleShareToSlackChannel", () => {
220
220
  });
221
221
 
222
222
  test("returns 404 when app not found", async () => {
223
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
223
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
224
224
  secureKeyValues.set(
225
225
  "oauth_connection/conn-slack-1/access_token",
226
226
  "xoxb-test",
@@ -232,7 +232,7 @@ describe("handleShareToSlackChannel", () => {
232
232
  });
233
233
 
234
234
  test("posts message and returns success", async () => {
235
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
235
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
236
236
  secureKeyValues.set(
237
237
  "oauth_connection/conn-slack-1/access_token",
238
238
  "xoxb-test",
@@ -24,6 +24,7 @@ mock.module("../util/platform.js", () => ({
24
24
  getWorkspaceConfigPath: () => join(TEST_DIR, "config.json"),
25
25
  getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
26
26
  getWorkspaceHooksDir: () => join(TEST_DIR, "hooks"),
27
+ getConversationsDir: () => join(TEST_DIR, "conversations"),
27
28
  getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
28
29
  ensureDataDir: () => {},
29
30
  getPidPath: () => join(TEST_DIR, "vellum.pid"),
@@ -560,4 +561,42 @@ describe("ensurePromptFiles", () => {
560
561
  const bootstrapPath = join(TEST_DIR, "BOOTSTRAP.md");
561
562
  expect(existsSync(bootstrapPath)).toBe(false);
562
563
  });
564
+
565
+ test("auto-deletes stale BOOTSTRAP.md when prior conversations exist", () => {
566
+ // Simulate a non-first-run workspace: core files + BOOTSTRAP.md still present
567
+ writeFileSync(join(TEST_DIR, "IDENTITY.md"), "My identity");
568
+ writeFileSync(join(TEST_DIR, "SOUL.md"), "My soul");
569
+ writeFileSync(join(TEST_DIR, "USER.md"), "My user");
570
+ writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), "# Stale bootstrap");
571
+
572
+ // Create a conversations directory with at least one entry
573
+ const convDir = join(TEST_DIR, "conversations");
574
+ mkdirSync(convDir, { recursive: true });
575
+ writeFileSync(join(convDir, "conv-001.json"), "{}");
576
+
577
+ ensurePromptFiles();
578
+
579
+ expect(existsSync(join(TEST_DIR, "BOOTSTRAP.md"))).toBe(false);
580
+ });
581
+
582
+ test("keeps BOOTSTRAP.md on first run even if conversations dir exists", () => {
583
+ // First run: no core files exist, BOOTSTRAP.md should be created and kept
584
+ const convDir = join(TEST_DIR, "conversations");
585
+ mkdirSync(convDir, { recursive: true });
586
+ writeFileSync(join(convDir, "conv-001.json"), "{}");
587
+
588
+ ensurePromptFiles();
589
+
590
+ expect(existsSync(join(TEST_DIR, "BOOTSTRAP.md"))).toBe(true);
591
+ });
592
+
593
+ test("keeps BOOTSTRAP.md when no conversations exist yet", () => {
594
+ // Non-first-run but no conversations — user hasn't chatted yet
595
+ writeFileSync(join(TEST_DIR, "IDENTITY.md"), "My identity");
596
+ writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), "# Bootstrap");
597
+
598
+ ensurePromptFiles();
599
+
600
+ expect(existsSync(join(TEST_DIR, "BOOTSTRAP.md"))).toBe(true);
601
+ });
563
602
  });
@@ -205,7 +205,7 @@ function makeSystemPrompt(size: "small" | "production" = "small"): string {
205
205
  "### OAuth Setup",
206
206
  "Most integrations use OAuth for authentication.",
207
207
  "Guide the user through the OAuth flow when setting up a new integration:",
208
- "1. Navigate to Settings > Integrations",
208
+ "1. Navigate to Settings > Models & Services",
209
209
  "2. Click 'Connect' for the desired service",
210
210
  "3. Authorize in the browser popup",
211
211
  "4. Confirm the connection is active",
@@ -284,20 +284,21 @@ describe("011-backfill-installation-id migration", () => {
284
284
  expect(parsed.assistants[0].installationId).toBe("sqlite-id");
285
285
  });
286
286
 
287
- test("respects BASE_DATA_DIR environment variable", () => {
287
+ test("ignores BASE_DATA_DIR and always reads lockfile from homedir", () => {
288
288
  process.env.BASE_DATA_DIR = "/custom-base";
289
289
  getMemoryCheckpointFn.mockReturnValue("sqlite-id");
290
290
 
291
- const customLockPath = "/custom-base/.vellum.lock.json";
291
+ // Lockfile under BASE_DATA_DIR should be ignored — the migration
292
+ // always reads from homedir() (per-user, not per-instance).
292
293
  setupFs({
293
- [customLockPath]: makeLockfile([{ assistantId: "my-assistant" }]),
294
+ [LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
294
295
  });
295
296
 
296
297
  backfillInstallationIdMigration.run(WORKSPACE_DIR);
297
298
 
298
299
  expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
299
300
  const [path] = writeFileSyncFn.mock.calls[0] as [string, string];
300
- expect(path).toBe(customLockPath);
301
+ expect(path).toBe(LOCK_PATH);
301
302
  });
302
303
 
303
304
  test("preserves other assistants in lockfile when writing", () => {
package/src/cli/AGENTS.md CHANGED
@@ -59,6 +59,46 @@ does and how to use it.
59
59
  4. **Use Commander's `.addHelpText("after", ...)`** for extended help. Don't
60
60
  cram everything into `.description()`.
61
61
 
62
+ ### No Redundant Command Lists in `addHelpText`
63
+
64
+ Commander already renders a `Commands:` section from registered subcommands.
65
+ Never duplicate that list in `.addHelpText("after", ...)`. The `addHelpText`
66
+ block is for **supplementary context only** — domain notes, key concepts, and
67
+ examples. Repeating command names and descriptions wastes vertical space and
68
+ creates a maintenance burden (two places to update when a subcommand changes).
69
+
70
+ **Bad:**
71
+
72
+ ```ts
73
+ oauth.addHelpText(
74
+ "after",
75
+ `
76
+ The oauth command group manages the full OAuth lifecycle:
77
+
78
+ connect Initiate an OAuth flow for a provider
79
+ disconnect Disconnect an OAuth provider
80
+ ...
81
+ `,
82
+ );
83
+ ```
84
+
85
+ **Good:**
86
+
87
+ ```ts
88
+ oauth.addHelpText(
89
+ "after",
90
+ `
91
+ Providers are seeded on startup for built-in integrations. Apps and connections
92
+ are created during the OAuth authorization flow or can be managed manually via
93
+ their respective subcommands.
94
+
95
+ Examples:
96
+ $ assistant oauth connect google --open-browser
97
+ $ assistant oauth status google
98
+ `,
99
+ );
100
+ ```
101
+
62
102
  ### ID and Key Arguments
63
103
 
64
104
  Options that accept IDs, keys, or opaque identifiers must include a short note
@@ -79,12 +119,12 @@ users and AI agents have no way to know what to pass.
79
119
 
80
120
  Common discovery patterns:
81
121
 
82
- | Argument type | Discovery command |
83
- | ------------- | ------------------------------------------------------------------------- |
84
- | Provider key | `assistant oauth providers list` |
85
- | Connection ID | `assistant oauth connections list` or `assistant oauth status <provider>` |
86
- | OAuth app ID | `assistant oauth apps list` |
87
- | Contact ID | `assistant contacts list` |
122
+ | Argument type | Discovery command |
123
+ | ------------- | ----------------------------------- |
124
+ | Provider key | `assistant oauth providers list` |
125
+ | Connection ID | `assistant oauth status <provider>` |
126
+ | OAuth app ID | `assistant oauth apps list` |
127
+ | Contact ID | `assistant contacts list` |
88
128
 
89
129
  ### Error Messages
90
130
 
@@ -104,7 +144,7 @@ throw new Error("Connection not found");
104
144
 
105
145
  ```ts
106
146
  throw new Error(
107
- `Connection "${id}" not found. Run 'assistant oauth connections list' to see available connections.`,
147
+ `Connection "${id}" not found. Run 'assistant oauth status <provider>' to see available connections.`,
108
148
  );
109
149
  ```
110
150
 
@@ -124,12 +124,6 @@ uses a dedicated user data directory at ~/Library/Application Support/Google/Chr
124
124
  and defaults to port 9222. Commands are routed through a Chrome extension
125
125
  relay that bridges the assistant to open Chrome tabs.
126
126
 
127
- Subgroups:
128
- relay Send commands to Chrome tabs via the browser extension relay
129
- launch Launch or connect to a Chrome CDP instance
130
- minimize Minimize the Chrome CDP window
131
- restore Restore the Chrome CDP window
132
-
133
127
  Examples:
134
128
  $ assistant browser chrome launch
135
129
  $ assistant browser chrome launch --start-url "https://example.com" --port 9333
@@ -150,17 +144,8 @@ Examples:
150
144
  `
151
145
  Routes commands to Chrome tabs through the browser extension relay. The relay
152
146
  connects the assistant to a Chrome extension that can inspect and control
153
- browser tabs.
154
-
155
- Available subcommands:
156
- find-tab Find a tab matching a URL pattern
157
- new-tab Open a new tab with a URL
158
- navigate Navigate an existing tab to a new URL
159
- evaluate Execute JavaScript in a tab
160
- get-cookies Fetch cookies for a domain
161
- set-cookie Set a cookie
162
- screenshot Capture a screenshot of a tab
163
- status Check browser extension relay connection status
147
+ browser tabs. Commands support URL glob patterns for tab discovery and
148
+ JavaScript evaluation with stdin piping for long scripts.
164
149
 
165
150
  Examples:
166
151
  $ assistant browser chrome relay find-tab --url "*://*.amazon.com/*"
@@ -132,7 +132,7 @@ Examples:
132
132
  "after",
133
133
  `
134
134
  Arguments:
135
- id UUID of the contact to retrieve
135
+ id UUID of the contact to retrieve. Run 'assistant contacts list' to find IDs.
136
136
 
137
137
  Returns the full contact record including role, display name, and all
138
138
  channel memberships (phone numbers, Telegram IDs, email addresses, etc.).
@@ -174,7 +174,8 @@ Examples:
174
174
  "after",
175
175
  `
176
176
  Arguments:
177
- keepId UUID of the surviving contact that will absorb the other
177
+ keepId UUID of the surviving contact that will absorb the other.
178
+ Run 'assistant contacts list' to find IDs.
178
179
  mergeId UUID of the contact to be merged and deleted
179
180
 
180
181
  All channel memberships, conversation history, and metadata from mergeId
@@ -331,7 +332,8 @@ Examples:
331
332
  "after",
332
333
  `
333
334
  Arguments:
334
- channelId UUID of the contact channel to update
335
+ channelId UUID of the contact channel to update. Run 'assistant contacts get <contactId>'
336
+ to see a contact's channel IDs.
335
337
 
336
338
  Updates the access-control fields on an existing channel. At least one of
337
339
  --status or --policy must be provided.
@@ -590,7 +592,7 @@ Examples:
590
592
  "after",
591
593
  `
592
594
  Arguments:
593
- inviteId UUID of the invite to revoke
595
+ inviteId UUID of the invite to revoke. Run 'assistant contacts invites list' to find IDs.
594
596
 
595
597
  Revokes an active invite so it can no longer be redeemed. Already-redeemed
596
598
  channel memberships are not affected. Returns the updated invite record.
@@ -12,6 +12,7 @@ import { ensureDaemonRunning } from "../../daemon/lifecycle.js";
12
12
  import { formatJson, formatMarkdown } from "../../export/formatter.js";
13
13
  import {
14
14
  clearAll as clearAllConversations,
15
+ countConversationsByScheduleJobId,
15
16
  createConversation,
16
17
  getConversation,
17
18
  getMessages,
@@ -29,6 +30,7 @@ import {
29
30
  loadOrCreateSigningKey,
30
31
  mintDaemonDeliveryToken,
31
32
  } from "../../runtime/auth/token-service.js";
33
+ import { deleteSchedule } from "../../schedule/schedule-store.js";
32
34
  import { timeAgo } from "../../util/time.js";
33
35
  import { initializeDb } from "../db.js";
34
36
  import { log } from "../logger.js";
@@ -119,7 +121,7 @@ Arguments:
119
121
  conversationId Optional conversation ID (or unique prefix). Defaults to the
120
122
  most recent conversation. Supports prefix matching — e.g.
121
123
  "abc123" matches the first conversation whose ID starts with
122
- "abc123".
124
+ "abc123". Run 'assistant conversations list' to find IDs.
123
125
 
124
126
  Two output formats are available:
125
127
  md Markdown conversation transcript (default). Human-readable rendering
@@ -275,6 +277,7 @@ Examples:
275
277
  `
276
278
  Arguments:
277
279
  conversationId Conversation ID (or unique prefix). Supports prefix matching.
280
+ Run 'assistant conversations list' to find IDs.
278
281
 
279
282
  Permanently wipes the conversation and reverts all memory changes it caused:
280
283
  restores superseded memory items, deletes conversation summaries, and cancels
@@ -353,6 +356,15 @@ Examples:
353
356
  return;
354
357
  }
355
358
 
359
+ // Cancel the associated schedule job (if any) before wiping —
360
+ // but only when this is the last conversation referencing it.
361
+ if (
362
+ conversation.scheduleJobId &&
363
+ countConversationsByScheduleJobId(conversation.scheduleJobId) <= 1
364
+ ) {
365
+ deleteSchedule(conversation.scheduleJobId);
366
+ }
367
+
356
368
  // Daemon not running — safe to wipe directly (no in-memory state).
357
369
  const result = wipeConversation(conversation.id);
358
370
 
@@ -129,6 +129,20 @@ export function registerCredentialExecutionCommand(program: Command): void {
129
129
  )
130
130
  .option("--json", "Machine-readable compact JSON output");
131
131
 
132
+ ce.addHelpText(
133
+ "after",
134
+ `
135
+ The Credential Execution Service (CES) mediates all secret-bearing operations.
136
+ Grants authorize specific credential handles for constrained purposes, and
137
+ audit records log each credentialed operation. Neither grants nor audit records
138
+ ever contain raw secret values — only sanitized metadata.
139
+
140
+ Examples:
141
+ $ assistant credential-execution grants list
142
+ $ assistant credential-execution grants revoke <grantId>
143
+ $ assistant credential-execution audit list`,
144
+ );
145
+
132
146
  // -------------------------------------------------------------------------
133
147
  // grants
134
148
  // -------------------------------------------------------------------------
@@ -215,7 +229,8 @@ that grant for credentialed operations. The grant is permanently removed
215
229
  from CES state.
216
230
 
217
231
  Arguments:
218
- grantId The stable grant identifier (UUID)
232
+ grantId The stable grant identifier (UUID). Run 'assistant credential-execution
233
+ grants list' to find grant IDs.
219
234
 
220
235
  Examples:
221
236
  $ assistant credential-execution grants revoke 7a3b1c2d-4e5f-6789-abcd-ef0123456789
@@ -397,10 +397,7 @@ Examples:
397
397
  credential
398
398
  .command("set <value>")
399
399
  .description("Store a secret and create or update its metadata")
400
- .requiredOption(
401
- "--service <service>",
402
- "Service namespace (e.g. integration:google)",
403
- )
400
+ .requiredOption("--service <service>", "Service namespace (e.g. google)")
404
401
  .requiredOption("--field <field>", "Field name (e.g. client_secret)")
405
402
  .option("--label <label>", 'Human-friendly label (e.g. "prod", "work")')
406
403
  .option("--description <description>", "What this credential is used for")
@@ -511,10 +508,7 @@ Examples:
511
508
  `${service}:${field}`,
512
509
  );
513
510
  if (secretResult === "error") {
514
- writeError(
515
- cmd,
516
- "Failed to delete credential from secure storage",
517
- );
511
+ writeError(cmd, "Failed to delete credential from secure storage");
518
512
  process.exitCode = 1;
519
513
  return;
520
514
  }