@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -0,0 +1,93 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+
6
+ import { extractTarToDir } from "../skills/catalog-install.js";
7
+
8
+ let tempDir: string;
9
+
10
+ function makeTarEntry(name: string, content: string): Buffer {
11
+ const header = Buffer.alloc(512, 0);
12
+ const nameBuffer = Buffer.from(name, "utf-8");
13
+ nameBuffer.copy(header, 0, 0, Math.min(nameBuffer.length, 100));
14
+
15
+ const mode = Buffer.from("0000644\0", "ascii");
16
+ mode.copy(header, 100);
17
+ Buffer.from("0000000\0", "ascii").copy(header, 108); // uid
18
+ Buffer.from("0000000\0", "ascii").copy(header, 116); // gid
19
+
20
+ const sizeOct = content.length.toString(8).padStart(11, "0") + "\0";
21
+ Buffer.from(sizeOct, "ascii").copy(header, 124);
22
+
23
+ Buffer.from("00000000000\0", "ascii").copy(header, 136); // mtime
24
+ Buffer.from(" ", "ascii").copy(header, 148); // checksum placeholder
25
+ header[156] = "0".charCodeAt(0);
26
+ Buffer.from("ustar\0", "ascii").copy(header, 257);
27
+ Buffer.from("00", "ascii").copy(header, 263);
28
+
29
+ let sum = 0;
30
+ for (let i = 0; i < 512; i += 1) sum += header[i] ?? 0;
31
+ const checksum = sum.toString(8).padStart(6, "0");
32
+ Buffer.from(`${checksum}\0 `, "ascii").copy(header, 148);
33
+
34
+ const data = Buffer.from(content, "utf-8");
35
+ const paddedSize = Math.ceil(data.length / 512) * 512;
36
+ const padded = Buffer.alloc(paddedSize, 0);
37
+ data.copy(padded);
38
+
39
+ return Buffer.concat([header, padded]);
40
+ }
41
+
42
+ function makeTar(entries: Array<{ name: string; content: string }>): Buffer {
43
+ const body = entries.map((entry) => makeTarEntry(entry.name, entry.content));
44
+ return Buffer.concat([...body, Buffer.alloc(1024, 0)]);
45
+ }
46
+
47
+ beforeEach(() => {
48
+ tempDir = join(
49
+ tmpdir(),
50
+ `skills-extract-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
51
+ );
52
+ mkdirSync(tempDir, { recursive: true });
53
+ });
54
+
55
+ afterEach(() => {
56
+ rmSync(tempDir, { recursive: true, force: true });
57
+ });
58
+
59
+ describe("extractTarToDir", () => {
60
+ test("extracts valid files and detects SKILL.md", () => {
61
+ const tar = makeTar([
62
+ { name: "SKILL.md", content: "# demo\n" },
63
+ { name: "scripts/run.sh", content: "echo ok\n" },
64
+ ]);
65
+
66
+ const foundSkillMd = extractTarToDir(tar, tempDir);
67
+
68
+ expect(foundSkillMd).toBe(true);
69
+ expect(readFileSync(join(tempDir, "SKILL.md"), "utf-8")).toBe("# demo\n");
70
+ expect(readFileSync(join(tempDir, "scripts", "run.sh"), "utf-8")).toBe(
71
+ "echo ok\n",
72
+ );
73
+ });
74
+
75
+ test("rejects traversal and absolute archive paths", () => {
76
+ const tar = makeTar([
77
+ { name: "SKILL.md", content: "# demo\n" },
78
+ { name: "../../escape.txt", content: "nope\n" },
79
+ { name: "..\\..\\win-escape.txt", content: "nope\n" },
80
+ { name: "/absolute.txt", content: "nope\n" },
81
+ { name: "C:/windows.txt", content: "nope\n" },
82
+ ]);
83
+
84
+ const foundSkillMd = extractTarToDir(tar, tempDir);
85
+
86
+ expect(foundSkillMd).toBe(true);
87
+ expect(existsSync(join(tempDir, "escape.txt"))).toBe(false);
88
+ expect(existsSync(join(tempDir, "win-escape.txt"))).toBe(false);
89
+ expect(existsSync(join(tempDir, "absolute.txt"))).toBe(false);
90
+ expect(existsSync(join(tempDir, "windows.txt"))).toBe(false);
91
+ expect(readFileSync(join(tempDir, "SKILL.md"), "utf-8")).toBe("# demo\n");
92
+ });
93
+ });
@@ -674,13 +674,13 @@ describe("ingress-dependent setup skills declare public-ingress", () => {
674
674
  expect(includes).toContain("public-ingress");
675
675
  });
676
676
 
677
- test("slack-oauth-setup includes browser", () => {
677
+ test("slack-oauth-setup includes collaborative-oauth-flow", () => {
678
678
  const includes = readSkillIncludes(
679
679
  FIRST_PARTY_SKILLS_DIR,
680
680
  "slack-oauth-setup",
681
681
  );
682
682
  expect(includes).toBeDefined();
683
- expect(includes).toContain("browser");
683
+ expect(includes).toContain("collaborative-oauth-flow");
684
684
  });
685
685
  });
686
686
 
@@ -0,0 +1,451 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type {
4
+ AuditResponse,
5
+ SkillAuditData,
6
+ SkillsShSearchResult,
7
+ } from "../skills/skillssh-registry.js";
8
+ import {
9
+ fetchSkillAudits,
10
+ fetchSkillFromGitHub,
11
+ formatAuditBadges,
12
+ providerDisplayName,
13
+ resolveSkillSource,
14
+ riskToDisplay,
15
+ searchSkillsRegistry,
16
+ validateSkillSlug,
17
+ } from "../skills/skillssh-registry.js";
18
+
19
+ // ─── Fetch mock helpers ──────────────────────────────────────────────────────
20
+
21
+ const originalFetch = globalThis.fetch;
22
+
23
+ let mockFetchImpl: (url: string | URL | Request) => Promise<Response>;
24
+
25
+ beforeEach(() => {
26
+ mockFetchImpl = () =>
27
+ Promise.resolve(new Response("not mocked", { status: 500 }));
28
+ globalThis.fetch = mock((input: string | URL | Request) =>
29
+ mockFetchImpl(typeof input === "string" ? input : input.toString()),
30
+ ) as unknown as typeof fetch;
31
+ });
32
+
33
+ afterEach(() => {
34
+ globalThis.fetch = originalFetch;
35
+ });
36
+
37
+ // ─── searchSkillsRegistry ────────────────────────────────────────────────────
38
+
39
+ describe("searchSkillsRegistry", () => {
40
+ test("sends correct query parameters and returns results", async () => {
41
+ const mockResults: SkillsShSearchResult[] = [
42
+ {
43
+ id: "vercel-labs/agent-skills/vercel-react-best-practices",
44
+ skillId: "vercel-react-best-practices",
45
+ name: "Vercel React Best Practices",
46
+ installs: 1200,
47
+ source: "vercel-labs/agent-skills",
48
+ },
49
+ ];
50
+
51
+ mockFetchImpl = (url: string | URL | Request) => {
52
+ const urlStr = url.toString();
53
+ expect(urlStr).toContain("skills.sh/api/search");
54
+ expect(urlStr).toContain("q=react");
55
+ expect(urlStr).toContain("limit=5");
56
+ return Promise.resolve(
57
+ new Response(JSON.stringify({ skills: mockResults }), {
58
+ status: 200,
59
+ headers: { "Content-Type": "application/json" },
60
+ }),
61
+ );
62
+ };
63
+
64
+ const results = await searchSkillsRegistry("react", 5);
65
+ expect(results).toEqual(mockResults);
66
+ });
67
+
68
+ test("omits limit parameter when not provided", async () => {
69
+ mockFetchImpl = (url: string | URL | Request) => {
70
+ const urlStr = url.toString();
71
+ expect(urlStr).not.toContain("limit=");
72
+ return Promise.resolve(
73
+ new Response(JSON.stringify({ skills: [] }), {
74
+ status: 200,
75
+ headers: { "Content-Type": "application/json" },
76
+ }),
77
+ );
78
+ };
79
+
80
+ const results = await searchSkillsRegistry("test");
81
+ expect(results).toEqual([]);
82
+ });
83
+
84
+ test("throws on non-OK response", async () => {
85
+ mockFetchImpl = () =>
86
+ Promise.resolve(new Response("Not Found", { status: 404 }));
87
+
88
+ await expect(searchSkillsRegistry("bad-query")).rejects.toThrow(
89
+ "skills.sh search failed: HTTP 404",
90
+ );
91
+ });
92
+ });
93
+
94
+ // ─── fetchSkillAudits ────────────────────────────────────────────────────────
95
+
96
+ describe("fetchSkillAudits", () => {
97
+ test("sends correct parameters and returns audit data", async () => {
98
+ const mockAudits: AuditResponse = {
99
+ "vercel-react-best-practices": {
100
+ ath: {
101
+ risk: "safe",
102
+ alerts: 0,
103
+ score: 100,
104
+ analyzedAt: "2025-01-15T00:00:00Z",
105
+ },
106
+ socket: {
107
+ risk: "low",
108
+ alerts: 1,
109
+ score: 95,
110
+ analyzedAt: "2025-01-15T00:00:00Z",
111
+ },
112
+ },
113
+ };
114
+
115
+ mockFetchImpl = (url: string | URL | Request) => {
116
+ const urlStr = url.toString();
117
+ expect(urlStr).toContain("add-skill.vercel.sh/audit");
118
+ expect(urlStr).toContain("source=vercel-labs%2Fagent-skills");
119
+ expect(urlStr).toContain(
120
+ "skills=vercel-react-best-practices%2Canother-skill",
121
+ );
122
+ return Promise.resolve(
123
+ new Response(JSON.stringify(mockAudits), {
124
+ status: 200,
125
+ headers: { "Content-Type": "application/json" },
126
+ }),
127
+ );
128
+ };
129
+
130
+ const audits = await fetchSkillAudits("vercel-labs/agent-skills", [
131
+ "vercel-react-best-practices",
132
+ "another-skill",
133
+ ]);
134
+ expect(audits).toEqual(mockAudits);
135
+ });
136
+
137
+ test("returns empty object for empty slugs list", async () => {
138
+ const audits = await fetchSkillAudits("some/source", []);
139
+ expect(audits).toEqual({});
140
+ // fetch should not have been called
141
+ expect(globalThis.fetch).not.toHaveBeenCalled();
142
+ });
143
+
144
+ test("throws on non-OK response", async () => {
145
+ mockFetchImpl = () =>
146
+ Promise.resolve(new Response("Internal Server Error", { status: 500 }));
147
+
148
+ await expect(fetchSkillAudits("some/source", ["slug"])).rejects.toThrow(
149
+ "Audit fetch failed: HTTP 500",
150
+ );
151
+ });
152
+ });
153
+
154
+ // ─── Display helpers ─────────────────────────────────────────────────────────
155
+
156
+ describe("riskToDisplay", () => {
157
+ test("maps risk levels correctly", () => {
158
+ expect(riskToDisplay("safe")).toBe("PASS");
159
+ expect(riskToDisplay("low")).toBe("PASS");
160
+ expect(riskToDisplay("medium")).toBe("WARN");
161
+ expect(riskToDisplay("high")).toBe("FAIL");
162
+ expect(riskToDisplay("critical")).toBe("FAIL");
163
+ expect(riskToDisplay("unknown")).toBe("?");
164
+ });
165
+ });
166
+
167
+ describe("providerDisplayName", () => {
168
+ test("maps known providers", () => {
169
+ expect(providerDisplayName("ath")).toBe("ATH");
170
+ expect(providerDisplayName("socket")).toBe("Socket");
171
+ expect(providerDisplayName("snyk")).toBe("Snyk");
172
+ });
173
+
174
+ test("returns raw name for unknown providers", () => {
175
+ expect(providerDisplayName("custom-auditor")).toBe("custom-auditor");
176
+ });
177
+ });
178
+
179
+ describe("formatAuditBadges", () => {
180
+ test("formats multiple providers as badges", () => {
181
+ const auditData: SkillAuditData = {
182
+ ath: { risk: "safe", analyzedAt: "2025-01-15T00:00:00Z" },
183
+ socket: { risk: "safe", analyzedAt: "2025-01-15T00:00:00Z" },
184
+ snyk: { risk: "medium", analyzedAt: "2025-01-15T00:00:00Z" },
185
+ };
186
+ expect(formatAuditBadges(auditData)).toBe(
187
+ "Security: [ATH:PASS] [Socket:PASS] [Snyk:WARN]",
188
+ );
189
+ });
190
+
191
+ test("returns fallback message when no providers present", () => {
192
+ expect(formatAuditBadges({})).toBe("Security: no audit data");
193
+ });
194
+
195
+ test("handles single provider", () => {
196
+ const auditData: SkillAuditData = {
197
+ ath: { risk: "critical", analyzedAt: "2025-01-15T00:00:00Z" },
198
+ };
199
+ expect(formatAuditBadges(auditData)).toBe("Security: [ATH:FAIL]");
200
+ });
201
+ });
202
+
203
+ // ─── resolveSkillSource ─────────────────────────────────────────────────────
204
+
205
+ describe("resolveSkillSource", () => {
206
+ test("parses owner/repo@skill-name format", () => {
207
+ const result = resolveSkillSource("vercel-labs/skills@find-skills");
208
+ expect(result).toEqual({
209
+ owner: "vercel-labs",
210
+ repo: "skills",
211
+ skillSlug: "find-skills",
212
+ });
213
+ });
214
+
215
+ test("parses owner/repo/skill-name format", () => {
216
+ const result = resolveSkillSource("vercel-labs/skills/find-skills");
217
+ expect(result).toEqual({
218
+ owner: "vercel-labs",
219
+ repo: "skills",
220
+ skillSlug: "find-skills",
221
+ });
222
+ });
223
+
224
+ test("parses full GitHub URL with main branch", () => {
225
+ const result = resolveSkillSource(
226
+ "https://github.com/vercel-labs/skills/tree/main/skills/find-skills",
227
+ );
228
+ expect(result).toEqual({
229
+ owner: "vercel-labs",
230
+ repo: "skills",
231
+ skillSlug: "find-skills",
232
+ ref: "main",
233
+ });
234
+ });
235
+
236
+ test("parses full GitHub URL with non-main branch", () => {
237
+ const result = resolveSkillSource(
238
+ "https://github.com/some-org/repo/tree/develop/skills/my-skill",
239
+ );
240
+ expect(result).toEqual({
241
+ owner: "some-org",
242
+ repo: "repo",
243
+ skillSlug: "my-skill",
244
+ ref: "develop",
245
+ });
246
+ });
247
+
248
+ test("parses GitHub URL with trailing slash", () => {
249
+ const result = resolveSkillSource(
250
+ "https://github.com/owner/repo/tree/main/skills/skill-name/",
251
+ );
252
+ expect(result).toEqual({
253
+ owner: "owner",
254
+ repo: "repo",
255
+ skillSlug: "skill-name",
256
+ ref: "main",
257
+ });
258
+ });
259
+
260
+ test("throws on bare skill name (no owner/repo)", () => {
261
+ expect(() => resolveSkillSource("find-skills")).toThrow(
262
+ 'Invalid skill source "find-skills"',
263
+ );
264
+ });
265
+
266
+ test("throws on empty string", () => {
267
+ expect(() => resolveSkillSource("")).toThrow('Invalid skill source ""');
268
+ });
269
+
270
+ test("throws on owner-only format", () => {
271
+ expect(() => resolveSkillSource("vercel-labs")).toThrow(
272
+ 'Invalid skill source "vercel-labs"',
273
+ );
274
+ });
275
+
276
+ test("throws on owner/repo without skill", () => {
277
+ expect(() => resolveSkillSource("vercel-labs/skills")).toThrow(
278
+ 'Invalid skill source "vercel-labs/skills"',
279
+ );
280
+ });
281
+
282
+ test("rejects path traversal in @ format slug", () => {
283
+ expect(() => resolveSkillSource("owner/repo@../../malicious")).toThrow(
284
+ 'Invalid skill source "owner/repo@../../malicious"',
285
+ );
286
+ });
287
+
288
+ test("rejects uppercase slug in @ format", () => {
289
+ expect(() => resolveSkillSource("owner/repo@BadSlug")).toThrow(
290
+ 'Invalid skill source "owner/repo@BadSlug"',
291
+ );
292
+ });
293
+ });
294
+
295
+ // ─── validateSkillSlug ──────────────────────────────────────────────────────
296
+
297
+ describe("validateSkillSlug", () => {
298
+ test("accepts valid slugs", () => {
299
+ expect(() => validateSkillSlug("my-skill")).not.toThrow();
300
+ expect(() => validateSkillSlug("skill123")).not.toThrow();
301
+ expect(() => validateSkillSlug("my.skill")).not.toThrow();
302
+ expect(() => validateSkillSlug("my_skill")).not.toThrow();
303
+ });
304
+
305
+ test("rejects path traversal characters", () => {
306
+ expect(() => validateSkillSlug("../../malicious")).toThrow(
307
+ "path traversal",
308
+ );
309
+ expect(() => validateSkillSlug("foo/bar")).toThrow("path traversal");
310
+ expect(() => validateSkillSlug("foo\\bar")).toThrow("path traversal");
311
+ });
312
+
313
+ test("rejects slugs starting with special chars", () => {
314
+ expect(() => validateSkillSlug(".hidden")).toThrow();
315
+ expect(() => validateSkillSlug("-dash")).toThrow();
316
+ });
317
+
318
+ test("rejects empty input", () => {
319
+ expect(() => validateSkillSlug("")).toThrow("Skill slug is required");
320
+ });
321
+ });
322
+
323
+ // ─── fetchSkillFromGitHub ───────────────────────────────────────────────────
324
+
325
+ describe("fetchSkillFromGitHub", () => {
326
+ test("fetches from conventional skills/<slug>/ path", async () => {
327
+ mockFetchImpl = (url: string | URL | Request) => {
328
+ const urlStr = url.toString();
329
+ // Probe request for skills/my-skill
330
+ if (urlStr.includes("/contents/skills/my-skill")) {
331
+ return Promise.resolve(
332
+ new Response(
333
+ JSON.stringify([
334
+ {
335
+ name: "SKILL.md",
336
+ type: "file",
337
+ download_url: "https://raw.example.com/SKILL.md",
338
+ },
339
+ ]),
340
+ { status: 200, headers: { "Content-Type": "application/json" } },
341
+ ),
342
+ );
343
+ }
344
+ // File download
345
+ if (urlStr.includes("raw.example.com/SKILL.md")) {
346
+ return Promise.resolve(new Response("# My Skill", { status: 200 }));
347
+ }
348
+ return Promise.resolve(new Response("not found", { status: 404 }));
349
+ };
350
+
351
+ const files = await fetchSkillFromGitHub("owner", "repo", "my-skill");
352
+ expect(files["SKILL.md"]).toBe("# My Skill");
353
+ });
354
+
355
+ test("falls back to tree search when skills/<slug>/ returns 404", async () => {
356
+ mockFetchImpl = (url: string | URL | Request) => {
357
+ const urlStr = url.toString();
358
+ // Probe for conventional path returns 404
359
+ if (urlStr.includes("/contents/skills/csv")) {
360
+ return Promise.resolve(new Response("Not Found", { status: 404 }));
361
+ }
362
+ // Tree search returns the skill at a non-standard path
363
+ if (urlStr.includes("/git/trees/")) {
364
+ return Promise.resolve(
365
+ new Response(
366
+ JSON.stringify({
367
+ tree: [
368
+ { path: "examples/skills/csv/SKILL.md", type: "blob" },
369
+ { path: "examples/skills/csv/scripts/filter.sh", type: "blob" },
370
+ { path: "README.md", type: "blob" },
371
+ ],
372
+ }),
373
+ { status: 200, headers: { "Content-Type": "application/json" } },
374
+ ),
375
+ );
376
+ }
377
+ // Subdirectory listing (must precede parent path check — both use
378
+ // .includes() and the parent path is a prefix of this one)
379
+ if (urlStr.includes("/contents/examples/skills/csv/scripts")) {
380
+ return Promise.resolve(
381
+ new Response(
382
+ JSON.stringify([
383
+ {
384
+ name: "filter.sh",
385
+ type: "file",
386
+ download_url: "https://raw.example.com/filter.sh",
387
+ },
388
+ ]),
389
+ { status: 200, headers: { "Content-Type": "application/json" } },
390
+ ),
391
+ );
392
+ }
393
+ // Contents API for the discovered path
394
+ if (urlStr.includes("/contents/examples/skills/csv")) {
395
+ return Promise.resolve(
396
+ new Response(
397
+ JSON.stringify([
398
+ {
399
+ name: "SKILL.md",
400
+ type: "file",
401
+ download_url: "https://raw.example.com/SKILL.md",
402
+ },
403
+ {
404
+ name: "scripts",
405
+ type: "dir",
406
+ download_url: null,
407
+ },
408
+ ]),
409
+ { status: 200, headers: { "Content-Type": "application/json" } },
410
+ ),
411
+ );
412
+ }
413
+ // File downloads
414
+ if (urlStr.includes("raw.example.com/SKILL.md")) {
415
+ return Promise.resolve(new Response("# CSV Skill", { status: 200 }));
416
+ }
417
+ if (urlStr.includes("raw.example.com/filter.sh")) {
418
+ return Promise.resolve(
419
+ new Response("#!/bin/bash\necho filter", { status: 200 }),
420
+ );
421
+ }
422
+ return Promise.resolve(new Response("not found", { status: 404 }));
423
+ };
424
+
425
+ const files = await fetchSkillFromGitHub("vercel-labs", "bash-tool", "csv");
426
+ expect(files["SKILL.md"]).toBe("# CSV Skill");
427
+ expect(files["scripts/filter.sh"]).toBe("#!/bin/bash\necho filter");
428
+ });
429
+
430
+ test("throws when skill not found in tree either", async () => {
431
+ mockFetchImpl = (url: string | URL | Request) => {
432
+ const urlStr = url.toString();
433
+ if (urlStr.includes("/contents/skills/missing")) {
434
+ return Promise.resolve(new Response("Not Found", { status: 404 }));
435
+ }
436
+ if (urlStr.includes("/git/trees/")) {
437
+ return Promise.resolve(
438
+ new Response(
439
+ JSON.stringify({ tree: [{ path: "README.md", type: "blob" }] }),
440
+ { status: 200, headers: { "Content-Type": "application/json" } },
441
+ ),
442
+ );
443
+ }
444
+ return Promise.resolve(new Response("not found", { status: 404 }));
445
+ };
446
+
447
+ await expect(
448
+ fetchSkillFromGitHub("owner", "repo", "missing"),
449
+ ).rejects.toThrow("Searched skills/missing/ and the full repo tree");
450
+ });
451
+ });
@@ -101,6 +101,8 @@ mock.module("../security/secure-keys.js", () => {
101
101
  };
102
102
  return {
103
103
  getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
104
+ getSecureKeyAsync: async (account: string) =>
105
+ secureKeyStore[account] ?? undefined,
104
106
  setSecureKey: syncSet,
105
107
  deleteSecureKey: syncDelete,
106
108
  setSecureKeyAsync: async (account: string, value: string) =>
@@ -230,15 +232,15 @@ describe("Slack channel config handler", () => {
230
232
  globalThis.fetch = originalFetch;
231
233
  });
232
234
 
233
- test("GET returns correct shape when not configured", () => {
234
- const result = getSlackChannelConfig();
235
+ test("GET returns correct shape when not configured", async () => {
236
+ const result = await getSlackChannelConfig();
235
237
  expect(result.success).toBe(true);
236
238
  expect(result.hasBotToken).toBe(false);
237
239
  expect(result.hasAppToken).toBe(false);
238
240
  expect(result.connected).toBe(false);
239
241
  });
240
242
 
241
- test("GET returns connected: true when oauth_connection is active and both keys exist", () => {
243
+ test("GET returns connected: true when oauth_connection is active and both keys exist", async () => {
242
244
  oauthConnectionStore["slack_channel"] = {
243
245
  id: "conn-slack",
244
246
  status: "active",
@@ -246,14 +248,14 @@ describe("Slack channel config handler", () => {
246
248
  secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
247
249
  secureKeyStore[credentialKey("slack_channel", "app_token")] = "xapp-test";
248
250
 
249
- const result = getSlackChannelConfig();
251
+ const result = await getSlackChannelConfig();
250
252
  expect(result.success).toBe(true);
251
253
  expect(result.hasBotToken).toBe(true);
252
254
  expect(result.hasAppToken).toBe(true);
253
255
  expect(result.connected).toBe(true);
254
256
  });
255
257
 
256
- test("GET reports per-field token presence independently of connection row", () => {
258
+ test("GET reports per-field token presence independently of connection row", async () => {
257
259
  // Only bot_token in keychain, no app_token, but connection row exists
258
260
  oauthConnectionStore["slack_channel"] = {
259
261
  id: "conn-slack",
@@ -261,7 +263,7 @@ describe("Slack channel config handler", () => {
261
263
  };
262
264
  secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
263
265
 
264
- const result = getSlackChannelConfig();
266
+ const result = await getSlackChannelConfig();
265
267
  expect(result.success).toBe(true);
266
268
  expect(result.hasBotToken).toBe(true);
267
269
  expect(result.hasAppToken).toBe(false);
@@ -269,7 +271,7 @@ describe("Slack channel config handler", () => {
269
271
  expect(result.connected).toBe(false);
270
272
  });
271
273
 
272
- test("GET returns metadata from config when available", () => {
274
+ test("GET returns metadata from config when available", async () => {
273
275
  oauthConnectionStore["slack_channel"] = {
274
276
  id: "conn-slack",
275
277
  status: "active",
@@ -285,7 +287,7 @@ describe("Slack channel config handler", () => {
285
287
  },
286
288
  };
287
289
 
288
- const result = getSlackChannelConfig();
290
+ const result = await getSlackChannelConfig();
289
291
  expect(result.teamId).toBe("T123");
290
292
  expect(result.teamName).toBe("TestTeam");
291
293
  expect(result.botUserId).toBe("U_BOT");
@@ -777,6 +777,7 @@ describe("Trust Store", () => {
777
777
  "computer_use_click",
778
778
  "computer_use_drag",
779
779
  "computer_use_key",
780
+ "computer_use_observe",
780
781
  "computer_use_open_app",
781
782
  "computer_use_run_applescript",
782
783
  "computer_use_scroll",
@@ -900,6 +901,20 @@ describe("Trust Store", () => {
900
901
  );
901
902
  });
902
903
 
904
+ test("findHighestPriorityRule matches default ask for computer_use_observe", () => {
905
+ const match = findHighestPriorityRule(
906
+ "computer_use_observe",
907
+ ["computer_use_observe:"],
908
+ "/tmp",
909
+ );
910
+ expect(match).not.toBeNull();
911
+ expect(match!.id).toBe("default:ask-computer_use_observe-global");
912
+ expect(match!.decision).toBe("ask");
913
+ expect(match!.priority).toBe(
914
+ DEFAULT_PRIORITY_BY_ID.get("default:ask-computer_use_observe-global")!,
915
+ );
916
+ });
917
+
903
918
  test("bootstrap delete rule matches only when workingDir is the workspace dir", () => {
904
919
  const workspaceDir = join(testDir, "workspace");
905
920
  // Should match when workingDir is the workspace directory — the bootstrap