@vellumai/assistant 0.4.3 → 0.4.4

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 (183) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +40 -3
  3. package/README.md +43 -35
  4. package/package.json +1 -1
  5. package/scripts/ipc/generate-swift.ts +1 -0
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  7. package/src/__tests__/actor-token-service.test.ts +1099 -0
  8. package/src/__tests__/agent-loop.test.ts +51 -0
  9. package/src/__tests__/approval-routes-http.test.ts +2 -0
  10. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  11. package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
  12. package/src/__tests__/call-controller.test.ts +49 -0
  13. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  14. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  15. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  16. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  17. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  18. package/src/__tests__/channel-guardian.test.ts +0 -87
  19. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  20. package/src/__tests__/checker.test.ts +33 -12
  21. package/src/__tests__/config-schema.test.ts +4 -0
  22. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  23. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  24. package/src/__tests__/conversation-routes.test.ts +12 -3
  25. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  26. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  27. package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
  28. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  29. package/src/__tests__/guardian-outbound-http.test.ts +4 -4
  30. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  31. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  32. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  33. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  34. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  35. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  36. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  37. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  38. package/src/__tests__/non-member-access-request.test.ts +131 -8
  39. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  40. package/src/__tests__/notification-decision-strategy.test.ts +62 -2
  41. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  42. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  43. package/src/__tests__/relay-server.test.ts +841 -39
  44. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  45. package/src/__tests__/session-agent-loop.test.ts +1 -0
  46. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  47. package/src/__tests__/session-init.benchmark.test.ts +0 -1
  48. package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
  49. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  50. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  51. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  52. package/src/__tests__/tool-executor.test.ts +21 -2
  53. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  54. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  55. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  56. package/src/__tests__/twilio-config.test.ts +2 -13
  57. package/src/agent/loop.ts +1 -1
  58. package/src/approvals/guardian-decision-primitive.ts +10 -2
  59. package/src/approvals/guardian-request-resolvers.ts +128 -9
  60. package/src/calls/call-constants.ts +21 -0
  61. package/src/calls/call-controller.ts +9 -2
  62. package/src/calls/call-domain.ts +28 -7
  63. package/src/calls/call-pointer-message-composer.ts +154 -0
  64. package/src/calls/call-pointer-messages.ts +106 -27
  65. package/src/calls/guardian-dispatch.ts +4 -2
  66. package/src/calls/relay-server.ts +424 -12
  67. package/src/calls/twilio-config.ts +4 -11
  68. package/src/calls/twilio-routes.ts +1 -1
  69. package/src/calls/types.ts +3 -1
  70. package/src/cli.ts +5 -4
  71. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  72. package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
  73. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  74. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  75. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  76. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  77. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  78. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  79. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  80. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  81. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  82. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  83. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
  84. package/src/config/calls-schema.ts +24 -0
  85. package/src/config/env.ts +22 -0
  86. package/src/config/feature-flag-registry.json +8 -0
  87. package/src/config/schema.ts +2 -2
  88. package/src/config/skills.ts +11 -0
  89. package/src/config/system-prompt.ts +11 -1
  90. package/src/config/templates/SOUL.md +2 -0
  91. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  92. package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
  93. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  94. package/src/daemon/call-pointer-generators.ts +59 -0
  95. package/src/daemon/computer-use-session.ts +2 -5
  96. package/src/daemon/handlers/apps.ts +76 -20
  97. package/src/daemon/handlers/config-channels.ts +5 -55
  98. package/src/daemon/handlers/config-inbox.ts +9 -3
  99. package/src/daemon/handlers/config-ingress.ts +28 -3
  100. package/src/daemon/handlers/config-telegram.ts +12 -0
  101. package/src/daemon/handlers/config.ts +2 -6
  102. package/src/daemon/handlers/pairing.ts +2 -0
  103. package/src/daemon/handlers/sessions.ts +48 -3
  104. package/src/daemon/handlers/shared.ts +17 -2
  105. package/src/daemon/ipc-contract/integrations.ts +1 -99
  106. package/src/daemon/ipc-contract/messages.ts +47 -1
  107. package/src/daemon/ipc-contract/notifications.ts +11 -0
  108. package/src/daemon/ipc-contract-inventory.json +2 -4
  109. package/src/daemon/lifecycle.ts +17 -0
  110. package/src/daemon/server.ts +14 -1
  111. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  112. package/src/daemon/session-agent-loop.ts +22 -11
  113. package/src/daemon/session-lifecycle.ts +1 -1
  114. package/src/daemon/session-process.ts +11 -1
  115. package/src/daemon/session-runtime-assembly.ts +3 -0
  116. package/src/daemon/session-surfaces.ts +3 -2
  117. package/src/daemon/session.ts +88 -1
  118. package/src/daemon/tool-side-effects.ts +22 -0
  119. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  120. package/src/home-base/prebuilt/index.html +40 -0
  121. package/src/inbound/platform-callback-registration.ts +157 -0
  122. package/src/memory/canonical-guardian-store.ts +1 -1
  123. package/src/memory/db-init.ts +4 -0
  124. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  125. package/src/memory/migrations/index.ts +1 -0
  126. package/src/memory/schema.ts +16 -0
  127. package/src/messaging/provider-types.ts +24 -0
  128. package/src/messaging/provider.ts +7 -0
  129. package/src/messaging/providers/gmail/adapter.ts +127 -0
  130. package/src/messaging/providers/sms/adapter.ts +40 -37
  131. package/src/notifications/adapters/macos.ts +45 -2
  132. package/src/notifications/broadcaster.ts +16 -0
  133. package/src/notifications/copy-composer.ts +39 -1
  134. package/src/notifications/decision-engine.ts +22 -9
  135. package/src/notifications/destination-resolver.ts +16 -2
  136. package/src/notifications/emit-signal.ts +16 -8
  137. package/src/notifications/guardian-question-mode.ts +419 -0
  138. package/src/notifications/signal.ts +14 -3
  139. package/src/permissions/checker.ts +13 -1
  140. package/src/permissions/prompter.ts +14 -0
  141. package/src/providers/anthropic/client.ts +20 -0
  142. package/src/providers/provider-send-message.ts +15 -3
  143. package/src/runtime/access-request-helper.ts +71 -1
  144. package/src/runtime/actor-token-service.ts +234 -0
  145. package/src/runtime/actor-token-store.ts +236 -0
  146. package/src/runtime/channel-approvals.ts +5 -3
  147. package/src/runtime/channel-readiness-service.ts +23 -64
  148. package/src/runtime/channel-readiness-types.ts +3 -4
  149. package/src/runtime/channel-retry-sweep.ts +4 -1
  150. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  151. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  152. package/src/runtime/guardian-context-resolver.ts +82 -0
  153. package/src/runtime/guardian-outbound-actions.ts +0 -3
  154. package/src/runtime/guardian-reply-router.ts +67 -30
  155. package/src/runtime/guardian-vellum-migration.ts +57 -0
  156. package/src/runtime/http-server.ts +65 -12
  157. package/src/runtime/http-types.ts +13 -0
  158. package/src/runtime/invite-redemption-service.ts +8 -0
  159. package/src/runtime/local-actor-identity.ts +76 -0
  160. package/src/runtime/middleware/actor-token.ts +271 -0
  161. package/src/runtime/routes/approval-routes.ts +82 -7
  162. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  163. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  164. package/src/runtime/routes/conversation-routes.ts +140 -52
  165. package/src/runtime/routes/events-routes.ts +20 -5
  166. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  167. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  168. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  169. package/src/runtime/routes/inbound-message-handler.ts +143 -2
  170. package/src/runtime/routes/integration-routes.ts +7 -15
  171. package/src/runtime/routes/pairing-routes.ts +163 -0
  172. package/src/runtime/routes/twilio-routes.ts +934 -0
  173. package/src/runtime/tool-grant-request-helper.ts +3 -1
  174. package/src/security/oauth2.ts +27 -2
  175. package/src/security/token-manager.ts +46 -10
  176. package/src/tools/browser/browser-execution.ts +4 -3
  177. package/src/tools/browser/browser-handoff.ts +10 -18
  178. package/src/tools/browser/browser-manager.ts +80 -25
  179. package/src/tools/browser/browser-screencast.ts +35 -119
  180. package/src/tools/permission-checker.ts +15 -4
  181. package/src/tools/tool-approval-handler.ts +242 -18
  182. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  183. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -7,6 +7,21 @@ metadata: {"vellum": {"emoji": "💬"}}
7
7
 
8
8
  You are a unified messaging assistant with access to multiple platforms (Slack, Gmail, Telegram, and more). Use the messaging tools to help users read, search, organize, draft, and send messages across all connected platforms.
9
9
 
10
+ ## Email Routing Priority
11
+
12
+ When the user mentions "email" — sending, reading, checking, decluttering, drafting, or anything else — **always default to the user's own email (Gmail)** unless they explicitly ask about the assistant's own email address (e.g., "set up your email", "send from your address", "check your inbox"). The vast majority of email requests are about the user's Gmail, not the assistant's AgentMail address.
13
+
14
+ Do not offer AgentMail as an option or mention it unless the user specifically asks. If Gmail is not connected, guide them through Gmail setup — do not suggest AgentMail as an alternative.
15
+
16
+ ## Communication Style
17
+
18
+ - **Be action-oriented.** When the user asks to do something ("declutter", "check my email"), start doing it immediately. Don't ask for permission to read their inbox — that's obviously what they want.
19
+ - **Keep it human.** Never mention OAuth, tokens, APIs, sandboxes, credential proxies, or other technical internals. If something isn't working, say "Gmail needs to be reconnected" — not "the OAuth2 access token for integration:gmail has expired."
20
+ - **Show progress.** When running a tool that scans many emails, tell the user what you're doing: "Scanning your inbox for clutter..." Don't go silent.
21
+ - **Be brief and warm.** One or two sentences per update is plenty. Don't over-explain what you're about to do — just do it and narrate lightly.
22
+
23
+ When a platform is connected (auth test succeeds), always use the messaging API tools for that platform. Never fall back to browser automation, shell commands (bash, curl), or any other approach for operations that messaging tools can handle. The messaging tools handle authentication internally — never try to access tokens or call APIs directly. Browser automation is only appropriate for initial credential setup (OAuth consent screens), not for day-to-day messaging operations.
24
+
10
25
  ## Connection Setup
11
26
 
12
27
  Before using any messaging tool, verify that the platform is connected by calling `messaging_auth_test` with the appropriate `platform` parameter. If the call fails with a token/authorization error, follow the steps below.
@@ -15,6 +30,14 @@ Before using any messaging tool, verify that the platform is connected by callin
15
30
 
16
31
  Gmail, Slack, and Telegram setup all require a publicly reachable URL for OAuth callbacks or webhook delivery. The **public-ingress** skill handles ngrok tunnel setup and persists the URL as `ingress.publicBaseUrl`. Each setup skill below declares `public-ingress` as a dependency and will prompt you to run it if `ingress.publicBaseUrl` is not configured.
17
32
 
33
+ ### Email Connection Flow
34
+
35
+ When the user asks to "connect my email", "set up email", "manage my email", or similar — and has not named a specific provider:
36
+
37
+ 1. **Discover what's connected.** Call `messaging_auth_test` for `gmail` (and any other email-capable platforms). If one succeeds, tell the user it's already connected and proceed with their request.
38
+ 2. **If nothing is connected**, ask which provider they use — but keep it brief and conversational (e.g., "Which email do you use — Gmail, Outlook, etc.?"), not a numbered list of options with descriptions.
39
+ 3. **Once the provider is known, act immediately.** Don't present setup options or explain OAuth. If it's Gmail, follow the Gmail section below. For any other provider, let the user know that only Gmail is fully supported right now, and offer to set up Gmail instead.
40
+
18
41
  ### Gmail
19
42
  1. **Try connecting directly first.** Call `credential_store` with `action: "oauth2_connect"` and `service: "gmail"`. The tool auto-fills Google's OAuth endpoints and looks up any previously stored client credentials — so this single call may be all that's needed.
20
43
  2. **If it fails because no client_id is found:** The user needs to create Google Cloud OAuth credentials first. Install and load the **google-oauth-setup** skill (which depends on **public-ingress** for the redirect URI):
@@ -70,12 +93,21 @@ If the user asks to verify their guardian identity for any channel (SMS, voice,
70
93
 
71
94
  The guardian-verify-setup skill handles the full outbound verification flow for all supported channels. It collects the user's destination (phone number or Telegram chat ID/handle), initiates an outbound verification session, and guides the user through entering or replying with the verification code. This is the single source of truth for guardian verification setup -- do not duplicate the verification flow inline.
72
95
 
96
+ ## Error Recovery
97
+
98
+ When a messaging tool fails with a token or authorization error:
99
+
100
+ 1. **Try to reconnect silently.** Call `credential_store` with `action: "oauth2_connect"` and the appropriate `service`. This often resolves expired tokens automatically.
101
+ 2. **If reconnection fails, go straight to setup.** Don't present options, ask which route the user prefers, or explain what went wrong technically. Just tell the user briefly (e.g., "Gmail needs to be reconnected — let me set that up") and immediately follow the connection setup flow for that platform (e.g., install and load **google-oauth-setup** for Gmail). The user came to you to get something done, not to troubleshoot OAuth — make it seamless.
102
+ 3. **Never try alternative approaches.** Don't use bash, curl, browser automation, or any workaround. If the messaging tools can't do it, the reconnection flow is the answer.
103
+ 4. **Never expose error details.** The user doesn't need to see error messages about tokens, OAuth, or API failures. Translate errors into plain language.
104
+
73
105
  ## Platform Selection
74
106
 
75
107
  - If the user specifies a platform (e.g., "check my Slack"), pass it as the `platform` parameter.
76
108
  - If only one platform is connected, it is auto-selected.
77
109
  - If multiple platforms are connected and the user doesn't specify, ask which platform they mean — or search across all of them.
78
- - **Do not assume a specific provider.** When the user says "email" or "manage my email" without naming a provider, call `messaging_auth_test` for each email-capable platform to discover what's connected don't default to Gmail or any other specific provider. Present whatever is connected; if nothing is, ask the user which email service they use and offer to set it up.
110
+ - **Be action-oriented with email.** When the user says "email" and wants to *do* something (declutter, check, search, send), check what's connected first. If nothing is connected, ask which provider briefly and then go straight into setup don't present menus, options lists, or explain the setup process. Just do it.
79
111
 
80
112
  ## Capabilities
81
113
 
@@ -217,27 +249,44 @@ Medium and high risk tools require a confidence score between 0 and 1:
217
249
 
218
250
  Use `messaging_analyze_activity` to classify channels or conversations by activity level (high, medium, low, dead). Useful for decluttering — suggest leaving dead channels or archiving old emails.
219
251
 
220
- ## Newsletter Decluttering
252
+ ## Email Decluttering
253
+
254
+ When a user asks to declutter, clean up, or organize their email — start scanning immediately. Don't ask what kind of cleanup they want or request permission to read their inbox. Go straight to scanning — but once results are ready, always show them via `ui_show` and let the user choose actions before archiving or unsubscribing.
255
+
256
+ ### Provider Selection
221
257
 
222
- Use `gmail_sender_digest` to help users identify and clean up high-volume senders like newsletters, marketing emails, and automated notifications.
258
+ - **Gmail connected**: Use the Gmail-specific tools (`gmail_sender_digest`, `gmail_archive_by_query`, `gmail_unsubscribe`, `gmail_filters`) they have richer features like unsubscribe support and filter creation.
259
+ - **Non-Gmail email connected**: Use the generic tools (`messaging_sender_digest`, `messaging_archive_by_sender`) — they work with any provider that supports these operations. Skip unsubscribe and filter offers since they are Gmail-specific.
260
+ - **Nothing connected**: Ask which email provider they use. If it's Gmail, go straight into the Gmail connection flow. For other providers, let the user know only Gmail is supported right now and offer to set up Gmail instead. Don't present a menu of options or explain what OAuth is.
223
261
 
224
262
  ### Workflow
225
263
 
226
- 1. **Scan**: Call `gmail_sender_digest` (default query targets Gmail's promotions category from the last 90 days)
264
+ 1. **Scan**: Call `gmail_sender_digest` (or `messaging_sender_digest` for non-Gmail). Default query targets promotions from the last 90 days.
227
265
  2. **Present**: Show results as a `ui_show` table with `selectionMode: "multiple"`:
228
- - Columns: Sender, Email Count, Unsubscribable, Date Range, Sample Subject
229
- - Action buttons: "Archive & Unsubscribe" (primary), "Archive Only" (secondary)
230
- 3. **Act on selection**: For each selected sender:
231
- - Prefer `gmail_archive_by_query` with the sender's `search_query` this archives all matching messages in one call, regardless of volume
232
- - Alternatively, use `gmail_batch_archive` with the sender's `message_ids` for smaller senders where all IDs are already collected
233
- - If the action is "Archive & Unsubscribe" and `has_unsubscribe` is true, call `gmail_unsubscribe` with the sender's `newest_message_id`
234
- 4. **Report**: Summarize results e.g. "Archived 247 messages from 8 senders. Unsubscribed from 6."
266
+ - **Gmail columns (exactly 3)**: Sender, Emails Found, Unsub?
267
+ - **Non-Gmail columns (exactly 2)**: Sender, Emails Found (omit the Unsub? column — unsubscribe is not available)
268
+ - **Pre-select all rows** (`selected: true`) users deselect what they want to keep
269
+ - **Caption**: "Showing emails from last 90 days in Promotions" (or adjusted to match the query used)
270
+ - **Gmail action buttons (exactly 2)**: "Archive & Unsubscribe" (primary), "Archive Only" (secondary). **NEVER offer Delete, Trash, or any destructive action.**
271
+ - **Non-Gmail action button (exactly 1)**: "Archive Selected" (primary). Do not offer an unsubscribe button — it is Gmail-specific. **NEVER offer Delete, Trash, or any destructive action.**
272
+ 3. **Live progress**: After the user clicks an action button:
273
+ - **Dismiss the table immediately** with `ui_dismiss` — it collapses to a completion chip
274
+ - **Show a `task_progress` card** with one step per selected sender (e.g., "Archiving TechCrunch (247 emails)"). Update each step from `in_progress` → `completed` as each sender finishes.
275
+ - When all senders are processed, set the progress card's `status: "completed"`.
276
+ 4. **Act on selection**: For each selected sender:
277
+ - Use `gmail_archive_by_query` (or `messaging_archive_by_sender` for non-Gmail) with the sender's `search_query` — this archives all matching messages in one call, regardless of volume
278
+ - If Gmail and the action is "Archive & Unsubscribe" and `has_unsubscribe` is true, call `gmail_unsubscribe` with the sender's `newest_message_id`
279
+ 5. **Accurate summary**: Use the **actual counts returned by the archive tool**, not the scan counts from the digest. The scan is a sample; the archive is comprehensive. Format: "Cleaned up [total_archived] emails from [sender_count] senders." For Gmail, append: "Unsubscribed from [unsub_count]."
280
+ 6. **Ongoing protection offer (Gmail only)**: After reporting results, offer auto-archive filters:
281
+ - "Want me to set up auto-archive filters so future emails from these senders skip your inbox?"
282
+ - If yes, call `gmail_filters` with `action: "create"` for each sender with `from` set to the sender's email and `remove_label_ids: ["INBOX"]`.
283
+ - Then offer a recurring declutter schedule: "Want me to scan for new clutter monthly?" If yes, use `schedule_create` to set up a monthly declutter check.
235
284
 
236
285
  ### Edge Cases
237
286
 
238
287
  - **Zero results**: Tell the user "No newsletter emails found" and suggest broadening the query (e.g. removing the category filter or extending the date range)
239
288
  - **Unsubscribe failures**: Report per-sender success/failure; the existing `gmail_unsubscribe` tool handles edge cases
240
- - **Large sender counts**: The `has_more` flag indicates a sender had more messages than collected — use `search_query` for follow-up archiving
289
+ - **Large sender counts**: The `has_more` flag indicates a sender had more messages than collected — `gmail_archive_by_query` handles this automatically via its own pagination
241
290
 
242
291
  ## Batch Operations
243
292
 
@@ -1017,6 +1017,64 @@
1017
1017
  "executor": "tools/gmail-sender-digest.ts",
1018
1018
  "execution_target": "host"
1019
1019
  },
1020
+ {
1021
+ "name": "messaging_sender_digest",
1022
+ "description": "Scan connected email platform and group messages by sender to identify high-volume senders (e.g. newsletters). Works with any email provider that supports sender digest. Returns top senders sorted by message count with metadata for bulk cleanup.",
1023
+ "category": "messaging",
1024
+ "risk": "low",
1025
+ "input_schema": {
1026
+ "type": "object",
1027
+ "properties": {
1028
+ "platform": {
1029
+ "type": "string",
1030
+ "description": "Platform (e.g. \"gmail\"). Auto-detected if only one is connected."
1031
+ },
1032
+ "query": {
1033
+ "type": "string",
1034
+ "description": "Search query (default 'category:promotions newer_than:90d')"
1035
+ },
1036
+ "max_messages": {
1037
+ "type": "number",
1038
+ "description": "Maximum messages to scan (default 500, cap 2000)"
1039
+ },
1040
+ "max_senders": {
1041
+ "type": "number",
1042
+ "description": "Maximum senders to return (default 30)"
1043
+ }
1044
+ }
1045
+ },
1046
+ "executor": "tools/messaging-sender-digest.ts",
1047
+ "execution_target": "host"
1048
+ },
1049
+ {
1050
+ "name": "messaging_archive_by_sender",
1051
+ "description": "Archive all messages matching a search query on the connected email platform. Paginates through all results and archives in bulk. Works with any email provider that supports archive by query. Include a confidence score (0-1).",
1052
+ "category": "messaging",
1053
+ "risk": "medium",
1054
+ "input_schema": {
1055
+ "type": "object",
1056
+ "properties": {
1057
+ "platform": {
1058
+ "type": "string",
1059
+ "description": "Platform (e.g. \"gmail\"). Auto-detected if only one is connected."
1060
+ },
1061
+ "query": {
1062
+ "type": "string",
1063
+ "description": "Search query (e.g. \"from:marketing@example.com category:promotions newer_than:90d\")"
1064
+ },
1065
+ "confidence": {
1066
+ "type": "number",
1067
+ "description": "Confidence score (0-1) for this action"
1068
+ }
1069
+ },
1070
+ "required": [
1071
+ "query",
1072
+ "confidence"
1073
+ ]
1074
+ },
1075
+ "executor": "tools/messaging-archive-by-sender.ts",
1076
+ "execution_target": "host"
1077
+ },
1020
1078
  {
1021
1079
  "name": "gmail_outreach_scan",
1022
1080
  "description": "Scan Gmail for cold outreach emails (sales, recruiting, marketing) using LLM classification. Returns top outreach senders with suggested cleanup actions. Read-only \u2014 use gmail_batch_archive and gmail_filters for cleanup.",
@@ -161,7 +161,12 @@ export async function run(input: Record<string, unknown>, _context: ToolContext)
161
161
  sample_subjects: s.sampleSubjects,
162
162
  }));
163
163
 
164
- return ok(JSON.stringify({ senders: result, total_scanned: allMessageIds.length }));
164
+ return ok(JSON.stringify({
165
+ senders: result,
166
+ total_scanned: allMessageIds.length,
167
+ query_used: query,
168
+ note: `message_count reflects emails found per sender within the ${allMessageIds.length} messages scanned. The archive tool may find additional messages beyond this sample.`,
169
+ }));
165
170
  });
166
171
  } catch (e) {
167
172
  return err(e instanceof Error ? e.message : String(e));
@@ -0,0 +1,35 @@
1
+ import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
2
+ import { err, ok, resolveProvider, withProviderToken } from './shared.js';
3
+
4
+ export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
5
+ const platform = input.platform as string | undefined;
6
+ const query = input.query as string;
7
+
8
+ if (!query) {
9
+ return err('query is required.');
10
+ }
11
+
12
+ try {
13
+ const provider = resolveProvider(platform);
14
+
15
+ if (!provider.archiveByQuery) {
16
+ return err(`The ${provider.displayName} provider does not support archive by query.`);
17
+ }
18
+
19
+ return withProviderToken(provider, async (token) => {
20
+ const result = await provider.archiveByQuery!(token, query);
21
+
22
+ if (result.archived === 0) {
23
+ return ok('No messages matched the query. Nothing archived.');
24
+ }
25
+
26
+ const summary = `Archived ${result.archived} message(s) matching query: ${query}`;
27
+ if (result.truncated) {
28
+ return ok(`${summary}\n\nNote: this operation was capped at 5000 messages. Additional messages matching the query may remain in the inbox. Run the command again to archive more.`);
29
+ }
30
+ return ok(summary);
31
+ });
32
+ } catch (e) {
33
+ return err(e instanceof Error ? e.message : String(e));
34
+ }
35
+ }
@@ -0,0 +1,52 @@
1
+ import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
2
+ import { err, ok, resolveProvider, withProviderToken } from './shared.js';
3
+
4
+ export async function run(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
5
+ const platform = input.platform as string | undefined;
6
+ const query = (input.query as string) ?? 'category:promotions newer_than:90d';
7
+ const maxMessages = input.max_messages as number | undefined;
8
+ const maxSenders = input.max_senders as number | undefined;
9
+
10
+ try {
11
+ const provider = resolveProvider(platform);
12
+
13
+ if (!provider.senderDigest) {
14
+ return err(`The ${provider.displayName} provider does not support sender digest scanning.`);
15
+ }
16
+
17
+ return withProviderToken(provider, async (token) => {
18
+ const result = await provider.senderDigest!(token, query, { maxMessages, maxSenders });
19
+
20
+ if (result.senders.length === 0) {
21
+ return ok(JSON.stringify({
22
+ senders: [],
23
+ total_scanned: result.totalScanned,
24
+ query_used: result.queryUsed,
25
+ message: 'No emails found matching the query. Try broadening the search (e.g. remove category filter or extend date range).',
26
+ }));
27
+ }
28
+
29
+ // Map to snake_case output format for LLM consumption
30
+ const senders = result.senders.map((s) => ({
31
+ id: s.id,
32
+ display_name: s.displayName,
33
+ email: s.email,
34
+ message_count: s.messageCount,
35
+ has_unsubscribe: s.hasUnsubscribe,
36
+ newest_message_id: s.newestMessageId,
37
+ search_query: s.searchQuery,
38
+ message_ids: s.messageIds,
39
+ has_more: s.hasMore,
40
+ }));
41
+
42
+ return ok(JSON.stringify({
43
+ senders,
44
+ total_scanned: result.totalScanned,
45
+ query_used: result.queryUsed,
46
+ note: `message_count reflects emails found per sender within the ${result.totalScanned} messages scanned. The archive tool may find additional messages beyond this sample.`,
47
+ }));
48
+ });
49
+ } catch (e) {
50
+ return err(e instanceof Error ? e.message : String(e));
51
+ }
52
+ }
@@ -17,7 +17,15 @@ Twilio credentials and phone number configuration are shared between voice calls
17
17
 
18
18
  The twilio-setup skill handles credential storage, phone number provisioning/assignment, and public ingress setup. Once complete, return here to enable the calls feature and start making calls.
19
19
 
20
- If Twilio is already configured (check `twilio_config` with `action: "get"`), skip directly to **Step 5: Enable Calls** below.
20
+ Check if Twilio is already configured:
21
+
22
+ ```bash
23
+ TOKEN=$(cat ~/.vellum/http-token)
24
+ curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
25
+ -H "Authorization: Bearer $TOKEN"
26
+ ```
27
+
28
+ If `hasCredentials` is `true` and `phoneNumber` is set, skip directly to **Step 5: Enable Calls** below.
21
29
 
22
30
  ## Overview
23
31
 
@@ -58,65 +66,48 @@ The user's assistant gets its own personal phone number through Twilio. All impl
58
66
  First, check whether Twilio is already configured:
59
67
 
60
68
  ```bash
61
- vellum config get calls.enabled
69
+ TOKEN=$(cat ~/.vellum/http-token)
70
+ curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
71
+ -H "Authorization: Bearer $TOKEN"
62
72
  ```
63
73
 
64
- Also check for existing credentials:
74
+ Also check calls feature status:
65
75
 
66
76
  ```bash
67
- credential_store action=list
77
+ vellum config get calls.enabled
68
78
  ```
69
79
 
70
- Look for entries with service `twilio` and fields `account_sid`, `auth_token`, and `phone_number`.
71
-
72
- If all three credentials exist and `calls.enabled` is `true`, skip to the **Making Calls** section. If credentials are partially configured, skip to whichever step is still needed.
80
+ If the config response shows `hasCredentials: true` and `phoneNumber` is set, and `calls.enabled` is `true`, skip to the **Making Calls** section. If credentials are partially configured, skip to whichever step is still needed.
73
81
 
74
82
  ## Step 2: Create a Twilio Account
75
83
 
76
84
  If the user doesn't have a Twilio account yet, guide them through setup:
77
85
 
78
86
  1. Tell the user: **"You'll need a Twilio account to make phone calls. Sign up at https://www.twilio.com/try-twilio — it's free to start and includes trial credit."**
79
- 2. Once they have an account, they need three pieces of information:
87
+ 2. Once they have an account, they need two pieces of information:
80
88
  - **Account SID** — found on the Twilio Console dashboard at https://console.twilio.com
81
89
  - **Auth Token** — found on the same dashboard (click "Show" to reveal it)
82
- - **Phone Number** — a Twilio phone number capable of making voice calls
83
-
84
- ### Getting a Twilio Phone Number
85
-
86
- If the user doesn't have a Twilio phone number yet:
87
-
88
- 1. Direct them to https://console.twilio.com/us1/develop/phone-numbers/manage/incoming
89
- 2. Click **"Buy a Number"**
90
- 3. Select a number with **Voice** capability enabled
91
- 4. For trial accounts, Twilio provides one free number automatically — check "Active Numbers" first
92
90
 
93
- Tell the user: **"This will be your assistant's personal phone number — the number that shows up on caller ID when calls are placed."**
91
+ Tell the user: **"The assistant will get its own personal phone number through Twilio — the number that shows up on caller ID when calls are placed."**
94
92
 
95
93
  ## Step 3: Store Twilio Credentials
96
94
 
97
- Once the user provides their credentials, store them securely using the `credential_store` tool. Ask the user to paste each value, then store them one at a time:
95
+ **IMPORTANT Secure credential collection only:** Never use credentials pasted in plaintext chat. Always collect credentials through the secure credential prompt flow:
98
96
 
99
- **Account SID:**
100
- ```
101
- credential_store action=store service=twilio field=account_sid value=<their_account_sid>
102
- ```
97
+ - Call `credential_store` with `action: "prompt"`, `service: "twilio"`, `field: "account_sid"`, `label: "Twilio Account SID"`, `description: "Enter your Account SID from the Twilio Console dashboard"`, and `placeholder: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"`.
98
+ - Call `credential_store` with `action: "prompt"`, `service: "twilio"`, `field: "auth_token"`, `label: "Twilio Auth Token"`, `description: "Enter your Auth Token from the Twilio Console dashboard"`, and `placeholder: "your_auth_token"`.
103
99
 
104
- **Auth Token:**
105
- ```
106
- credential_store action=store service=twilio field=auth_token value=<their_auth_token>
107
- ```
100
+ After both credentials are collected, send them to the gateway:
108
101
 
109
- **Phone Number** (must be in E.164 format, e.g. `+14155551234`):
110
- ```
111
- credential_store action=store service=twilio field=phone_number value=<their_phone_number>
112
- ```
113
-
114
- After storing, verify each credential was saved:
115
- ```
116
- credential_store action=list
102
+ ```bash
103
+ TOKEN=$(cat ~/.vellum/http-token)
104
+ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/credentials" \
105
+ -H "Authorization: Bearer $TOKEN" \
106
+ -H "Content-Type: application/json" \
107
+ -d '{"accountSid":"<value from credential_store for twilio/account_sid>","authToken":"<value from credential_store for twilio/auth_token>"}'
117
108
  ```
118
109
 
119
- Confirm that entries for service `twilio` with fields `account_sid`, `auth_token`, and `phone_number` appear in the output.
110
+ The endpoint validates the credentials against the Twilio API before storing them. If credentials are invalid, ask the user to re-enter via the secure prompt.
120
111
 
121
112
  **Important:** Credentials are stored in the OS keychain (macOS Keychain / Linux secret-service) or encrypted at rest. They are never logged or exposed in plaintext.
122
113
 
@@ -163,7 +154,7 @@ vellum config get calls.enabled
163
154
 
164
155
  Before making real calls, offer a quick verification:
165
156
 
166
- 1. Confirm credentials are stored: all three Twilio credentials (`account_sid`, `auth_token`, `phone_number`) must be present
157
+ 1. Confirm credentials are stored: check the Twilio config endpoint for `hasCredentials: true` and `phoneNumber`
167
158
  2. Confirm ingress is running: `ingress.publicBaseUrl` must be set and the tunnel active
168
159
  3. Confirm calls are enabled: `calls.enabled` must be `true`
169
160
 
@@ -605,7 +596,7 @@ The following behavioral changes were introduced with the cross-channel guardian
605
596
  ## Troubleshooting
606
597
 
607
598
  ### "Twilio credentials not configured"
608
- Run Step 3 to store your Account SID, Auth Token, and Phone Number via `credential_store`.
599
+ Run Step 3 to store your Account SID and Auth Token via the secure credential prompt flow, or load the `twilio-setup` skill.
609
600
 
610
601
  ### "Calls feature is disabled"
611
602
  Run `vellum config set calls.enabled true`.
@@ -17,7 +17,7 @@ OAuth uses the official X API v2. It is the most reliable connection method and
17
17
 
18
18
  - Supports: **post** and **reply**
19
19
  - Read-only operations (timeline, search, home, bookmarks, notifications, likes, followers, following, media) always use the browser path directly, regardless of the strategy setting.
20
- - Setup: Collect the OAuth Client ID (and optional Client Secret) from the user in chat using `credential_store` with `action: "prompt"` (canonical field names: `client_id`, `client_secret`), then initiate the `twitter_auth_start` IPC flow. See the **First-Use Decision Flow** for the full sequence.
20
+ - Setup: Collect the OAuth Client ID (and optional Client Secret) from the user in chat using `credential_store` with `action: "prompt"` (canonical field names: `client_id`, `client_secret`), then initiate the `twitter_auth_start` flow. See the **First-Use Decision Flow** for the full sequence.
21
21
  - Set the strategy: `vellum x strategy set oauth`
22
22
 
23
23
  ### Browser session (no developer credentials needed)
@@ -56,7 +56,7 @@ When the user triggers a Twitter operation and no strategy has been configured y
56
56
 
57
57
  ### OAuth Setup Sequence
58
58
 
59
- When the user chooses OAuth, collect their X developer credentials conversationally using the secure UI. The OAuth flow delegates to the generic connect orchestrator via the `twitter_auth_start` IPC message (which internally uses the shared orchestrator while preserving Twitter-specific guards like integration-mode checks and refresh-token cleanup).
59
+ When the user chooses OAuth, collect their X developer credentials conversationally using the secure UI. The OAuth flow delegates to the generic connect orchestrator, which resolves the Twitter provider profile, computes scopes via policy, opens the X authorization page in the user's browser, verifies the user's identity, and stores tokens. The orchestrator also manages stale refresh-token cleanup and enforces integration-mode guards.
60
60
 
61
61
  1. **Collect the Client ID securely:**
62
62
  Call `credential_store` with `action: "prompt"`, `service: "integration:twitter"`, `field: "client_id"`, `label: "X (Twitter) OAuth Client ID"`, `description: "Enter the Client ID from your X Developer App"`, and `placeholder: "your-client-id"`.
@@ -65,7 +65,7 @@ When the user chooses OAuth, collect their X developer credentials conversationa
65
65
  Ask the user if their X app uses a confidential client (has a Client Secret). If yes, call `credential_store` with `action: "prompt"`, `service: "integration:twitter"`, `field: "client_secret"`, `label: "X (Twitter) OAuth Client Secret"`, `description: "Enter the Client Secret from your X Developer App (leave blank if using a public client)"`, and `placeholder: "your-client-secret"`.
66
66
 
67
67
  3. **Initiate the OAuth flow:**
68
- Send the `twitter_auth_start` IPC message. The handler delegates to the shared connect orchestrator, which resolves the Twitter provider profile, computes scopes via policy, opens the X authorization page in the user's browser, verifies the user's identity, and stores tokens. The handler also manages stale refresh-token cleanup and enforces integration-mode guards. Wait for the `twitter_auth_result` response.
68
+ Trigger the Twitter auth start flow. The connect orchestrator resolves the Twitter provider profile, computes scopes via policy, opens the X authorization page in the user's browser, verifies the user's identity, and stores tokens. Wait for the auth result.
69
69
 
70
70
  4. **Confirm success:**
71
71
  Tell the user: "Great, your X account is connected! You can always update these credentials from the Settings page."
@@ -2,6 +2,7 @@
2
2
  name: "Vercel Token Setup"
3
3
  description: "Set up a Vercel API token for publishing apps using browser automation"
4
4
  includes: ["browser"]
5
+ credential-setup-for: "vercel:api_token"
5
6
  metadata: {"vellum": {"emoji": "▲"}}
6
7
  ---
7
8
 
@@ -137,6 +137,30 @@ export const CallsConfigSchema = z.object({
137
137
  .min(50, 'calls.accessRequestPollIntervalMs must be >= 50')
138
138
  .max(10_000, 'calls.accessRequestPollIntervalMs must be at most 10000')
139
139
  .default(500),
140
+ guardianWaitUpdateInitialIntervalMs: z
141
+ .number({ error: 'calls.guardianWaitUpdateInitialIntervalMs must be a number' })
142
+ .int('calls.guardianWaitUpdateInitialIntervalMs must be an integer')
143
+ .min(1000, 'calls.guardianWaitUpdateInitialIntervalMs must be >= 1000')
144
+ .max(60_000, 'calls.guardianWaitUpdateInitialIntervalMs must be at most 60000')
145
+ .default(5000),
146
+ guardianWaitUpdateInitialWindowMs: z
147
+ .number({ error: 'calls.guardianWaitUpdateInitialWindowMs must be a number' })
148
+ .int('calls.guardianWaitUpdateInitialWindowMs must be an integer')
149
+ .min(1000, 'calls.guardianWaitUpdateInitialWindowMs must be >= 1000')
150
+ .max(60_000, 'calls.guardianWaitUpdateInitialWindowMs must be at most 60000')
151
+ .default(30_000),
152
+ guardianWaitUpdateSteadyMinIntervalMs: z
153
+ .number({ error: 'calls.guardianWaitUpdateSteadyMinIntervalMs must be a number' })
154
+ .int('calls.guardianWaitUpdateSteadyMinIntervalMs must be an integer')
155
+ .min(1000, 'calls.guardianWaitUpdateSteadyMinIntervalMs must be >= 1000')
156
+ .max(60_000, 'calls.guardianWaitUpdateSteadyMinIntervalMs must be at most 60000')
157
+ .default(7000),
158
+ guardianWaitUpdateSteadyMaxIntervalMs: z
159
+ .number({ error: 'calls.guardianWaitUpdateSteadyMaxIntervalMs must be a number' })
160
+ .int('calls.guardianWaitUpdateSteadyMaxIntervalMs must be an integer')
161
+ .min(1000, 'calls.guardianWaitUpdateSteadyMaxIntervalMs must be >= 1000')
162
+ .max(60_000, 'calls.guardianWaitUpdateSteadyMaxIntervalMs must be at most 60000')
163
+ .default(10_000),
140
164
  disclosure: CallsDisclosureConfigSchema.default(CallsDisclosureConfigSchema.parse({})),
141
165
  safety: CallsSafetyConfigSchema.default(CallsSafetyConfigSchema.parse({})),
142
166
  voice: CallsVoiceConfigSchema.default(CallsVoiceConfigSchema.parse({})),
package/src/config/env.ts CHANGED
@@ -162,6 +162,28 @@ export function getOllamaBaseUrlEnv(): string | undefined {
162
162
  return str('OLLAMA_BASE_URL');
163
163
  }
164
164
 
165
+ // ── Platform ─────────────────────────────────────────────────────────────────
166
+
167
+ export function getPlatformBaseUrl(): string {
168
+ return str('PLATFORM_BASE_URL') ?? '';
169
+ }
170
+
171
+ /**
172
+ * PLATFORM_ASSISTANT_ID — UUID of this assistant on the platform.
173
+ * Required for registering callback routes when containerized.
174
+ */
175
+ export function getPlatformAssistantId(): string {
176
+ return str('PLATFORM_ASSISTANT_ID') ?? '';
177
+ }
178
+
179
+ /**
180
+ * PLATFORM_INTERNAL_API_KEY — static internal gateway key for authenticating
181
+ * with the platform's internal gateway callback route registration endpoint.
182
+ */
183
+ export function getPlatformInternalApiKey(): string {
184
+ return str('PLATFORM_INTERNAL_API_KEY') ?? '';
185
+ }
186
+
165
187
  // ── Startup validation ──────────────────────────────────────────────────────
166
188
 
167
189
  /**
@@ -41,6 +41,14 @@
41
41
  "description": "Enable X (Twitter) skill section in the system prompt",
42
42
  "defaultEnabled": true
43
43
  },
44
+ {
45
+ "id": "messaging",
46
+ "scope": "assistant",
47
+ "key": "feature_flags.messaging.enabled",
48
+ "label": "Messaging",
49
+ "description": "Enable messaging skill section in the system prompt",
50
+ "defaultEnabled": true
51
+ },
44
52
  {
45
53
  "id": "collect-usage-data",
46
54
  "scope": "assistant",
@@ -204,8 +204,8 @@ export const AssistantConfigSchema = z.object({
204
204
  maxToolUseTurns: z
205
205
  .number({ error: 'maxToolUseTurns must be a number' })
206
206
  .int('maxToolUseTurns must be an integer')
207
- .positive('maxToolUseTurns must be a positive integer')
208
- .default(60),
207
+ .nonnegative('maxToolUseTurns must be a non-negative integer')
208
+ .default(0),
209
209
  thinking: ThinkingConfigSchema.default(ThinkingConfigSchema.parse({})),
210
210
  contextWindow: ContextWindowConfigSchema.default(ContextWindowConfigSchema.parse({})),
211
211
  memory: MemoryConfigSchema.default(MemoryConfigSchema.parse({})),
@@ -65,6 +65,8 @@ export interface SkillSummary {
65
65
  toolManifest?: SkillToolManifestMeta;
66
66
  /** IDs of child skills that this skill includes (metadata-only, not auto-activated). */
67
67
  includes?: string[];
68
+ /** Declares which credential this skill sets up (e.g. "vercel:api_token"). */
69
+ credentialSetupFor?: string;
68
70
  }
69
71
 
70
72
  export interface SkillDefinition extends SkillSummary {
@@ -251,6 +253,7 @@ interface ParsedFrontmatter {
251
253
  disableModelInvocation: boolean;
252
254
  metadata?: VellumMetadata;
253
255
  includes?: string[];
256
+ credentialSetupFor?: string;
254
257
  }
255
258
 
256
259
  function parseIncludes(raw: string | undefined, skillFilePath: string): string[] | undefined {
@@ -338,6 +341,7 @@ function parseFrontmatter(content: string, skillFilePath: string): ParsedFrontma
338
341
  }
339
342
 
340
343
  const includes = parseIncludes(fields.includes, skillFilePath);
344
+ const credentialSetupFor = fields['credential-setup-for']?.trim() || undefined;
341
345
 
342
346
  return {
343
347
  name,
@@ -348,6 +352,7 @@ function parseFrontmatter(content: string, skillFilePath: string): ParsedFrontma
348
352
  disableModelInvocation,
349
353
  metadata,
350
354
  includes,
355
+ credentialSetupFor,
351
356
  };
352
357
  }
353
358
 
@@ -471,6 +476,7 @@ function readSkillFromDirectory(directoryPath: string, skillsDir: string, source
471
476
  metadata: parsed.metadata,
472
477
  toolManifest: detectToolManifest(directoryPath),
473
478
  includes: parsed.includes,
479
+ credentialSetupFor: parsed.credentialSetupFor,
474
480
  };
475
481
  } catch (err) {
476
482
  log.warn({ err, skillFilePath }, 'Failed to read skill file');
@@ -512,6 +518,7 @@ function readBundledSkillFromDirectory(directoryPath: string): SkillDefinition |
512
518
  metadata: parsed.metadata,
513
519
  toolManifest: detectToolManifest(directoryPath),
514
520
  includes: parsed.includes,
521
+ credentialSetupFor: parsed.credentialSetupFor,
515
522
  };
516
523
  } catch (err) {
517
524
  log.warn({ err, skillFilePath }, 'Failed to read bundled skill file');
@@ -566,6 +573,7 @@ function loadBundledSkills(): SkillSummary[] {
566
573
  metadata: skill.metadata,
567
574
  toolManifest: skill.toolManifest,
568
575
  includes: skill.includes,
576
+ credentialSetupFor: skill.credentialSetupFor,
569
577
  });
570
578
  }
571
579
 
@@ -686,6 +694,7 @@ function skillSummaryFromDefinition(skill: SkillDefinition, source: SkillSource)
686
694
  metadata: skill.metadata,
687
695
  toolManifest: skill.toolManifest,
688
696
  includes: skill.includes,
697
+ credentialSetupFor: skill.credentialSetupFor,
689
698
  };
690
699
  }
691
700
 
@@ -731,6 +740,7 @@ export function loadSkillCatalog(workspaceSkillsDir?: string, extraDirs?: string
731
740
  metadata: parsed.metadata,
732
741
  toolManifest: detectToolManifest(directory),
733
742
  includes: parsed.includes,
743
+ credentialSetupFor: parsed.credentialSetupFor,
734
744
  });
735
745
  } catch (err) {
736
746
  log.warn({ err, directory }, 'Failed to read skill from extraDirs');
@@ -813,6 +823,7 @@ export function loadSkillCatalog(workspaceSkillsDir?: string, extraDirs?: string
813
823
  metadata: parsed.metadata,
814
824
  toolManifest: detectToolManifest(directory),
815
825
  includes: parsed.includes,
826
+ credentialSetupFor: parsed.credentialSetupFor,
816
827
  };
817
828
 
818
829
  if (seenIds.has(id)) {