@vellumai/assistant 0.4.46 → 0.4.49

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 (382) hide show
  1. package/ARCHITECTURE.md +7 -7
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/security.md +5 -5
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/anthropic-provider.test.ts +156 -0
  12. package/src/__tests__/approval-cascade.test.ts +810 -0
  13. package/src/__tests__/approval-primitive.test.ts +0 -1
  14. package/src/__tests__/approval-routes-http.test.ts +2 -0
  15. package/src/__tests__/assistant-attachments.test.ts +12 -34
  16. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  17. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  18. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  19. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  20. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  21. package/src/__tests__/channel-guardian.test.ts +0 -2
  22. package/src/__tests__/channel-readiness-routes.test.ts +35 -25
  23. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  24. package/src/__tests__/checker.test.ts +9 -29
  25. package/src/__tests__/cli.test.ts +23 -0
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-watcher.test.ts +0 -1
  29. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  30. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  31. package/src/__tests__/context-token-estimator.test.ts +196 -13
  32. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  33. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  34. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  36. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  37. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  38. package/src/__tests__/credential-broker.test.ts +2 -1
  39. package/src/__tests__/credential-metadata-store.test.ts +239 -26
  40. package/src/__tests__/credential-resolve.test.ts +5 -4
  41. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  42. package/src/__tests__/credential-security-invariants.test.ts +111 -7
  43. package/src/__tests__/credential-vault-unit.test.ts +287 -54
  44. package/src/__tests__/credential-vault.test.ts +406 -12
  45. package/src/__tests__/credentials-cli.test.ts +82 -6
  46. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  47. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  48. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/gemini-image-service.test.ts +75 -45
  51. package/src/__tests__/gemini-provider.test.ts +9 -6
  52. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  53. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  54. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  55. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  56. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  57. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  58. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  59. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  60. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  61. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  62. package/src/__tests__/heartbeat-service.test.ts +0 -1
  63. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  64. package/src/__tests__/host-shell-tool.test.ts +27 -15
  65. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  66. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  67. package/src/__tests__/integration-status.test.ts +38 -25
  68. package/src/__tests__/intent-routing.test.ts +0 -1
  69. package/src/__tests__/invite-routes-http.test.ts +10 -9
  70. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  71. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  72. package/src/__tests__/media-generate-image.test.ts +63 -2
  73. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  74. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +373 -14
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +756 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  83. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  84. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  85. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  86. package/src/__tests__/recording-handler.test.ts +3 -4
  87. package/src/__tests__/registry.test.ts +2 -2
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  91. package/src/__tests__/schema-transforms.test.ts +226 -0
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  95. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  96. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  97. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  98. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  99. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  100. package/src/__tests__/sequence-store.test.ts +0 -1
  101. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  102. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  103. package/src/__tests__/skill-include-graph.test.ts +66 -0
  104. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  105. package/src/__tests__/skill-load-tool.test.ts +149 -1
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  107. package/src/__tests__/skills-uninstall.test.ts +3 -3
  108. package/src/__tests__/skills.test.ts +3 -12
  109. package/src/__tests__/slack-channel-config.test.ts +76 -11
  110. package/src/__tests__/slack-share-routes.test.ts +17 -14
  111. package/src/__tests__/system-prompt.test.ts +0 -1
  112. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  113. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  114. package/src/__tests__/terminal-tools.test.ts +4 -3
  115. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  116. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  117. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  118. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  119. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  120. package/src/__tests__/tool-executor.test.ts +0 -1
  121. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  122. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  123. package/src/__tests__/trust-store.test.ts +1 -22
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  126. package/src/__tests__/twilio-config.test.ts +2 -1
  127. package/src/__tests__/twilio-provider.test.ts +4 -2
  128. package/src/__tests__/twilio-routes.test.ts +5 -20
  129. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  130. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  131. package/src/agent/ax-tree-compaction.test.ts +235 -0
  132. package/src/agent/loop.ts +76 -130
  133. package/src/calls/call-domain.ts +8 -10
  134. package/src/calls/relay-server.ts +9 -13
  135. package/src/calls/twilio-config.ts +4 -8
  136. package/src/calls/twilio-provider.ts +2 -1
  137. package/src/calls/twilio-rest.ts +2 -1
  138. package/src/calls/twilio-routes.ts +1 -2
  139. package/src/calls/voice-ingress-preflight.ts +1 -1
  140. package/src/cli/commands/browser-relay.ts +46 -15
  141. package/src/cli/commands/completions.ts +0 -3
  142. package/src/cli/commands/credentials.ts +110 -23
  143. package/src/cli/commands/oauth/apps.ts +255 -0
  144. package/src/cli/commands/oauth/connections.ts +299 -0
  145. package/src/cli/commands/oauth/index.ts +52 -0
  146. package/src/cli/commands/oauth/providers.ts +242 -0
  147. package/src/cli/commands/skills.ts +4 -338
  148. package/src/cli/program.ts +1 -5
  149. package/src/cli/reference.ts +1 -3
  150. package/src/cli.ts +3 -2
  151. package/src/config/assistant-feature-flags.ts +0 -3
  152. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  153. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  154. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  155. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  156. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  157. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  158. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  159. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  160. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  161. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  162. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  163. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  164. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  165. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  166. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  167. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  168. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  169. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  170. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  171. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  172. package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
  173. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  174. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  175. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  176. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  177. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  178. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  179. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  180. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  181. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  182. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  183. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  184. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  185. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  186. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  187. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  188. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  189. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  190. package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
  191. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  192. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  193. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  194. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  195. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  196. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  197. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  198. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  199. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  200. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  201. package/src/config/env-registry.ts +14 -83
  202. package/src/config/env.ts +11 -50
  203. package/src/config/feature-flag-registry.json +16 -16
  204. package/src/config/schema.ts +3 -1
  205. package/src/config/skills.ts +21 -2
  206. package/src/context/image-dimensions.ts +229 -0
  207. package/src/context/token-estimator.ts +75 -12
  208. package/src/context/window-manager.ts +49 -10
  209. package/src/daemon/assistant-attachments.ts +1 -13
  210. package/src/daemon/guardian-action-generators.ts +4 -5
  211. package/src/daemon/handlers/config-ingress.ts +8 -33
  212. package/src/daemon/handlers/config-slack-channel.ts +76 -56
  213. package/src/daemon/handlers/config-telegram.ts +53 -24
  214. package/src/daemon/handlers/sessions.ts +10 -24
  215. package/src/daemon/handlers/shared.ts +0 -130
  216. package/src/daemon/host-cu-proxy.ts +401 -0
  217. package/src/daemon/lifecycle.ts +39 -63
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/computer-use.ts +2 -119
  220. package/src/daemon/message-types/host-cu.ts +19 -0
  221. package/src/daemon/message-types/integrations.ts +1 -0
  222. package/src/daemon/message-types/messages.ts +3 -0
  223. package/src/daemon/server.ts +14 -21
  224. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  225. package/src/daemon/session-attachments.ts +1 -2
  226. package/src/daemon/session-messaging.ts +3 -1
  227. package/src/daemon/session-slash.ts +1 -1
  228. package/src/daemon/session-surfaces.ts +40 -28
  229. package/src/daemon/session-tool-setup.ts +20 -11
  230. package/src/daemon/session.ts +139 -16
  231. package/src/daemon/tool-side-effects.ts +2 -8
  232. package/src/daemon/watch-handler.ts +2 -2
  233. package/src/email/providers/index.ts +2 -1
  234. package/src/events/tool-metrics-listener.ts +2 -2
  235. package/src/hooks/manager.ts +1 -4
  236. package/src/inbound/public-ingress-urls.ts +7 -7
  237. package/src/instrument.ts +15 -1
  238. package/src/logfire.ts +16 -5
  239. package/src/media/app-icon-generator.ts +30 -4
  240. package/src/media/avatar-router.ts +26 -3
  241. package/src/media/gemini-image-service.ts +28 -2
  242. package/src/memory/conversation-key-store.ts +21 -0
  243. package/src/memory/db-init.ts +4 -0
  244. package/src/memory/guardian-action-store.ts +1 -1
  245. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  246. package/src/memory/migrations/index.ts +1 -0
  247. package/src/memory/schema/guardian.ts +1 -1
  248. package/src/memory/schema/index.ts +1 -0
  249. package/src/memory/schema/oauth.ts +65 -0
  250. package/src/messaging/provider.ts +19 -13
  251. package/src/messaging/providers/gmail/adapter.ts +40 -23
  252. package/src/messaging/providers/gmail/client.ts +283 -122
  253. package/src/messaging/providers/gmail/people-client.ts +32 -24
  254. package/src/messaging/providers/slack/adapter.ts +29 -19
  255. package/src/messaging/providers/slack/client.ts +265 -78
  256. package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
  257. package/src/messaging/providers/whatsapp/adapter.ts +17 -11
  258. package/src/messaging/registry.ts +2 -31
  259. package/src/notifications/copy-composer.ts +0 -5
  260. package/src/notifications/signal.ts +4 -5
  261. package/src/oauth/byo-connection.test.ts +537 -0
  262. package/src/oauth/byo-connection.ts +128 -0
  263. package/src/oauth/connect-orchestrator.ts +139 -56
  264. package/src/oauth/connect-types.ts +17 -23
  265. package/src/oauth/connection-resolver.ts +58 -0
  266. package/src/oauth/connection.ts +38 -0
  267. package/src/oauth/manual-token-connection.ts +104 -0
  268. package/src/oauth/oauth-store.ts +496 -0
  269. package/src/oauth/platform-connection.test.ts +192 -0
  270. package/src/oauth/platform-connection.ts +111 -0
  271. package/src/oauth/provider-behaviors.ts +124 -0
  272. package/src/oauth/scope-policy.ts +9 -2
  273. package/src/oauth/seed-providers.ts +161 -0
  274. package/src/oauth/token-persistence.ts +74 -78
  275. package/src/permissions/checker.ts +8 -4
  276. package/src/permissions/defaults.ts +0 -1
  277. package/src/permissions/prompter.ts +10 -1
  278. package/src/permissions/trust-store.ts +13 -0
  279. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  280. package/src/prompts/system-prompt.ts +70 -45
  281. package/src/providers/anthropic/client.ts +133 -24
  282. package/src/providers/gemini/client.ts +15 -6
  283. package/src/providers/managed-proxy/constants.ts +2 -2
  284. package/src/providers/managed-proxy/context.ts +5 -1
  285. package/src/providers/ratelimit.ts +17 -0
  286. package/src/providers/registry.ts +2 -2
  287. package/src/providers/retry.ts +1 -27
  288. package/src/runtime/AGENTS.md +17 -0
  289. package/src/runtime/auth/route-policy.ts +0 -3
  290. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  291. package/src/runtime/channel-readiness-service.ts +168 -195
  292. package/src/runtime/channel-readiness-types.ts +4 -0
  293. package/src/runtime/channel-reply-delivery.ts +0 -40
  294. package/src/runtime/gateway-client.ts +0 -7
  295. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  296. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  297. package/src/runtime/guardian-action-message-composer.ts +3 -23
  298. package/src/runtime/http-server.ts +17 -10
  299. package/src/runtime/http-types.ts +2 -3
  300. package/src/runtime/middleware/rate-limiter.ts +74 -20
  301. package/src/runtime/middleware/twilio-validation.ts +1 -11
  302. package/src/runtime/pending-interactions.ts +14 -12
  303. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  304. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  305. package/src/runtime/routes/conversation-routes.ts +73 -19
  306. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  307. package/src/runtime/routes/events-routes.ts +21 -11
  308. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  309. package/src/runtime/routes/host-cu-routes.ts +97 -0
  310. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  311. package/src/runtime/routes/integrations/slack/share.ts +6 -6
  312. package/src/runtime/routes/integrations/twilio.ts +6 -5
  313. package/src/runtime/routes/log-export-routes.ts +126 -8
  314. package/src/runtime/routes/secret-routes.ts +3 -2
  315. package/src/runtime/routes/settings-routes.ts +113 -48
  316. package/src/runtime/routes/surface-action-routes.ts +1 -1
  317. package/src/runtime/routes/watch-routes.ts +128 -0
  318. package/src/schedule/integration-status.ts +10 -8
  319. package/src/security/credential-key.ts +14 -0
  320. package/src/security/keychain-broker-client.ts +5 -6
  321. package/src/security/oauth2.ts +1 -1
  322. package/src/security/token-manager.ts +145 -43
  323. package/src/skills/catalog-install.ts +358 -0
  324. package/src/skills/include-graph.ts +32 -0
  325. package/src/telegram/bot-username.ts +2 -3
  326. package/src/tools/apps/definitions.ts +0 -5
  327. package/src/tools/assets/materialize.ts +0 -5
  328. package/src/tools/assets/search.ts +0 -5
  329. package/src/tools/browser/headless-browser.ts +1 -67
  330. package/src/tools/browser/network-recorder.ts +1 -1
  331. package/src/tools/browser/network-recording-types.ts +1 -1
  332. package/src/tools/claude-code/claude-code.ts +0 -5
  333. package/src/tools/computer-use/definitions.ts +46 -11
  334. package/src/tools/computer-use/registry.ts +4 -5
  335. package/src/tools/credentials/broker.ts +5 -4
  336. package/src/tools/credentials/metadata-store.ts +22 -74
  337. package/src/tools/credentials/resolve.ts +2 -1
  338. package/src/tools/credentials/vault.ts +139 -151
  339. package/src/tools/filesystem/edit.ts +1 -6
  340. package/src/tools/filesystem/read.ts +0 -5
  341. package/src/tools/filesystem/write.ts +1 -6
  342. package/src/tools/host-filesystem/edit.ts +1 -6
  343. package/src/tools/host-filesystem/read.ts +1 -6
  344. package/src/tools/host-filesystem/write.ts +1 -6
  345. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  346. package/src/tools/memory/definitions.ts +0 -5
  347. package/src/tools/network/web-fetch.ts +0 -5
  348. package/src/tools/network/web-search.ts +0 -5
  349. package/src/tools/registry.ts +2 -7
  350. package/src/tools/schema-transforms.ts +99 -0
  351. package/src/tools/skills/load.ts +62 -8
  352. package/src/tools/swarm/delegate.ts +0 -5
  353. package/src/tools/system/avatar-generator.ts +0 -5
  354. package/src/tools/ui-surface/definitions.ts +0 -15
  355. package/src/tools/watch/screen-watch.ts +0 -5
  356. package/src/tools/watch/watch-state.ts +0 -12
  357. package/src/util/logger.ts +7 -41
  358. package/src/util/platform.ts +9 -28
  359. package/src/version.ts +10 -0
  360. package/src/watcher/providers/github.ts +51 -52
  361. package/src/watcher/providers/gmail.ts +88 -80
  362. package/src/watcher/providers/google-calendar.ts +94 -86
  363. package/src/watcher/providers/linear.ts +87 -93
  364. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  365. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  366. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  367. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  368. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  369. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  370. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  371. package/src/cli/commands/dev.ts +0 -129
  372. package/src/cli/commands/map.ts +0 -391
  373. package/src/cli/commands/oauth.ts +0 -77
  374. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  375. package/src/daemon/computer-use-session.ts +0 -1020
  376. package/src/daemon/ride-shotgun-handler.ts +0 -567
  377. package/src/oauth/provider-profiles.ts +0 -192
  378. package/src/prompts/computer-use-prompt.ts +0 -98
  379. package/src/runtime/routes/computer-use-routes.ts +0 -641
  380. package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
  381. package/src/runtime/telegram-streaming-delivery.ts +0 -383
  382. package/src/tools/computer-use/request-computer-control.ts +0 -61
@@ -1,6 +1,7 @@
1
1
  import * as http from "node:http";
2
2
  import { afterEach, describe, expect, mock, test } from "bun:test";
3
3
 
4
+ import { credentialKey } from "../security/credential-key.js";
4
5
  import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
5
6
  import type { CredentialInjectionTemplate } from "../tools/credentials/policy-types.js";
6
7
  import type { ResolvedCredential } from "../tools/credentials/resolve.js";
@@ -82,7 +83,7 @@ function makeResolved(
82
83
  credentialId,
83
84
  service,
84
85
  field,
85
- storageKey: `credential:${service}:${field}`,
86
+ storageKey: credentialKey(service, field),
86
87
  injectionTemplates: templates,
87
88
  metadata: {
88
89
  credentialId,
@@ -154,7 +155,7 @@ describe("policyCallback credential injection", () => {
154
155
  resolveByIdResults.set("cred-local", resolved);
155
156
  credentialMetadataList.push(resolved.metadata);
156
157
  secureKeyValues.set(
157
- "credential:test-service:api-key",
158
+ credentialKey("test-service", "api-key"),
158
159
  "fal_secretvalue123",
159
160
  );
160
161
 
@@ -194,7 +195,10 @@ describe("policyCallback credential injection", () => {
194
195
  const resolved = makeResolved("cred-bearer", [tpl]);
195
196
  resolveByIdResults.set("cred-bearer", resolved);
196
197
  credentialMetadataList.push(resolved.metadata);
197
- secureKeyValues.set("credential:test-service:api-key", "tok_abc123");
198
+ secureKeyValues.set(
199
+ credentialKey("test-service", "api-key"),
200
+ "tok_abc123",
201
+ );
198
202
 
199
203
  const session = createSession(
200
204
  CONV_ID,
@@ -311,7 +315,7 @@ describe("MITM rewriteCallback credential injection", () => {
311
315
  const tpl = makeTemplate("*.fal.ai", "authorization", "Key ");
312
316
  const resolved = makeResolved("cred-fal", [tpl]);
313
317
  resolveByIdResults.set("cred-fal", resolved);
314
- secureKeyValues.set("credential:test-service:api-key", "fal_secret");
318
+ secureKeyValues.set(credentialKey("test-service", "api-key"), "fal_secret");
315
319
 
316
320
  const templates = new Map([["cred-fal", [tpl]]]);
317
321
  const headers: Record<string, string> = {
@@ -350,7 +354,7 @@ describe("MITM rewriteCallback credential injection", () => {
350
354
 
351
355
  const tpl = makeTemplate("*.fal.ai", "authorization", "Key ");
352
356
  resolveByIdResults.set("cred-fal", makeResolved("cred-fal", [tpl]));
353
- secureKeyValues.set("credential:test-service:api-key", "fal_secret");
357
+ secureKeyValues.set(credentialKey("test-service", "api-key"), "fal_secret");
354
358
 
355
359
  const templates = new Map([["cred-fal", [tpl]]]);
356
360
  const headers: Record<string, string> = {
@@ -479,8 +483,8 @@ describe("composeWith injection", () => {
479
483
  );
480
484
  resolveByServiceFieldResults.set("twilio:auth_token", composedResolved);
481
485
 
482
- secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
483
- secureKeyValues.set("credential:twilio:auth_token", "secret456");
486
+ secureKeyValues.set(credentialKey("twilio", "account_sid"), "ACtest123");
487
+ secureKeyValues.set(credentialKey("twilio", "auth_token"), "secret456");
484
488
 
485
489
  const session = createSession(
486
490
  CONV_ID,
@@ -531,7 +535,7 @@ describe("composeWith injection", () => {
531
535
  );
532
536
  resolveByIdResults.set("cred-primary", primaryResolved);
533
537
  credentialMetadataList.push(primaryResolved.metadata);
534
- secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
538
+ secureKeyValues.set(credentialKey("twilio", "account_sid"), "ACtest123");
535
539
 
536
540
  // Do NOT register the composed credential in resolveByServiceFieldResults
537
541
 
@@ -578,7 +582,10 @@ describe("composeWith injection", () => {
578
582
  const resolved = makeResolved("cred-b64", [tpl]);
579
583
  resolveByIdResults.set("cred-b64", resolved);
580
584
  credentialMetadataList.push(resolved.metadata);
581
- secureKeyValues.set("credential:test-service:api-key", "plaintext");
585
+ secureKeyValues.set(
586
+ credentialKey("test-service", "api-key"),
587
+ "plaintext",
588
+ );
582
589
 
583
590
  const session = createSession(CONV_ID, ["cred-b64"], undefined, DATA_DIR);
584
591
  const started = await startSession(session.id);
@@ -624,7 +631,7 @@ describe("composeWith injection", () => {
624
631
  );
625
632
  resolveByIdResults.set("cred-primary", primaryResolved);
626
633
  credentialMetadataList.push(primaryResolved.metadata);
627
- secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
634
+ secureKeyValues.set(credentialKey("twilio", "account_sid"), "ACtest123");
628
635
 
629
636
  // Composed credential metadata resolves, but no secret value stored
630
637
  const composedResolved = makeResolved(
@@ -634,7 +641,7 @@ describe("composeWith injection", () => {
634
641
  "auth_token",
635
642
  );
636
643
  resolveByServiceFieldResults.set("twilio:auth_token", composedResolved);
637
- // Do NOT set secureKeyValues for "credential:twilio:auth_token"
644
+ // Do NOT set secureKeyValues for credentialKey("twilio", "auth_token")
638
645
 
639
646
  const session = createSession(
640
647
  CONV_ID,
@@ -700,8 +707,11 @@ describe("composeWith injection", () => {
700
707
  composedResolved,
701
708
  );
702
709
 
703
- secureKeyValues.set("credential:my-service:primary-key", "value1");
704
- secureKeyValues.set("credential:my-service:secondary-key", "value2");
710
+ secureKeyValues.set(credentialKey("my-service", "primary-key"), "value1");
711
+ secureKeyValues.set(
712
+ credentialKey("my-service", "secondary-key"),
713
+ "value2",
714
+ );
705
715
 
706
716
  const session = createSession(
707
717
  CONV_ID,
@@ -64,7 +64,7 @@ function makeResolved(
64
64
  credentialId,
65
65
  service: "test-service",
66
66
  field: "api-key",
67
- storageKey: `credential:test-service:api-key`,
67
+ storageKey: `credential/test-service/api-key`,
68
68
  injectionTemplates: templates,
69
69
  metadata: {
70
70
  credentialId,
@@ -56,7 +56,7 @@ function makeResolved(
56
56
  credentialId,
57
57
  service: "test-service",
58
58
  field: "api-key",
59
- storageKey: `credential:test-service:api-key`,
59
+ storageKey: `credential/test-service/api-key`,
60
60
  injectionTemplates: templates,
61
61
  metadata: {
62
62
  credentialId,
@@ -22,7 +22,6 @@ mock.module("../util/logger.js", () => ({
22
22
  new Proxy({} as Record<string, unknown>, {
23
23
  get: () => () => {},
24
24
  }),
25
- isDebug: () => false,
26
25
  }));
27
26
 
28
27
  const { checkIngressForSecrets } =
@@ -69,6 +69,8 @@ mock.module("./policy-validate.js", () => ({
69
69
  }),
70
70
  }));
71
71
 
72
+ import { credentialKey } from "../security/credential-key.js";
73
+
72
74
  const { credentialStoreTool } = await import("../tools/credentials/vault.js");
73
75
 
74
76
  describe("one-time send override", () => {
@@ -96,7 +98,7 @@ describe("one-time send override", () => {
96
98
  expect(result.isError).toBe(true);
97
99
  expect(result.content).toContain("not enabled");
98
100
  // Value must NOT be stored in keychain
99
- expect(storedKeys.has("credential:svc:key")).toBe(false);
101
+ expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
100
102
  });
101
103
 
102
104
  test("transient_send succeeds when allowOneTimeSend is enabled", async () => {
@@ -119,7 +121,7 @@ describe("one-time send override", () => {
119
121
  expect(result.isError).toBe(false);
120
122
  expect(result.content).toContain("NOT saved");
121
123
  // Value must NOT be stored in keychain
122
- expect(storedKeys.has("credential:svc:key")).toBe(false);
124
+ expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
123
125
  });
124
126
 
125
127
  test("store delivery always persists to keychain regardless of allowOneTimeSend", async () => {
@@ -138,7 +140,7 @@ describe("one-time send override", () => {
138
140
  );
139
141
  expect(result.isError).toBe(false);
140
142
  expect(result.content).toContain("stored");
141
- expect(storedKeys.has("credential:svc:key")).toBe(true);
143
+ expect(storedKeys.has(credentialKey("svc", "key"))).toBe(true);
142
144
  });
143
145
 
144
146
  test("transient_send response content never contains the secret value", async () => {
@@ -117,6 +117,7 @@ function makeCompletingSession(): Session {
117
117
  updateClient: () => {},
118
118
  setHostBashProxy: () => {},
119
119
  setHostFileProxy: () => {},
120
+ setHostCuProxy: () => {},
120
121
  hasAnyPendingConfirmation: () => false,
121
122
  hasPendingConfirmation: () => false,
122
123
  denyAllPendingConfirmations: () => {},
@@ -173,6 +174,7 @@ function makeHangingSession(): Session {
173
174
  updateClient: () => {},
174
175
  setHostBashProxy: () => {},
175
176
  setHostFileProxy: () => {},
177
+ setHostCuProxy: () => {},
176
178
  hasAnyPendingConfirmation: () => false,
177
179
  hasPendingConfirmation: () => false,
178
180
  denyAllPendingConfirmations: () => {},
@@ -257,6 +259,7 @@ function makePendingApprovalSession(
257
259
  updateClient: () => {},
258
260
  setHostBashProxy: () => {},
259
261
  setHostFileProxy: () => {},
262
+ setHostCuProxy: () => {},
260
263
  hasAnyPendingConfirmation: () => pending.size > 0,
261
264
  hasPendingConfirmation: (candidateRequestId: string) =>
262
265
  pending.has(candidateRequestId),
@@ -364,11 +367,17 @@ describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
364
367
  interface: "macos",
365
368
  }),
366
369
  });
367
- const body = (await res.json()) as { accepted: boolean; messageId: string };
370
+ const body = (await res.json()) as {
371
+ accepted: boolean;
372
+ messageId: string;
373
+ conversationId: string;
374
+ };
368
375
 
369
376
  expect(res.status).toBe(202);
370
377
  expect(body.accepted).toBe(true);
371
378
  expect(body.messageId).toBeDefined();
379
+ expect(typeof body.conversationId).toBe("string");
380
+ expect(body.conversationId.length).toBeGreaterThan(0);
372
381
 
373
382
  await stopServer();
374
383
  });
@@ -485,8 +494,8 @@ describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
485
494
  createCanonicalGuardianRequest({
486
495
  id: requestId,
487
496
  kind: "tool_approval",
488
- sourceType: "desktop",
489
- sourceChannel: "vellum",
497
+ sourceType: "voice",
498
+ sourceChannel: "slack",
490
499
  conversationId,
491
500
  toolName: "call_start",
492
501
  status: "pending",
@@ -511,8 +520,8 @@ describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
511
520
  body: JSON.stringify({
512
521
  conversationKey,
513
522
  content: "sure let's do that",
514
- sourceChannel: "vellum",
515
- interface: "macos",
523
+ sourceChannel: "slack",
524
+ interface: "slack",
516
525
  }),
517
526
  });
518
527
  const body = (await res.json()) as {
@@ -808,11 +817,17 @@ describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
808
817
  interface: "macos",
809
818
  }),
810
819
  });
811
- const body2 = (await res2.json()) as { accepted: boolean; queued: boolean };
820
+ const body2 = (await res2.json()) as {
821
+ accepted: boolean;
822
+ queued: boolean;
823
+ conversationId: string;
824
+ };
812
825
 
813
826
  expect(res2.status).toBe(202);
814
827
  expect(body2.accepted).toBe(true);
815
828
  expect(body2.queued).toBe(true);
829
+ expect(typeof body2.conversationId).toBe("string");
830
+ expect(body2.conversationId.length).toBeGreaterThan(0);
816
831
 
817
832
  await stopServer();
818
833
  });
@@ -22,7 +22,6 @@ mock.module("../util/logger.js", () => ({
22
22
  new Proxy({} as Record<string, unknown>, {
23
23
  get: () => () => {},
24
24
  }),
25
- isDebug: () => false,
26
25
  truncateForLog: (value: string) => value,
27
26
  }));
28
27
 
@@ -127,7 +127,6 @@ mock.module("../util/logger.js", () => ({
127
127
  new Proxy({} as Record<string, unknown>, {
128
128
  get: () => () => {},
129
129
  }),
130
- isDebug: () => false,
131
130
  truncateForLog: (value: string, maxLen = 500) =>
132
131
  value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
133
132
  initLogger: () => {},
@@ -342,7 +341,7 @@ describe("Session initialization benchmark", () => {
342
341
  }
343
342
 
344
343
  timings.sort((a, b) => a - b);
345
- expect(median(timings)).toBeLessThan(10);
344
+ expect(median(timings)).toBeLessThan(15);
346
345
  });
347
346
 
348
347
  test("buildSystemPrompt assembles prompt under 50ms (median of 5)", () => {
@@ -384,9 +383,9 @@ describe("Session initialization benchmark", () => {
384
383
  await initializeTools();
385
384
  const definitions = getAllToolDefinitions();
386
385
 
387
- // Sanity: we expect a meaningful number of core tools (at least 20)
386
+ // Sanity: we expect a meaningful number of core tools (at least 18)
388
387
  // but not an unreasonable explosion (under 200)
389
- expect(definitions.length).toBeGreaterThanOrEqual(20);
388
+ expect(definitions.length).toBeGreaterThanOrEqual(18);
390
389
  expect(definitions.length).toBeLessThan(200);
391
390
  });
392
391
  });
@@ -548,6 +547,6 @@ describe("End-to-end session creation benchmark", () => {
548
547
  }
549
548
 
550
549
  timings.sort((a, b) => a - b);
551
- expect(median(timings)).toBeLessThan(10);
550
+ expect(median(timings)).toBeLessThan(15);
552
551
  });
553
552
  });
@@ -5,6 +5,7 @@ import {
5
5
  redirectToSecurePrompt,
6
6
  } from "../daemon/session-messaging.js";
7
7
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
8
+ import { credentialKey } from "../security/credential-key.js";
8
9
 
9
10
  const setSecureKeyMock = mock((_key?: string, _value?: string) => true);
10
11
  const upsertCredentialMetadataMock = mock(
@@ -108,7 +109,7 @@ describe("session-messaging secret redirect", () => {
108
109
  label: "Telegram Bot Token",
109
110
  });
110
111
  expect(setSecureKeyMock).toHaveBeenCalledWith(
111
- "credential:telegram:bot_token",
112
+ credentialKey("telegram", "bot_token"),
112
113
  "123456789:ABCDefGHIJklmnopQRSTuvwxyz012345678",
113
114
  );
114
115
  expect(upsertCredentialMetadataMock).toHaveBeenCalledWith(
@@ -158,7 +159,7 @@ describe("session-messaging secret redirect", () => {
158
159
  label: "Telegram Bot Token",
159
160
  });
160
161
  expect(setSecureKeyMock).toHaveBeenCalledWith(
161
- "credential:telegram:bot_token",
162
+ credentialKey("telegram", "bot_token"),
162
163
  "123456789:ABCDefGHIJklmnopQRSTuvwxyz012345678",
163
164
  );
164
165
  });
@@ -197,7 +198,7 @@ describe("session-messaging secret redirect", () => {
197
198
  label: "Telegram Bot Token",
198
199
  });
199
200
  expect(setSecureKeyMock).toHaveBeenCalledWith(
200
- "credential:telegram:bot_token",
201
+ credentialKey("telegram", "bot_token"),
201
202
  "123456789:ABCDefGHIJklmnopQRSTuvwxyz012345678",
202
203
  );
203
204
  });
@@ -231,7 +232,7 @@ describe("session-messaging secret redirect", () => {
231
232
  label: "Secure Credential Entry",
232
233
  });
233
234
  expect(setSecureKeyMock).toHaveBeenCalledWith(
234
- "credential:detected:Some Unknown Secret",
235
+ credentialKey("detected", "Some Unknown Secret"),
235
236
  "opaque-secret",
236
237
  );
237
238
  expect(upsertCredentialMetadataMock).toHaveBeenCalledWith(
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import type { SkillSummary } from "../config/skills.js";
4
4
  import {
5
+ collectAllMissing,
5
6
  getImmediateChildren,
6
7
  indexCatalogById,
7
8
  traverseIncludes,
@@ -298,3 +299,68 @@ describe("validateIncludes — cycle detection", () => {
298
299
  }
299
300
  });
300
301
  });
302
+
303
+ describe("collectAllMissing", () => {
304
+ test("returns empty set when skill has no includes", () => {
305
+ const catalog = [makeSkill("root")];
306
+ const index = indexCatalogById(catalog);
307
+ expect(collectAllMissing("root", index)).toEqual(new Set([]));
308
+ });
309
+
310
+ test("returns empty set when all includes are present", () => {
311
+ const catalog = [makeSkill("A", ["B"]), makeSkill("B")];
312
+ const index = indexCatalogById(catalog);
313
+ expect(collectAllMissing("A", index)).toEqual(new Set([]));
314
+ });
315
+
316
+ test("returns immediate missing children", () => {
317
+ const catalog = [makeSkill("A", ["B", "C"]), makeSkill("C")];
318
+ const index = indexCatalogById(catalog);
319
+ expect(collectAllMissing("A", index)).toEqual(new Set(["B"]));
320
+ });
321
+
322
+ test("returns transitive missing children", () => {
323
+ const catalog = [makeSkill("A", ["B"]), makeSkill("B", ["C"])];
324
+ const index = indexCatalogById(catalog);
325
+ expect(collectAllMissing("A", index)).toEqual(new Set(["C"]));
326
+ });
327
+
328
+ test("returns multiple missing at different levels", () => {
329
+ // A→B→C, B present but C missing
330
+ const catalog1 = [makeSkill("A", ["B"]), makeSkill("B", ["C"])];
331
+ const index1 = indexCatalogById(catalog1);
332
+ expect(collectAllMissing("A", index1)).toEqual(new Set(["C"]));
333
+
334
+ // A includes B and C, both missing
335
+ const catalog2 = [makeSkill("A", ["B", "C"])];
336
+ const index2 = indexCatalogById(catalog2);
337
+ expect(collectAllMissing("A", index2)).toEqual(new Set(["B", "C"]));
338
+ });
339
+
340
+ test("handles diamond with missing leaf", () => {
341
+ const catalog = [
342
+ makeSkill("A", ["B", "C"]),
343
+ makeSkill("B", ["D"]),
344
+ makeSkill("C", ["D"]),
345
+ ];
346
+ const index = indexCatalogById(catalog);
347
+ const result = collectAllMissing("A", index);
348
+ expect(result).toEqual(new Set(["D"]));
349
+ // Verify no duplicates (Set handles this, but confirm size)
350
+ expect(result.size).toBe(1);
351
+ });
352
+
353
+ test("does not loop infinitely on cycles", () => {
354
+ const catalog = [makeSkill("A", ["B"]), makeSkill("B", ["A"])];
355
+ const index = indexCatalogById(catalog);
356
+ expect(collectAllMissing("A", index)).toEqual(new Set([]));
357
+ });
358
+
359
+ test("handles cycle with missing node", () => {
360
+ const catalog = [makeSkill("A", ["B"]), makeSkill("B", ["C"])];
361
+ // C is missing, and if C referenced B it would be a cycle — but C isn't in catalog
362
+ // So A→B→C, C missing
363
+ const index = indexCatalogById(catalog);
364
+ expect(collectAllMissing("A", index)).toEqual(new Set(["C"]));
365
+ });
366
+ });
@@ -61,7 +61,6 @@ mock.module("../util/logger.js", () => ({
61
61
  ...realLogger,
62
62
  getLogger: () => noopLogger,
63
63
  getCliLogger: () => noopLogger,
64
- isDebug: () => false,
65
64
  truncateForLog: (value: string) => value,
66
65
  initLogger: () => {},
67
66
  pruneOldLogFiles: () => 0,
@@ -60,10 +60,16 @@ mock.module("../util/logger.js", () => ({
60
60
  new Proxy({} as Record<string, unknown>, {
61
61
  get: () => () => {},
62
62
  }),
63
- isDebug: () => false,
64
63
  truncateForLog: (s: unknown) => String(s),
65
64
  }));
66
65
 
66
+ // Mock autoInstallFromCatalog — default returns false (not found in catalog).
67
+ // Tests can override via `mockAutoInstall.mockImplementation(...)`.
68
+ const mockAutoInstall = mock((_skillId: string) => Promise.resolve(false));
69
+ mock.module("../skills/catalog-install.js", () => ({
70
+ autoInstallFromCatalog: (skillId: string) => mockAutoInstall(skillId),
71
+ }));
72
+
67
73
  await import("../tools/skills/load.js");
68
74
  const { getTool } = await import("../tools/registry.js");
69
75
 
@@ -145,6 +151,10 @@ async function executeSkillLoad(
145
151
  describe("skill_load tool", () => {
146
152
  beforeEach(() => {
147
153
  mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
154
+ mockAutoInstall.mockReset();
155
+ mockAutoInstall.mockImplementation((_skillId: string) =>
156
+ Promise.resolve(false),
157
+ );
148
158
  });
149
159
 
150
160
  afterEach(() => {
@@ -850,4 +860,142 @@ describe("skill_load tool", () => {
850
860
  "Use `skill_execute` to call these tools.",
851
861
  );
852
862
  });
863
+
864
+ test("auto-installs missing includes from catalog", async () => {
865
+ // Parent includes "dep-a" which is not initially in the catalog
866
+ writeSkillWithIncludes(
867
+ "auto-parent",
868
+ "Auto Parent",
869
+ "Has auto-installable dep",
870
+ "Parent body",
871
+ ["dep-a"],
872
+ );
873
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- auto-parent\n");
874
+
875
+ // Mock autoInstallFromCatalog to succeed and write the skill to disk
876
+ mockAutoInstall.mockImplementation((skillId: string) => {
877
+ if (skillId === "dep-a") {
878
+ writeSkill("dep-a", "Dep A", "A dependency", "Dep A body");
879
+ // Add to SKILLS.md so catalog reload finds it
880
+ writeFileSync(
881
+ join(TEST_DIR, "skills", "SKILLS.md"),
882
+ "- auto-parent\n- dep-a\n",
883
+ );
884
+ return Promise.resolve(true);
885
+ }
886
+ return Promise.resolve(false);
887
+ });
888
+
889
+ const result = await executeSkillLoad({ skill: "auto-parent" });
890
+ expect(result.isError).toBe(false);
891
+ expect(result.content).toContain("Skill: Auto Parent");
892
+ expect(result.content).toContain("<loaded_skill");
893
+ expect(mockAutoInstall).toHaveBeenCalledWith("dep-a");
894
+ });
895
+
896
+ test("auto-installs transitive missing includes across rounds", async () => {
897
+ // Skill A includes B, B includes C. Neither B nor C in initial catalog.
898
+ writeSkillWithIncludes("trans-a", "Trans A", "Top level", "Body A", [
899
+ "trans-b",
900
+ ]);
901
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- trans-a\n");
902
+
903
+ let round = 0;
904
+ mockAutoInstall.mockImplementation((skillId: string) => {
905
+ if (skillId === "trans-b" && round === 0) {
906
+ // First round: install B (which includes C)
907
+ writeSkillWithIncludes("trans-b", "Trans B", "Mid level", "Body B", [
908
+ "trans-c",
909
+ ]);
910
+ writeFileSync(
911
+ join(TEST_DIR, "skills", "SKILLS.md"),
912
+ "- trans-a\n- trans-b\n",
913
+ );
914
+ round++;
915
+ return Promise.resolve(true);
916
+ }
917
+ if (skillId === "trans-c") {
918
+ // Second round: install C
919
+ writeSkill("trans-c", "Trans C", "Leaf", "Body C");
920
+ writeFileSync(
921
+ join(TEST_DIR, "skills", "SKILLS.md"),
922
+ "- trans-a\n- trans-b\n- trans-c\n",
923
+ );
924
+ return Promise.resolve(true);
925
+ }
926
+ return Promise.resolve(false);
927
+ });
928
+
929
+ const result = await executeSkillLoad({ skill: "trans-a" });
930
+ expect(result.isError).toBe(false);
931
+ expect(result.content).toContain("Skill: Trans A");
932
+ expect(result.content).toContain("<loaded_skill");
933
+ expect(mockAutoInstall).toHaveBeenCalledWith("trans-b");
934
+ expect(mockAutoInstall).toHaveBeenCalledWith("trans-c");
935
+ });
936
+
937
+ test("returns error when auto-install of missing include fails", async () => {
938
+ writeSkillWithIncludes(
939
+ "fail-parent",
940
+ "Fail Parent",
941
+ "Has failing dep",
942
+ "Body",
943
+ ["dep-x"],
944
+ );
945
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- fail-parent\n");
946
+
947
+ // autoInstallFromCatalog throws an error
948
+ mockAutoInstall.mockImplementation((skillId: string) => {
949
+ if (skillId === "dep-x") {
950
+ return Promise.reject(new Error("Network error"));
951
+ }
952
+ return Promise.resolve(false);
953
+ });
954
+
955
+ const result = await executeSkillLoad({ skill: "fail-parent" });
956
+ expect(result.isError).toBe(true);
957
+ expect(result.content).toContain("dep-x");
958
+ expect(result.content).toContain("not found");
959
+ expect(result.content).not.toContain("<loaded_skill");
960
+ });
961
+
962
+ test("stops after MAX_INSTALL_ROUNDS", async () => {
963
+ // Pathological case: each install round reveals a new missing dep
964
+ writeSkillWithIncludes("loop-root", "Loop Root", "Infinite deps", "Body", [
965
+ "loop-dep-0",
966
+ ]);
967
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- loop-root\n");
968
+
969
+ let installCount = 0;
970
+ mockAutoInstall.mockImplementation((skillId: string) => {
971
+ const id = skillId;
972
+ if (id.startsWith("loop-dep-")) {
973
+ installCount++;
974
+ const nextDepId = `loop-dep-${installCount}`;
975
+ // Install the requested dep, but it includes yet another missing dep
976
+ writeSkillWithIncludes(
977
+ id,
978
+ `Loop Dep ${installCount}`,
979
+ "Generated dep",
980
+ "Body",
981
+ [nextDepId],
982
+ );
983
+ // Update SKILLS.md to include all installed deps so far
984
+ const entries = ["- loop-root\n"];
985
+ for (let i = 0; i < installCount; i++) {
986
+ entries.push(`- loop-dep-${i}\n`);
987
+ }
988
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), entries.join(""));
989
+ return Promise.resolve(true);
990
+ }
991
+ return Promise.resolve(false);
992
+ });
993
+
994
+ const result = await executeSkillLoad({ skill: "loop-root" });
995
+ // Should terminate with an error (the final dep is still missing)
996
+ expect(result.isError).toBe(true);
997
+ expect(result.content).toContain("not found");
998
+ // Should have terminated — installCount should be bounded by MAX_INSTALL_ROUNDS (5)
999
+ expect(installCount).toBeLessThanOrEqual(5);
1000
+ });
853
1001
  });
@@ -210,7 +210,6 @@ mock.module("../util/logger.js", () => ({
210
210
  debug: () => {},
211
211
  error: () => {},
212
212
  }),
213
- isDebug: () => false,
214
213
  }));
215
214
 
216
215
  // ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@ import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
11
 
12
- import { uninstallSkillLocally } from "../cli/commands/skills.js";
12
+ import { uninstallSkillLocally } from "../skills/catalog-install.js";
13
13
 
14
14
  let tempDir: string;
15
15
  let originalBaseDataDir: string | undefined;
@@ -58,7 +58,7 @@ describe("assistant skills uninstall", () => {
58
58
 
59
59
  // GIVEN a skill is installed locally
60
60
  installFakeSkill("weather");
61
- writeSkillsIndex("- weather\n- google-oauth-setup\n");
61
+ writeSkillsIndex("- weather\n- google-oauth-applescript\n");
62
62
 
63
63
  // WHEN we uninstall the skill
64
64
  uninstallSkillLocally("weather");
@@ -71,7 +71,7 @@ describe("assistant skills uninstall", () => {
71
71
  expect(index).not.toContain("weather");
72
72
 
73
73
  // AND other skills should remain in the index
74
- expect(index).toContain("google-oauth-setup");
74
+ expect(index).toContain("google-oauth-applescript");
75
75
  });
76
76
 
77
77
  test("errors when skill is not installed", () => {