@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
@@ -142,7 +142,7 @@ Examples:
142
142
  "after",
143
143
  `
144
144
  Arguments:
145
- id The sequence ID (e.g. seq_abc123)
145
+ id The sequence ID (e.g. seq_abc123). Run 'assistant sequence list' to find IDs.
146
146
 
147
147
  Returns full sequence details: name, status, channel, description, exit-on-reply
148
148
  setting, all steps with delay and approval configuration, and enrollment
@@ -215,7 +215,7 @@ Examples:
215
215
  "after",
216
216
  `
217
217
  Arguments:
218
- id The sequence ID to pause (e.g. seq_abc123)
218
+ id The sequence ID to pause (e.g. seq_abc123). Run 'assistant sequence list' to find IDs.
219
219
 
220
220
  Pauses a sequence, halting all scheduled step deliveries. Existing active
221
221
  enrollments remain in their current state but no new steps will be sent
@@ -246,7 +246,7 @@ Examples:
246
246
  "after",
247
247
  `
248
248
  Arguments:
249
- id The sequence ID to resume (e.g. seq_abc123)
249
+ id The sequence ID to resume (e.g. seq_abc123). Run 'assistant sequence list' to find IDs.
250
250
 
251
251
  Resumes a paused sequence, re-enabling scheduled step deliveries for all
252
252
  active enrollments. No-op if the sequence is already active.
@@ -276,7 +276,8 @@ Examples:
276
276
  "after",
277
277
  `
278
278
  Arguments:
279
- enrollmentId The enrollment ID to cancel (e.g. enr_xyz789)
279
+ enrollmentId The enrollment ID to cancel (e.g. enr_xyz789). Run 'assistant sequence get <id>'
280
+ to see enrollment IDs for a sequence.
280
281
 
281
282
  Immediately cancels a specific enrollment, stopping all future step
282
283
  deliveries for that contact in this sequence. The enrollment status
@@ -101,6 +101,19 @@ export function registerShotgunCommand(program: Command): void {
101
101
  .command("shotgun")
102
102
  .description("Start and monitor screen-watch (shotgun) sessions via IPC");
103
103
 
104
+ shotgun.addHelpText(
105
+ "after",
106
+ `
107
+ Screen-watch sessions capture periodic screenshots and feed them to the
108
+ assistant for observation. The CLI communicates with the running assistant
109
+ via IPC signal files — the assistant must be running for these commands
110
+ to work.
111
+
112
+ Examples:
113
+ $ assistant shotgun start --duration 600 --focus "browsing workflow"
114
+ $ assistant shotgun status <watchId>`,
115
+ );
116
+
104
117
  shotgun
105
118
  .command("start")
106
119
  .description("Start a new screen-watch session")
@@ -206,6 +219,9 @@ Examples:
206
219
  .addHelpText(
207
220
  "after",
208
221
  `
222
+ Arguments:
223
+ watchId The watch session ID returned by 'assistant shotgun start'.
224
+
209
225
  Queries the status of an existing screen-watch session by watchId.
210
226
 
211
227
  Output (JSON): { ok, watchId, conversationId, status }
@@ -1,3 +1,6 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
1
4
  import type { Command } from "commander";
2
5
 
3
6
  import type { CatalogSkill } from "../../skills/catalog-install.js";
@@ -8,7 +11,11 @@ import {
8
11
  readLocalCatalog,
9
12
  uninstallSkillLocally,
10
13
  } from "../../skills/catalog-install.js";
11
- import type { AuditResponse } from "../../skills/skillssh-registry.js";
14
+ import { filterByQuery } from "../../skills/catalog-search.js";
15
+ import type {
16
+ AuditResponse,
17
+ SkillsShSearchResult,
18
+ } from "../../skills/skillssh-registry.js";
12
19
  import {
13
20
  fetchSkillAudits,
14
21
  formatAuditBadges,
@@ -16,6 +23,7 @@ import {
16
23
  resolveSkillSource,
17
24
  searchSkillsRegistry,
18
25
  } from "../../skills/skillssh-registry.js";
26
+ import { getWorkspaceSkillsDir } from "../../util/platform.js";
19
27
  import { log } from "../logger.js";
20
28
 
21
29
  // ---------------------------------------------------------------------------
@@ -49,6 +57,16 @@ Examples:
49
57
  .command("list")
50
58
  .description("List available catalog skills")
51
59
  .option("--json", "Machine-readable JSON output")
60
+ .addHelpText(
61
+ "after",
62
+ `
63
+ Lists all skills available in the Vellum catalog with their ID, name,
64
+ description, and dependency information.
65
+
66
+ Examples:
67
+ $ assistant skills list
68
+ $ assistant skills list --json`,
69
+ )
52
70
  .action(async (opts: { json?: boolean }) => {
53
71
  try {
54
72
  // In dev mode, use the local catalog as the source of truth
@@ -93,20 +111,20 @@ Examples:
93
111
 
94
112
  skills
95
113
  .command("search <query>")
96
- .description("Search the skills.sh community registry")
97
- .option("--limit <n>", "Maximum number of results", "10")
114
+ .description("Search the Vellum catalog and skills.sh community registry")
115
+ .option("--limit <n>", "Maximum number of community results", "10")
98
116
  .option("--json", "Machine-readable JSON output")
99
117
  .addHelpText(
100
118
  "after",
101
119
  `
102
120
  Arguments:
103
121
  query Free-text search term matched against skill names, descriptions,
104
- and tags in the skills.sh registry.
122
+ and tags. Searches the Vellum catalog first, then the skills.sh
123
+ community registry.
105
124
 
106
- Searches the skills.sh community registry and displays matching skills
107
- with install counts and security audit badges (ATH, Socket, Snyk).
108
- Audit fetch failures are non-fatal results are still shown without
109
- security data.
125
+ Displays results from both sources with clear labels. When a skill ID
126
+ exists in both the Vellum catalog and the community registry, a conflict
127
+ note is shown with guidance on which install command to use.
110
128
 
111
129
  Examples:
112
130
  $ assistant skills search react
@@ -118,36 +136,80 @@ Examples:
118
136
  const limit = parseInt(opts.limit, 10) || 10;
119
137
 
120
138
  try {
121
- const results = await searchSkillsRegistry(query, limit);
139
+ // ── Vellum catalog search ────────────────────────────────────
140
+ const repoSkillsDir = getRepoSkillsDir();
141
+ let catalog: CatalogSkill[];
142
+ if (repoSkillsDir) {
143
+ catalog = readLocalCatalog(repoSkillsDir);
144
+ } else {
145
+ try {
146
+ catalog = await fetchCatalog();
147
+ } catch {
148
+ catalog = [];
149
+ }
150
+ }
151
+
152
+ const catalogMatches = filterByQuery(catalog, query, [
153
+ (s) => s.id,
154
+ (s) => s.name,
155
+ (s) => s.description,
156
+ ]);
157
+
158
+ // ── Community registry search (non-fatal on failure) ─────────
159
+ let registryResults: SkillsShSearchResult[] = [];
160
+ let registryError: string | undefined;
161
+ try {
162
+ registryResults = await searchSkillsRegistry(query, limit);
163
+ } catch (err) {
164
+ registryError = err instanceof Error ? err.message : String(err);
165
+ }
166
+
167
+ // ── Conflict detection ───────────────────────────────────────
168
+ const catalogIds = new Set(catalogMatches.map((s) => s.id));
169
+ const conflictIds = new Set(
170
+ registryResults
171
+ .filter((r) => catalogIds.has(r.skillId))
172
+ .map((r) => r.skillId),
173
+ );
122
174
 
123
- if (results.length === 0) {
175
+ if (catalogMatches.length === 0 && registryResults.length === 0) {
124
176
  if (json) {
125
- console.log(JSON.stringify({ ok: true, results: [], audits: {} }));
177
+ console.log(
178
+ JSON.stringify({
179
+ ok: true,
180
+ catalog: [],
181
+ community: [],
182
+ audits: {},
183
+ ...(registryError ? { registryError } : {}),
184
+ }),
185
+ );
126
186
  } else {
127
187
  log.info(`No skills found for "${query}".`);
188
+ if (registryError) {
189
+ log.warn(`(skills.sh registry unavailable: ${registryError})`);
190
+ }
128
191
  }
129
192
  return;
130
193
  }
131
194
 
132
- // Group skill slugs by source for batch audit lookups
133
- const sourceToSlugs = new Map<string, string[]>();
134
- for (const r of results) {
135
- const slugs = sourceToSlugs.get(r.source) ?? [];
136
- slugs.push(r.skillId);
137
- sourceToSlugs.set(r.source, slugs);
138
- }
139
-
140
- // Fetch audits for each unique source, keyed by source/skillId
141
- // to avoid collisions when different sources share the same slug.
195
+ // ── Fetch audits for community results ───────────────────────
142
196
  const allAudits: AuditResponse = {};
143
- for (const [source, slugs] of sourceToSlugs) {
144
- try {
145
- const audits = await fetchSkillAudits(source, slugs);
146
- for (const [skillId, auditData] of Object.entries(audits)) {
147
- allAudits[`${source}/${skillId}`] = auditData;
197
+ if (registryResults.length > 0) {
198
+ const sourceToSlugs = new Map<string, string[]>();
199
+ for (const r of registryResults) {
200
+ const slugs = sourceToSlugs.get(r.source) ?? [];
201
+ slugs.push(r.skillId);
202
+ sourceToSlugs.set(r.source, slugs);
203
+ }
204
+ for (const [source, slugs] of sourceToSlugs) {
205
+ try {
206
+ const audits = await fetchSkillAudits(source, slugs);
207
+ for (const [skillId, auditData] of Object.entries(audits)) {
208
+ allAudits[`${source}/${skillId}`] = auditData;
209
+ }
210
+ } catch {
211
+ // Audit fetch failures are non-fatal
148
212
  }
149
- } catch {
150
- // Audit fetch failures are non-fatal; display results without audits
151
213
  }
152
214
  }
153
215
 
@@ -155,26 +217,68 @@ Examples:
155
217
  console.log(
156
218
  JSON.stringify({
157
219
  ok: true,
158
- results,
220
+ catalog: catalogMatches,
221
+ community: registryResults,
159
222
  audits: allAudits,
223
+ ...(registryError ? { registryError } : {}),
160
224
  }),
161
225
  );
162
226
  return;
163
227
  }
164
228
 
165
- log.info(`Search results for "${query}" (${results.length}):\n`);
166
- for (const r of results) {
167
- log.info(` ${r.name}`);
168
- log.info(` ID: ${r.skillId}`);
169
- log.info(` Source: ${r.source}`);
170
- log.info(` Installs: ${r.installs}`);
171
- const auditData = allAudits[`${r.source}/${r.skillId}`];
172
- if (auditData) {
173
- log.info(` ${formatAuditBadges(auditData)}`);
174
- } else {
175
- log.info(" Security: no audit data");
229
+ // ── Installed-state detection ─────────────────────────────────
230
+ const skillsDir = getWorkspaceSkillsDir();
231
+ const isInstalled = (id: string) =>
232
+ existsSync(join(skillsDir, id, "SKILL.md"));
233
+
234
+ // ── Display catalog results ──────────────────────────────────
235
+ if (catalogMatches.length > 0) {
236
+ log.info(`Vellum catalog (${catalogMatches.length}):\n`);
237
+ for (const s of catalogMatches) {
238
+ const emoji = s.emoji ? `${s.emoji} ` : "";
239
+ const installed = isInstalled(s.id);
240
+ const badge = installed ? " [installed]" : "";
241
+ log.info(` ${emoji}${s.name}${badge}`);
242
+ if (s.name !== s.id) {
243
+ log.info(` ID: ${s.id}`);
244
+ }
245
+ log.info(` Description: ${s.description}`);
246
+ log.info(` Install: assistant skills install ${s.id}`);
247
+ if (conflictIds.has(s.id)) {
248
+ log.info(` NOTE: Also found in community registry`);
249
+ }
250
+ log.info("");
251
+ }
252
+ }
253
+
254
+ // ── Display community results ────────────────────────────────
255
+ if (registryResults.length > 0) {
256
+ log.info(`Community registry (${registryResults.length}):\n`);
257
+ for (const r of registryResults) {
258
+ const installed = isInstalled(r.skillId);
259
+ const badge = installed ? " [installed]" : "";
260
+ log.info(` ${r.name}${badge}`);
261
+ if (r.name !== r.skillId) {
262
+ log.info(` ID: ${r.skillId}`);
263
+ }
264
+ log.info(` Source: ${r.source}`);
265
+ log.info(` Installs: ${r.installs}`);
266
+ const auditData = allAudits[`${r.source}/${r.skillId}`];
267
+ if (auditData) {
268
+ log.info(` ${formatAuditBadges(auditData)}`);
269
+ } else {
270
+ log.info(" Security: no audit data");
271
+ }
272
+ log.info(
273
+ ` Install: assistant skills add ${r.source}@${r.skillId}`,
274
+ );
275
+ if (conflictIds.has(r.skillId)) {
276
+ log.info(` NOTE: Conflicts with Vellum catalog skill`);
277
+ }
278
+ log.info("");
176
279
  }
177
- log.info("");
280
+ } else if (registryError) {
281
+ log.warn(`\n(skills.sh registry unavailable: ${registryError})`);
178
282
  }
179
283
  } catch (err) {
180
284
  const msg = err instanceof Error ? err.message : String(err);
@@ -192,6 +296,21 @@ Examples:
192
296
  .description("Install a skill from the catalog")
193
297
  .option("--overwrite", "Replace an already installed skill")
194
298
  .option("--json", "Machine-readable JSON output")
299
+ .addHelpText(
300
+ "after",
301
+ `
302
+ Arguments:
303
+ skill-id Skill identifier from the Vellum catalog. Run 'assistant skills list'
304
+ to see available IDs. For community skills, use 'assistant skills add'.
305
+
306
+ Downloads and installs the skill into the workspace skills directory. If the
307
+ skill is already installed, use --overwrite to replace it.
308
+
309
+ Examples:
310
+ $ assistant skills install weather
311
+ $ assistant skills install weather --overwrite
312
+ $ assistant skills install weather --json`,
313
+ )
195
314
  .action(
196
315
  async (
197
316
  skillId: string,
@@ -244,6 +363,19 @@ Examples:
244
363
  .command("uninstall <skill-id>")
245
364
  .description("Uninstall a previously installed skill")
246
365
  .option("--json", "Machine-readable JSON output")
366
+ .addHelpText(
367
+ "after",
368
+ `
369
+ Arguments:
370
+ skill-id Skill identifier to remove. Run 'assistant skills list' to see
371
+ installed skills.
372
+
373
+ Removes the skill directory from the workspace. This action cannot be undone.
374
+
375
+ Examples:
376
+ $ assistant skills uninstall weather
377
+ $ assistant skills uninstall weather --json`,
378
+ )
247
379
  .action(async (skillId: string, opts: { json?: boolean }) => {
248
380
  const json = opts.json ?? false;
249
381
 
@@ -178,11 +178,6 @@ Reads from the local LLM usage event ledger (llm_usage_events table) to
178
178
  display token consumption and cost data. Operates on the local SQLite
179
179
  database directly — does not require the assistant to be running.
180
180
 
181
- Subcommands:
182
- totals Aggregate totals for a time range (default when no subcommand given)
183
- daily Per-day token and cost breakdown
184
- breakdown Grouped breakdown by actor, provider, or model
185
-
186
181
  Time range can be specified with --range presets (today, week, month, all)
187
182
  or explicit --from / --to epoch-millisecond timestamps.
188
183
 
@@ -285,12 +280,11 @@ Examples:
285
280
  .addHelpText(
286
281
  "after",
287
282
  `
288
- Arguments:
289
- --group-by <dimension> One of: actor, provider, model (default: model)
290
- actor Groups by the subsystem that made the call (main_agent,
291
- title_generator, etc.)
292
- provider Groups by LLM provider (anthropic, openai, etc.)
293
- model Groups by model name (claude-sonnet-4-20250514, etc.)
283
+ Grouping dimensions:
284
+ actor Groups by the subsystem that made the call (main_agent,
285
+ title_generator, etc.)
286
+ provider Groups by LLM provider (anthropic, openai, etc.)
287
+ model Groups by model name (claude-sonnet-4-20250514, etc.)
294
288
 
295
289
  Shows one row per group with input/output tokens, estimated cost, and
296
290
  call count. Rows are sorted by cost descending.
@@ -88,34 +88,6 @@ async function daemonFetch(
88
88
  // Internal helpers
89
89
  // ---------------------------------------------------------------------------
90
90
 
91
- /**
92
- * Derive the canonical credential storage key from a "service:field" name.
93
- * Mirrors the parsing in secret-routes.ts handleAddSecret / handleDeleteSecret.
94
- *
95
- * Uses lastIndexOf to split on the *last* colon so compound service names
96
- * (e.g. "integration:google") are preserved intact while the single-segment
97
- * field name is extracted correctly.
98
- */
99
- function deriveCredentialStorageKey(name: string): string {
100
- // Already a canonical storage key (credential/service/field) — return as-is
101
- // to avoid double-encoding (e.g. "credential/integration:google/access_token"
102
- // would otherwise become "credential/credential/integration/google/access_token").
103
- if (name.startsWith("credential/")) {
104
- return name;
105
- }
106
-
107
- const colonIdx = name.lastIndexOf(":");
108
- if (colonIdx < 1 || colonIdx === name.length - 1) {
109
- // Malformed — return raw name so the caller stores *something*.
110
- // The daemon would reject this with a 400, so this only fires in
111
- // the offline fallback path with bad input.
112
- return name;
113
- }
114
- const service = name.slice(0, colonIdx);
115
- const field = name.slice(colonIdx + 1);
116
- return credentialKey(service, field);
117
- }
118
-
119
91
  function deriveReadSecretRequest(account: string): {
120
92
  type: "api_key" | "credential";
121
93
  name: string;
@@ -227,11 +199,17 @@ export async function setSecureKeyViaDaemon(
227
199
  }
228
200
 
229
201
  // Daemon unreachable — fall back to direct write.
230
- // For credentials, derive the canonical storage key (credential/service/field)
231
- // to match the daemon path which uses credentialKey().
232
- const storageKey =
233
- type === "credential" ? deriveCredentialStorageKey(name) : name;
234
- return setSecureKeyAsync(storageKey, value);
202
+ // For credentials, convert "service:field" to the canonical
203
+ // "credential/service/field" storage key using credentialKey().
204
+ if (type === "credential" && !name.startsWith("credential/")) {
205
+ const colonIdx = name.lastIndexOf(":");
206
+ if (colonIdx > 0 && colonIdx < name.length - 1) {
207
+ const service = name.slice(0, colonIdx);
208
+ const field = name.slice(colonIdx + 1);
209
+ return setSecureKeyAsync(credentialKey(service, field), value);
210
+ }
211
+ }
212
+ return setSecureKeyAsync(name, value);
235
213
  }
236
214
 
237
215
  /**
@@ -260,11 +238,17 @@ export async function deleteSecureKeyViaDaemon(
260
238
  }
261
239
 
262
240
  // Daemon unreachable — fall back to direct delete.
263
- // For credentials, derive the canonical storage key (credential/service/field)
264
- // to match the daemon path which uses credentialKey().
265
- const storageKey =
266
- type === "credential" ? deriveCredentialStorageKey(name) : name;
267
- return deleteSecureKeyAsync(storageKey);
241
+ // For credentials, convert "service:field" to the canonical
242
+ // "credential/service/field" storage key using credentialKey().
243
+ if (type === "credential" && !name.startsWith("credential/")) {
244
+ const colonIdx = name.lastIndexOf(":");
245
+ if (colonIdx > 0 && colonIdx < name.length - 1) {
246
+ const service = name.slice(0, colonIdx);
247
+ const field = name.slice(colonIdx + 1);
248
+ return deleteSecureKeyAsync(credentialKey(service, field));
249
+ }
250
+ }
251
+ return deleteSecureKeyAsync(name);
268
252
  }
269
253
 
270
254
  /**
@@ -25,7 +25,7 @@ import { registerMcpCommand } from "./commands/mcp.js";
25
25
  import { registerMemoryCommand } from "./commands/memory.js";
26
26
  import { registerNotificationsCommand } from "./commands/notifications.js";
27
27
  import { registerOAuthCommand } from "./commands/oauth/index.js";
28
- import { registerPlatformCommand } from "./commands/platform.js";
28
+ import { registerPlatformCommand } from "./commands/platform/index.js";
29
29
  import { registerSequenceCommand } from "./commands/sequence.js";
30
30
  import { registerShotgunCommand } from "./commands/shotgun.js";
31
31
  import { registerSkillsCommand } from "./commands/skills.js";
@@ -18,10 +18,10 @@
18
18
  */
19
19
 
20
20
  import { existsSync, readFileSync } from "node:fs";
21
- import { homedir } from "node:os";
22
21
  import { dirname, join } from "node:path";
23
22
 
24
- import { getBaseDataDir, getIsContainerized } from "./env-registry.js";
23
+ import { getRootDir } from "../util/platform.js";
24
+ import { getIsContainerized } from "./env-registry.js";
25
25
  import type { AssistantConfig } from "./schema.js";
26
26
 
27
27
  // ---------------------------------------------------------------------------
@@ -135,17 +135,13 @@ interface FeatureFlagFileData {
135
135
  *
136
136
  * Docker: `GATEWAY_SECURITY_DIR/feature-flags.json`
137
137
  * Local: `~/.vellum/protected/feature-flags.json`
138
- *
139
- * Uses `BASE_DATA_DIR` when set (multi-instance mode) so per-instance
140
- * feature flag files are correctly scoped.
141
138
  */
142
139
  function getFeatureFlagOverridesPath(): string {
143
140
  const securityDir = process.env.GATEWAY_SECURITY_DIR;
144
141
  if (securityDir) {
145
142
  return join(securityDir, "feature-flags.json");
146
143
  }
147
- const root = join(getBaseDataDir() || homedir(), ".vellum");
148
- return join(root, "protected", "feature-flags.json");
144
+ return join(getRootDir(), "protected", "feature-flags.json");
149
145
  }
150
146
 
151
147
  /**
@@ -44,7 +44,7 @@ export async function run(
44
44
  }
45
45
 
46
46
  try {
47
- const connection = await resolveOAuthConnection("integration:google", {
47
+ const connection = await resolveOAuthConnection("google", {
48
48
  account,
49
49
  });
50
50
  switch (action) {
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: conversations
3
+ description: Manage conversation threads (rename)
4
+ compatibility: "Designed for Vellum personal assistants"
5
+ metadata:
6
+ emoji: "\U0001F4AC"
7
+ vellum:
8
+ display-name: "Conversations"
9
+ ---
10
+
11
+ Tools for managing conversation threads.
12
+
13
+ ## Renaming
14
+
15
+ Use the `rename_conversation` tool to rename the current conversation thread when:
16
+ - The topic has shifted significantly from the original title
17
+ - The auto-generated title is generic or unhelpful
18
+ - The user explicitly asks to rename the thread
19
+
20
+ Keep titles concise (under 60 characters) and descriptive of the current topic.
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": 1,
3
+ "tools": [
4
+ {
5
+ "name": "rename_conversation",
6
+ "description": "Rename the current conversation thread. Use this when the conversation topic has shifted significantly from the original title, or when you notice the title is generic/unhelpful and you can provide a better one based on what has been discussed.",
7
+ "category": "conversation",
8
+ "risk": "low",
9
+ "input_schema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "title": {
13
+ "type": "string",
14
+ "description": "The new title for the conversation. Should be concise (under 60 characters) and descriptive of the current topic."
15
+ }
16
+ },
17
+ "required": ["title"]
18
+ },
19
+ "executor": "tools/rename-conversation.ts",
20
+ "execution_target": "host"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ getConversation,
3
+ updateConversationTitle,
4
+ } from "../../../../memory/conversation-crud.js";
5
+ import { buildAssistantEvent } from "../../../../runtime/assistant-event.js";
6
+ import { assistantEventHub } from "../../../../runtime/assistant-event-hub.js";
7
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../../../runtime/assistant-scope.js";
8
+ import type {
9
+ ToolContext,
10
+ ToolExecutionResult,
11
+ } from "../../../../tools/types.js";
12
+ import { getLogger } from "../../../../util/logger.js";
13
+
14
+ const log = getLogger("rename-conversation");
15
+
16
+ export async function run(
17
+ input: Record<string, unknown>,
18
+ context: ToolContext,
19
+ ): Promise<ToolExecutionResult> {
20
+ const title = input.title;
21
+ if (typeof title !== "string" || title.trim() === "") {
22
+ return {
23
+ content: "Error: title must be a non-empty string.",
24
+ isError: true,
25
+ };
26
+ }
27
+
28
+ const trimmedTitle = title.trim();
29
+ const conversationId = context.conversationId;
30
+
31
+ const conversation = getConversation(conversationId);
32
+ if (!conversation) {
33
+ return {
34
+ content: `Error: conversation ${conversationId} not found.`,
35
+ isError: true,
36
+ };
37
+ }
38
+
39
+ // Persist with isAutoTitle = 0 so auto-generation won't overwrite it
40
+ updateConversationTitle(conversationId, trimmedTitle, 0);
41
+
42
+ // Notify connected clients so the UI updates immediately
43
+ const assistantId = context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
44
+ assistantEventHub
45
+ .publish(
46
+ buildAssistantEvent(
47
+ assistantId,
48
+ {
49
+ type: "conversation_title_updated",
50
+ conversationId,
51
+ title: trimmedTitle,
52
+ },
53
+ conversationId,
54
+ ),
55
+ )
56
+ .catch((err) => {
57
+ log.warn({ err }, "Failed to publish conversation_title_updated event");
58
+ });
59
+
60
+ log.info({ conversationId, title: trimmedTitle }, "Conversation renamed");
61
+
62
+ return {
63
+ content: `Conversation renamed to "${trimmedTitle}".`,
64
+ isError: false,
65
+ };
66
+ }