@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,15 +1,18 @@
1
1
  /**
2
- * Metadata-driven token manager for OAuth2 credentials.
2
+ * Token manager for OAuth2 credentials.
3
3
  *
4
- * Reads refresh configuration (tokenUrl, clientId) from credential metadata
5
- * rather than requiring an IntegrationDefinition, enabling autonomous token
6
- * refresh for any OAuth2 service that stores its config in metadata.
4
+ * Reads refresh configuration (tokenUrl, clientId, authMethod) exclusively
5
+ * from the SQLite oauth-store (provider + app + connection rows). After a
6
+ * successful refresh, writes tokens to new-format secure key paths and
7
+ * updates the oauth_connection row.
7
8
  */
8
9
 
9
10
  import {
10
- getCredentialMetadata,
11
- upsertCredentialMetadata,
12
- } from "../tools/credentials/metadata-store.js";
11
+ getApp,
12
+ getConnectionByProvider,
13
+ getProvider,
14
+ updateConnection,
15
+ } from "../oauth/oauth-store.js";
13
16
  import { getLogger } from "../util/logger.js";
14
17
  import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
15
18
  import { getSecureKey, setSecureKeyAsync } from "./secure-keys.js";
@@ -106,11 +109,34 @@ function recordRefreshFailure(service: string): void {
106
109
  }
107
110
  }
108
111
 
112
+ // ── Per-service refresh deduplication ─────────────────────────────────
113
+ // When multiple concurrent `withValidToken` calls detect an expired or
114
+ // 401-rejected token for the same service, only one actual refresh
115
+ // attempt is made. Other callers join the in-flight promise.
116
+
117
+ const inflightRefreshes = new Map<string, Promise<string>>();
118
+
119
+ function deduplicatedRefresh(service: string): Promise<string> {
120
+ const existing = inflightRefreshes.get(service);
121
+ if (existing) return existing;
122
+
123
+ const promise = doRefresh(service).finally(() => {
124
+ inflightRefreshes.delete(service);
125
+ });
126
+ inflightRefreshes.set(service, promise);
127
+ return promise;
128
+ }
129
+
109
130
  /** @internal Test-only: reset all circuit breaker state */
110
131
  export function _resetRefreshBreakers(): void {
111
132
  refreshBreakers.clear();
112
133
  }
113
134
 
135
+ /** @internal Test-only: reset in-flight refresh deduplication state */
136
+ export function _resetInflightRefreshes(): void {
137
+ inflightRefreshes.clear();
138
+ }
139
+
114
140
  /** @internal Test-only: get breaker state for a service */
115
141
  export function _getRefreshBreakerState(
116
142
  service: string,
@@ -132,54 +158,113 @@ export class TokenExpiredError extends Error {
132
158
 
133
159
  /**
134
160
  * Check whether the access token for a service is expired or will expire
135
- * within the buffer window, based on the `expiresAt` field in credential metadata.
161
+ * within the buffer window, based on the `expiresAt` field in the
162
+ * oauth_connection row.
136
163
  */
137
164
  function isTokenExpired(service: string): boolean {
138
- const meta = getCredentialMetadata(service, "access_token");
139
- if (!meta?.expiresAt) return false;
140
- return Date.now() >= meta.expiresAt - EXPIRY_BUFFER_MS;
165
+ try {
166
+ const conn = getConnectionByProvider(service);
167
+ if (!conn?.expiresAt) return false;
168
+ return Date.now() >= conn.expiresAt - EXPIRY_BUFFER_MS;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // ── Refresh config resolution ─────────────────────────────────────────
175
+
176
+ /** Shared shape for resolved refresh configuration. */
177
+ interface RefreshConfig {
178
+ tokenUrl: string;
179
+ clientId: string;
180
+ /** OAuth client secret (optional — PKCE flows may omit it). */
181
+ secret?: string;
182
+ refreshToken?: string;
183
+ authMethod?: TokenEndpointAuthMethod;
184
+ connId: string;
141
185
  }
142
186
 
143
187
  /**
144
- * Attempt to refresh the OAuth2 access token for a service using the
145
- * refresh token and OAuth2 config stored in credential metadata.
188
+ * Resolve refresh configuration from the SQLite oauth-store.
146
189
  *
147
- * Returns the new access token on success.
148
- * Throws `TokenExpiredError` if refresh is not possible.
190
+ * Looks up connection -> app -> provider to read tokenUrl, clientId, and
191
+ * authMethod. Throws `TokenExpiredError` if the connection is not found
192
+ * or incomplete.
149
193
  */
150
- async function doRefresh(service: string): Promise<string> {
151
- const refreshToken = getSecureKey(`credential:${service}:refresh_token`);
152
- if (!refreshToken) {
194
+ function resolveRefreshConfig(service: string): RefreshConfig {
195
+ const conn = getConnectionByProvider(service);
196
+ if (!conn) {
153
197
  throw new TokenExpiredError(
154
198
  service,
155
- `No refresh token available for "${service}". Re-authorization required.${recoveryHint(service)}`,
199
+ `No OAuth connection found for "${service}". Re-authorization required.${recoveryHint(service)}`,
156
200
  );
157
201
  }
158
202
 
159
- const meta = getCredentialMetadata(service, "access_token");
160
- const tokenUrl = meta?.oauth2TokenUrl;
161
- const clientId = meta?.oauth2ClientId;
203
+ const app = getApp(conn.oauthAppId);
204
+ if (!app) {
205
+ throw new TokenExpiredError(
206
+ service,
207
+ `No OAuth app found for "${service}". Re-authorization required.${recoveryHint(service)}`,
208
+ );
209
+ }
210
+
211
+ const provider = getProvider(conn.providerKey);
212
+ if (!provider) {
213
+ throw new TokenExpiredError(
214
+ service,
215
+ `No OAuth provider found for "${service}". Re-authorization required.${recoveryHint(service)}`,
216
+ );
217
+ }
162
218
 
219
+ const tokenUrl = provider.tokenUrl;
220
+ const clientId = app.clientId;
163
221
  if (!tokenUrl || !clientId) {
164
- // Legacy credentials created by the old integration flow don't store
165
- // oauth2TokenUrl/oauth2ClientId in metadata. The client ID is user-specific
166
- // (from their Google Cloud Console) and cannot be recovered, so the only
167
- // path forward is re-authorization via the new oauth2_connect flow.
168
- const isLegacy = service === "integration:gmail" && !tokenUrl && !clientId;
169
- const hint = isLegacy
170
- ? ` This is a one-time migration: your old Gmail connection needs to be re-authorized. Ask me to "reconnect Gmail" to set it up again.`
171
- : "";
172
222
  throw new TokenExpiredError(
173
223
  service,
174
- `Missing OAuth2 refresh config for "${service}".${hint}${recoveryHint(service)}`,
224
+ `Missing OAuth2 refresh config for "${service}".${recoveryHint(service)}`,
175
225
  );
176
226
  }
177
227
 
178
- const clientSecret = meta?.oauth2ClientSecret as string | undefined;
179
- const authMethod = meta?.oauth2TokenEndpointAuthMethod as
228
+ const secret = getSecureKey(`oauth_app/${app.id}/client_secret`);
229
+
230
+ const refreshToken = getSecureKey(
231
+ `oauth_connection/${conn.id}/refresh_token`,
232
+ );
233
+
234
+ const authMethod = provider.tokenEndpointAuthMethod as
180
235
  | TokenEndpointAuthMethod
181
236
  | undefined;
182
- const resolvedTokenUrl = tokenUrl;
237
+
238
+ return {
239
+ connId: conn.id,
240
+ tokenUrl,
241
+ clientId,
242
+ secret,
243
+ refreshToken,
244
+ authMethod,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Attempt to refresh the OAuth2 access token for a service.
250
+ *
251
+ * Reads refresh config exclusively from the SQLite oauth-store (provider,
252
+ * app, connection).
253
+ *
254
+ * Returns the new access token on success.
255
+ * Throws `TokenExpiredError` if refresh is not possible.
256
+ */
257
+ async function doRefresh(service: string): Promise<string> {
258
+ const refreshConfig = resolveRefreshConfig(service);
259
+ const { tokenUrl, clientId, secret, authMethod, connId, refreshToken } =
260
+ refreshConfig;
261
+
262
+ if (!refreshToken) {
263
+ throw new TokenExpiredError(
264
+ service,
265
+ `No refresh token available for "${service}". Re-authorization required.${recoveryHint(service)}`,
266
+ );
267
+ }
183
268
 
184
269
  if (isRefreshBreakerOpen(service)) {
185
270
  const state = refreshBreakers.get(service)!;
@@ -196,10 +281,10 @@ async function doRefresh(service: string): Promise<string> {
196
281
  let result;
197
282
  try {
198
283
  result = await refreshOAuth2Token(
199
- resolvedTokenUrl,
284
+ tokenUrl,
200
285
  clientId,
201
286
  refreshToken,
202
- clientSecret,
287
+ secret,
203
288
  authMethod,
204
289
  );
205
290
  } catch (err) {
@@ -217,9 +302,10 @@ async function doRefresh(service: string): Promise<string> {
217
302
  throw err;
218
303
  }
219
304
 
305
+ // ----- Store refreshed access_token -----
220
306
  if (
221
307
  !(await setSecureKeyAsync(
222
- `credential:${service}:access_token`,
308
+ `oauth_connection/${connId}/access_token`,
223
309
  result.accessToken,
224
310
  ))
225
311
  ) {
@@ -232,7 +318,7 @@ async function doRefresh(service: string): Promise<string> {
232
318
  if (result.refreshToken) {
233
319
  if (
234
320
  !(await setSecureKeyAsync(
235
- `credential:${service}:refresh_token`,
321
+ `oauth_connection/${connId}/refresh_token`,
236
322
  result.refreshToken,
237
323
  ))
238
324
  ) {
@@ -243,7 +329,7 @@ async function doRefresh(service: string): Promise<string> {
243
329
  }
244
330
  }
245
331
 
246
- // Update metadata with new expiry.
332
+ // Update oauth_connection row with new expiry.
247
333
  // Use null to explicitly clear a stale expiresAt when the provider omits
248
334
  // expires_in (or returns 0), so isTokenExpired won't keep forcing refreshes.
249
335
  const expiresAt =
@@ -251,7 +337,17 @@ async function doRefresh(service: string): Promise<string> {
251
337
  ? Date.now() + result.expiresIn * 1000
252
338
  : null;
253
339
 
254
- upsertCredentialMetadata(service, "access_token", { expiresAt });
340
+ try {
341
+ updateConnection(connId, {
342
+ expiresAt,
343
+ hasRefreshToken: !!result.refreshToken,
344
+ });
345
+ } catch (err) {
346
+ log.warn(
347
+ { err, service },
348
+ "Failed to update oauth_connection after refresh",
349
+ );
350
+ }
255
351
 
256
352
  recordRefreshSuccess(service);
257
353
  log.info({ service }, "OAuth2 access token refreshed successfully");
@@ -265,12 +361,18 @@ async function doRefresh(service: string): Promise<string> {
265
361
  * 1. Retrieves the stored access token (throws if none exists).
266
362
  * 2. If the token is expired or near-expiry, refreshes it before calling the callback.
267
363
  * 3. If the callback throws with a 401 status, attempts one refresh-and-retry cycle.
364
+ *
365
+ * Retained only for BYO connection internals — prefer
366
+ * `resolveOAuthConnection(service).request()` for new code.
268
367
  */
269
368
  export async function withValidToken<T>(
270
369
  service: string,
271
370
  callback: (token: string) => Promise<T>,
272
371
  ): Promise<T> {
273
- let token = getSecureKey(`credential:${service}:access_token`);
372
+ const conn = getConnectionByProvider(service);
373
+ let token = conn
374
+ ? getSecureKey(`oauth_connection/${conn.id}/access_token`)
375
+ : undefined;
274
376
  if (!token) {
275
377
  throw new TokenExpiredError(
276
378
  service,
@@ -280,14 +382,14 @@ export async function withValidToken<T>(
280
382
 
281
383
  // Proactively refresh if expired or about to expire.
282
384
  if (isTokenExpired(service)) {
283
- token = await doRefresh(service);
385
+ token = await deduplicatedRefresh(service);
284
386
  }
285
387
 
286
388
  try {
287
389
  return await callback(token);
288
390
  } catch (err: unknown) {
289
391
  if (is401Error(err)) {
290
- token = await doRefresh(service);
392
+ token = await deduplicatedRefresh(service);
291
393
  return callback(token);
292
394
  }
293
395
  throw err;
@@ -0,0 +1,358 @@
1
+ import { execSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import {
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ import { gunzipSync } from "node:zlib";
15
+
16
+ import { getLogger } from "../util/logger.js";
17
+ import {
18
+ getWorkspaceConfigPath,
19
+ getWorkspaceSkillsDir,
20
+ readPlatformToken,
21
+ } from "../util/platform.js";
22
+
23
+ const log = getLogger("catalog-install");
24
+
25
+ // ─── Types ───────────────────────────────────────────────────────────────────
26
+
27
+ export interface CatalogSkill {
28
+ id: string;
29
+ name: string;
30
+ description: string;
31
+ emoji?: string;
32
+ includes?: string[];
33
+ version?: string;
34
+ }
35
+
36
+ export interface CatalogManifest {
37
+ version: number;
38
+ skills: CatalogSkill[];
39
+ }
40
+
41
+ // ─── Path helpers ────────────────────────────────────────────────────────────
42
+
43
+ export function getSkillsIndexPath(): string {
44
+ return join(getWorkspaceSkillsDir(), "SKILLS.md");
45
+ }
46
+
47
+ /**
48
+ * Resolve the repo-level skills/ directory when running in dev mode.
49
+ * Returns the path if VELLUM_DEV is set and the directory exists, or undefined.
50
+ */
51
+ export function getRepoSkillsDir(): string | undefined {
52
+ if (!process.env.VELLUM_DEV) return undefined;
53
+
54
+ // assistant/src/skills/catalog-install.ts -> ../../../skills/
55
+ const candidate = join(import.meta.dir, "..", "..", "..", "skills");
56
+ if (existsSync(join(candidate, "catalog.json"))) {
57
+ return candidate;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ // ─── Platform API ────────────────────────────────────────────────────────────
63
+
64
+ function getConfigPlatformUrl(): string | undefined {
65
+ try {
66
+ const configPath = getWorkspaceConfigPath();
67
+ if (!existsSync(configPath)) return undefined;
68
+ const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
69
+ string,
70
+ unknown
71
+ >;
72
+ const platform = raw.platform as Record<string, unknown> | undefined;
73
+ const baseUrl = platform?.baseUrl;
74
+ if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
75
+ } catch {
76
+ // ignore
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ function getPlatformUrl(): string {
82
+ return (
83
+ process.env.VELLUM_PLATFORM_URL ??
84
+ getConfigPlatformUrl() ??
85
+ "https://platform.vellum.ai"
86
+ );
87
+ }
88
+
89
+ function buildHeaders(): Record<string, string> {
90
+ const headers: Record<string, string> = {};
91
+ const token = readPlatformToken();
92
+ if (token) {
93
+ headers["X-Session-Token"] = token;
94
+ }
95
+ return headers;
96
+ }
97
+
98
+ // ─── Catalog operations ──────────────────────────────────────────────────────
99
+
100
+ export async function fetchCatalog(): Promise<CatalogSkill[]> {
101
+ const url = `${getPlatformUrl()}/v1/skills/`;
102
+ const response = await fetch(url, {
103
+ headers: buildHeaders(),
104
+ signal: AbortSignal.timeout(10000),
105
+ });
106
+
107
+ if (!response.ok) {
108
+ throw new Error(
109
+ `Platform API error ${response.status}: ${response.statusText}`,
110
+ );
111
+ }
112
+
113
+ const manifest = (await response.json()) as CatalogManifest;
114
+ if (!Array.isArray(manifest.skills)) {
115
+ throw new Error("Platform catalog has invalid skills array");
116
+ }
117
+ return manifest.skills;
118
+ }
119
+
120
+ export function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
121
+ try {
122
+ const raw = readFileSync(join(repoSkillsDir, "catalog.json"), "utf-8");
123
+ const manifest = JSON.parse(raw) as CatalogManifest;
124
+ if (!Array.isArray(manifest.skills)) return [];
125
+ return manifest.skills;
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ // ─── Tar extraction ──────────────────────────────────────────────────────────
132
+
133
+ /**
134
+ * Extract all files from a tar archive (uncompressed) into a directory.
135
+ * Returns true if a SKILL.md was found in the archive.
136
+ */
137
+ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
138
+ let foundSkillMd = false;
139
+ let offset = 0;
140
+ while (offset + 512 <= tarBuffer.length) {
141
+ const header = tarBuffer.subarray(offset, offset + 512);
142
+
143
+ // End-of-archive (two consecutive zero blocks)
144
+ if (header.every((b) => b === 0)) break;
145
+
146
+ // Filename (bytes 0-99, null-terminated)
147
+ const nameEnd = header.indexOf(0, 0);
148
+ const name = header
149
+ .subarray(0, Math.min(nameEnd >= 0 ? nameEnd : 100, 100))
150
+ .toString("utf-8");
151
+
152
+ // File type (byte 156): '5' = directory, '0' or '\0' = regular file
153
+ const typeFlag = header[156];
154
+
155
+ // File size (bytes 124-135, octal)
156
+ const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
157
+ const size = parseInt(sizeStr, 8) || 0;
158
+
159
+ offset += 512; // past header
160
+
161
+ // Skip directories and empty names
162
+ if (name && typeFlag !== 53 /* '5' */) {
163
+ // Prevent path traversal
164
+ const normalizedName = name.replace(/^\.\//, "");
165
+ if (!normalizedName.startsWith("..") && !normalizedName.includes("/..")) {
166
+ const destPath = join(destDir, normalizedName);
167
+ mkdirSync(dirname(destPath), { recursive: true });
168
+ writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
169
+
170
+ if (
171
+ normalizedName === "SKILL.md" ||
172
+ normalizedName.endsWith("/SKILL.md")
173
+ ) {
174
+ foundSkillMd = true;
175
+ }
176
+ }
177
+ }
178
+
179
+ // Skip to next header (data padded to 512 bytes)
180
+ offset += Math.ceil(size / 512) * 512;
181
+ }
182
+ return foundSkillMd;
183
+ }
184
+
185
+ export async function fetchAndExtractSkill(
186
+ skillId: string,
187
+ destDir: string,
188
+ ): Promise<void> {
189
+ const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
190
+ const response = await fetch(url, {
191
+ headers: buildHeaders(),
192
+ signal: AbortSignal.timeout(15000),
193
+ });
194
+
195
+ if (!response.ok) {
196
+ throw new Error(
197
+ `Failed to fetch skill "${skillId}": HTTP ${response.status}`,
198
+ );
199
+ }
200
+
201
+ const gzipBuffer = Buffer.from(await response.arrayBuffer());
202
+ const tarBuffer = gunzipSync(gzipBuffer);
203
+ const foundSkillMd = extractTarToDir(tarBuffer, destDir);
204
+
205
+ if (!foundSkillMd) {
206
+ throw new Error(`SKILL.md not found in archive for "${skillId}"`);
207
+ }
208
+ }
209
+
210
+ // ─── SKILLS.md index management ──────────────────────────────────────────────
211
+
212
+ function atomicWriteFile(filePath: string, content: string): void {
213
+ const dir = dirname(filePath);
214
+ mkdirSync(dir, { recursive: true });
215
+ const tmpPath = join(dir, `.tmp-${randomUUID()}`);
216
+ writeFileSync(tmpPath, content, "utf-8");
217
+ renameSync(tmpPath, filePath);
218
+ }
219
+
220
+ export function upsertSkillsIndex(id: string): void {
221
+ const indexPath = getSkillsIndexPath();
222
+ let lines: string[] = [];
223
+ if (existsSync(indexPath)) {
224
+ lines = readFileSync(indexPath, "utf-8").split("\n");
225
+ }
226
+
227
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
228
+ const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
229
+ if (lines.some((line) => pattern.test(line))) return;
230
+
231
+ const nonEmpty = lines.filter((l) => l.trim());
232
+ nonEmpty.push(`- ${id}`);
233
+ const content = nonEmpty.join("\n");
234
+ atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
235
+ }
236
+
237
+ export function removeSkillsIndexEntry(id: string): void {
238
+ const indexPath = getSkillsIndexPath();
239
+ if (!existsSync(indexPath)) return;
240
+
241
+ const lines = readFileSync(indexPath, "utf-8").split("\n");
242
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
243
+ const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
244
+ const filtered = lines.filter((line) => !pattern.test(line));
245
+
246
+ // If nothing changed, skip the write
247
+ if (filtered.length === lines.length) return;
248
+
249
+ const content = filtered.join("\n");
250
+ atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
251
+ }
252
+
253
+ // ─── Install / uninstall ─────────────────────────────────────────────────────
254
+
255
+ export function uninstallSkillLocally(skillId: string): void {
256
+ const skillDir = join(getWorkspaceSkillsDir(), skillId);
257
+
258
+ if (!existsSync(skillDir)) {
259
+ throw new Error(`Skill "${skillId}" is not installed.`);
260
+ }
261
+
262
+ rmSync(skillDir, { recursive: true, force: true });
263
+ removeSkillsIndexEntry(skillId);
264
+ }
265
+
266
+ export async function installSkillLocally(
267
+ skillId: string,
268
+ catalogEntry: CatalogSkill,
269
+ overwrite: boolean,
270
+ ): Promise<void> {
271
+ const skillDir = join(getWorkspaceSkillsDir(), skillId);
272
+ const skillFilePath = join(skillDir, "SKILL.md");
273
+
274
+ if (existsSync(skillFilePath) && !overwrite) {
275
+ throw new Error(
276
+ `Skill "${skillId}" is already installed. Use --overwrite to replace it.`,
277
+ );
278
+ }
279
+
280
+ mkdirSync(skillDir, { recursive: true });
281
+
282
+ // In dev mode, install from the local repo skills directory if available
283
+ const repoSkillsDir = getRepoSkillsDir();
284
+ const repoSkillSource = repoSkillsDir
285
+ ? join(repoSkillsDir, skillId)
286
+ : undefined;
287
+
288
+ if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
289
+ cpSync(repoSkillSource, skillDir, { recursive: true });
290
+ } else {
291
+ await fetchAndExtractSkill(skillId, skillDir);
292
+ }
293
+
294
+ // Write version metadata
295
+ if (catalogEntry.version) {
296
+ const meta = {
297
+ version: catalogEntry.version,
298
+ installedAt: new Date().toISOString(),
299
+ };
300
+ atomicWriteFile(
301
+ join(skillDir, "version.json"),
302
+ JSON.stringify(meta, null, 2) + "\n",
303
+ );
304
+ }
305
+
306
+ // Install npm dependencies if the skill has a package.json
307
+ if (existsSync(join(skillDir, "package.json"))) {
308
+ const bunPath = `${homedir()}/.bun/bin`;
309
+ execSync("bun install", {
310
+ cwd: skillDir,
311
+ stdio: "inherit",
312
+ env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
313
+ });
314
+ }
315
+
316
+ // Register in SKILLS.md only after all steps succeed
317
+ upsertSkillsIndex(skillId);
318
+ }
319
+
320
+ // ─── Auto-install (for skill_load) ──────────────────────────────────────────
321
+
322
+ /**
323
+ * Attempt to find and install a skill from the first-party catalog.
324
+ * Returns true if the skill was installed, false if not found in catalog.
325
+ * Throws on install failures (network, filesystem, etc).
326
+ */
327
+ export async function autoInstallFromCatalog(
328
+ skillId: string,
329
+ ): Promise<boolean> {
330
+ // Check local catalog first (dev mode), then remote
331
+ const repoSkillsDir = getRepoSkillsDir();
332
+ let entry: CatalogSkill | undefined;
333
+
334
+ if (repoSkillsDir) {
335
+ const localCatalog = readLocalCatalog(repoSkillsDir);
336
+ entry = localCatalog.find((s) => s.id === skillId);
337
+ }
338
+
339
+ if (!entry) {
340
+ try {
341
+ const remoteCatalog = await fetchCatalog();
342
+ entry = remoteCatalog.find((s) => s.id === skillId);
343
+ } catch (err) {
344
+ log.warn(
345
+ { err, skillId },
346
+ "Failed to fetch remote catalog for auto-install",
347
+ );
348
+ return false;
349
+ }
350
+ }
351
+
352
+ if (!entry) {
353
+ return false;
354
+ }
355
+
356
+ await installSkillLocally(skillId, entry, false);
357
+ return true;
358
+ }
@@ -151,3 +151,35 @@ export function traverseIncludes(
151
151
  dfs(rootId);
152
152
  return { visited };
153
153
  }
154
+
155
+ /**
156
+ * Collect all missing skill IDs reachable from the root's include graph.
157
+ * DFS traversal that tracks visited nodes to prevent infinite loops on cycles.
158
+ * The root itself is never reported as missing (it's already loaded by the caller).
159
+ */
160
+ export function collectAllMissing(
161
+ rootId: string,
162
+ catalogIndex: Map<string, SkillSummary>,
163
+ ): Set<string> {
164
+ const missing = new Set<string>();
165
+ const visited = new Set<string>();
166
+
167
+ function dfs(id: string): void {
168
+ if (visited.has(id)) return;
169
+ visited.add(id);
170
+
171
+ const skill = catalogIndex.get(id);
172
+ if (!skill?.includes) return;
173
+
174
+ for (const childId of skill.includes) {
175
+ if (!catalogIndex.has(childId)) {
176
+ missing.add(childId);
177
+ } else if (!visited.has(childId)) {
178
+ dfs(childId);
179
+ }
180
+ }
181
+ }
182
+
183
+ dfs(rootId);
184
+ return missing;
185
+ }