@vellumai/assistant 0.5.0 → 0.5.2

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 (347) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-queue.test.ts +36 -1
  37. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  38. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  39. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  41. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  42. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  43. package/src/__tests__/conversation-store.test.ts +24 -21
  44. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  45. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  46. package/src/__tests__/conversation-title-service.test.ts +137 -0
  47. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  48. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  49. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  51. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  52. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  53. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  54. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  55. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  56. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  57. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  58. package/src/__tests__/diagnostics-export.test.ts +70 -1
  59. package/src/__tests__/filesystem-tools.test.ts +4 -2
  60. package/src/__tests__/first-greeting.test.ts +80 -0
  61. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  63. package/src/__tests__/history-repair.test.ts +103 -10
  64. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  65. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  66. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  67. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  68. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  69. package/src/__tests__/media-generate-image.test.ts +47 -94
  70. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  71. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  72. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  73. package/src/__tests__/migration-export-http.test.ts +3 -1
  74. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  75. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  76. package/src/__tests__/mime-builder.test.ts +3 -2
  77. package/src/__tests__/non-member-access-request.test.ts +12 -1
  78. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  80. package/src/__tests__/oauth-store.test.ts +115 -0
  81. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  82. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  83. package/src/__tests__/recording-handler.test.ts +17 -0
  84. package/src/__tests__/registry.test.ts +3 -8
  85. package/src/__tests__/relay-server.test.ts +1 -1
  86. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  87. package/src/__tests__/schema-transforms.test.ts +165 -5
  88. package/src/__tests__/server-history-render.test.ts +2 -2
  89. package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
  90. package/src/__tests__/skill-feature-flags.test.ts +13 -13
  91. package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
  92. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  93. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  94. package/src/__tests__/starter-task-flow.test.ts +1 -0
  95. package/src/__tests__/suggestion-routes.test.ts +443 -0
  96. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  97. package/src/__tests__/swarm-recursion.test.ts +1 -0
  98. package/src/__tests__/swarm-tool.test.ts +1 -0
  99. package/src/__tests__/system-prompt.test.ts +8 -0
  100. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  101. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  102. package/src/__tests__/top-level-renderer.test.ts +22 -0
  103. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  104. package/src/__tests__/web-fetch.test.ts +6 -2
  105. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  106. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  107. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  108. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  109. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  110. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  111. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  112. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  113. package/src/agent/attachments.ts +27 -1
  114. package/src/agent/loop.ts +29 -1
  115. package/src/avatar/traits-png-sync.ts +80 -25
  116. package/src/bundler/app-bundler.ts +4 -4
  117. package/src/calls/call-domain.ts +1 -0
  118. package/src/calls/voice-session-bridge.ts +1 -0
  119. package/src/cli/commands/auth.ts +92 -0
  120. package/src/cli/commands/avatar.ts +7 -6
  121. package/src/cli/commands/config.ts +2 -0
  122. package/src/cli/commands/oauth/providers.ts +29 -0
  123. package/src/cli/program.ts +12 -0
  124. package/src/cli.ts +15 -48
  125. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  126. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  127. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  128. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  129. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  130. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  131. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  135. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  136. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  137. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  139. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  140. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  141. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  142. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  143. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  144. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  145. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  146. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  148. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  149. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  150. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  151. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  152. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  153. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  154. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  155. package/src/config/bundled-tool-registry.ts +2 -14
  156. package/src/config/feature-flag-registry.json +16 -0
  157. package/src/config/loader.ts +64 -0
  158. package/src/config/raw-config-utils.ts +30 -0
  159. package/src/config/schema-utils.ts +28 -7
  160. package/src/config/schema.ts +8 -0
  161. package/src/config/schemas/elevenlabs.ts +18 -0
  162. package/src/config/schemas/memory-lifecycle.ts +4 -2
  163. package/src/config/schemas/memory-storage.ts +1 -1
  164. package/src/config/schemas/services.ts +8 -6
  165. package/src/contacts/contact-store.ts +13 -6
  166. package/src/contacts/contacts-write.ts +0 -1
  167. package/src/context/window-manager.ts +13 -2
  168. package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
  169. package/src/daemon/conversation-agent-loop.ts +56 -19
  170. package/src/daemon/conversation-attachments.ts +18 -36
  171. package/src/daemon/conversation-error.ts +2 -1
  172. package/src/daemon/conversation-history.ts +18 -4
  173. package/src/daemon/conversation-lifecycle.ts +39 -15
  174. package/src/daemon/conversation-messaging.ts +70 -26
  175. package/src/daemon/conversation-process.ts +58 -34
  176. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  177. package/src/daemon/conversation-slash.ts +121 -256
  178. package/src/daemon/conversation-surfaces.ts +143 -20
  179. package/src/daemon/conversation-tool-setup.ts +0 -6
  180. package/src/daemon/conversation-workspace.ts +21 -1
  181. package/src/daemon/conversation.ts +51 -29
  182. package/src/daemon/first-greeting.ts +35 -0
  183. package/src/daemon/handlers/config-embeddings.ts +148 -0
  184. package/src/daemon/handlers/config-model.ts +71 -26
  185. package/src/daemon/handlers/conversations.ts +0 -23
  186. package/src/daemon/handlers/recording.ts +26 -21
  187. package/src/daemon/history-repair.ts +28 -8
  188. package/src/daemon/host-cu-proxy.ts +2 -2
  189. package/src/daemon/lifecycle.ts +106 -64
  190. package/src/daemon/message-protocol.ts +3 -0
  191. package/src/daemon/message-types/conversations.ts +19 -0
  192. package/src/daemon/message-types/messages.ts +1 -0
  193. package/src/daemon/message-types/shared.ts +2 -0
  194. package/src/daemon/message-types/surfaces.ts +2 -0
  195. package/src/daemon/message-types/upgrades.ts +23 -0
  196. package/src/daemon/server.ts +83 -12
  197. package/src/daemon/shutdown-handlers.ts +8 -5
  198. package/src/daemon/startup-error.ts +9 -0
  199. package/src/daemon/tool-side-effects.ts +11 -28
  200. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  201. package/src/instrument.ts +0 -4
  202. package/src/media/app-icon-generator.ts +2 -2
  203. package/src/memory/app-git-service.ts +28 -16
  204. package/src/memory/app-store.ts +230 -41
  205. package/src/memory/attachments-store.ts +558 -130
  206. package/src/memory/conversation-attention-store.ts +70 -0
  207. package/src/memory/conversation-crud.ts +442 -3
  208. package/src/memory/conversation-directories.ts +125 -0
  209. package/src/memory/conversation-disk-view.ts +390 -0
  210. package/src/memory/conversation-key-store.ts +17 -5
  211. package/src/memory/conversation-queries.ts +5 -1
  212. package/src/memory/conversation-title-service.ts +21 -49
  213. package/src/memory/db-init.ts +28 -0
  214. package/src/memory/embedding-backend.ts +42 -53
  215. package/src/memory/embedding-gemini.test.ts +4 -4
  216. package/src/memory/embedding-local.ts +1 -3
  217. package/src/memory/embedding-ollama.ts +1 -3
  218. package/src/memory/embedding-openai.ts +1 -3
  219. package/src/memory/indexer.ts +9 -7
  220. package/src/memory/items-extractor.ts +42 -13
  221. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  222. package/src/memory/job-handlers/embedding.test.ts +1 -4
  223. package/src/memory/llm-request-log-store.ts +100 -1
  224. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  225. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  226. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  227. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  228. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  229. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  230. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  231. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  232. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  233. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  234. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  235. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  236. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  237. package/src/memory/migrations/index.ts +7 -0
  238. package/src/memory/migrations/registry.ts +13 -0
  239. package/src/memory/retriever.test.ts +601 -2
  240. package/src/memory/retriever.ts +85 -9
  241. package/src/memory/schema/conversations.ts +6 -0
  242. package/src/memory/schema/infrastructure.ts +13 -7
  243. package/src/memory/schema/oauth.ts +6 -0
  244. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  245. package/src/notifications/copy-composer.ts +26 -0
  246. package/src/notifications/decision-engine.ts +14 -1
  247. package/src/notifications/emit-signal.ts +1 -1
  248. package/src/notifications/signal.ts +36 -0
  249. package/src/oauth/byo-connection.test.ts +1 -45
  250. package/src/oauth/byo-connection.ts +2 -8
  251. package/src/oauth/connect-orchestrator.ts +15 -11
  252. package/src/oauth/connection-resolver.test.ts +191 -0
  253. package/src/oauth/connection-resolver.ts +66 -38
  254. package/src/oauth/connection.ts +0 -1
  255. package/src/oauth/oauth-store.ts +97 -47
  256. package/src/oauth/platform-connection.test.ts +0 -1
  257. package/src/oauth/platform-connection.ts +11 -3
  258. package/src/oauth/seed-providers.ts +78 -3
  259. package/src/oauth/token-persistence.ts +16 -10
  260. package/src/permissions/checker.ts +62 -19
  261. package/src/prompts/system-prompt.ts +2 -0
  262. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  263. package/src/providers/anthropic/client.ts +8 -1
  264. package/src/providers/failover.ts +4 -1
  265. package/src/providers/gemini/client.ts +50 -0
  266. package/src/providers/model-catalog.ts +92 -0
  267. package/src/providers/model-intents.ts +29 -20
  268. package/src/providers/openai/client.ts +49 -0
  269. package/src/providers/types.ts +2 -0
  270. package/src/runtime/access-request-helper.ts +16 -7
  271. package/src/runtime/auth/credential-service.ts +3 -1
  272. package/src/runtime/auth/route-policy.ts +14 -1
  273. package/src/runtime/btw-sidechain.ts +101 -0
  274. package/src/runtime/channel-reply-delivery.ts +17 -1
  275. package/src/runtime/http-router.ts +3 -1
  276. package/src/runtime/http-server.ts +196 -141
  277. package/src/runtime/http-types.ts +1 -0
  278. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  279. package/src/runtime/routes/access-request-decision.ts +41 -0
  280. package/src/runtime/routes/app-management-routes.ts +6 -3
  281. package/src/runtime/routes/app-routes.ts +7 -3
  282. package/src/runtime/routes/approval-routes.ts +1 -0
  283. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  284. package/src/runtime/routes/attachment-routes.ts +45 -15
  285. package/src/runtime/routes/btw-routes.ts +21 -61
  286. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  287. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  288. package/src/runtime/routes/conversation-routes.ts +222 -28
  289. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  290. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  291. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  292. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  293. package/src/runtime/routes/log-export-routes.ts +3 -0
  294. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  295. package/src/runtime/routes/memory-item-routes.ts +4 -0
  296. package/src/runtime/routes/migration-routes.ts +4 -1
  297. package/src/runtime/routes/oauth-apps.ts +291 -0
  298. package/src/runtime/routes/secret-routes.ts +28 -1
  299. package/src/runtime/routes/settings-routes.ts +14 -0
  300. package/src/runtime/routes/trace-event-routes.ts +4 -1
  301. package/src/schedule/schedule-store.ts +9 -21
  302. package/src/security/secure-keys.ts +21 -0
  303. package/src/signals/bash.ts +1 -1
  304. package/src/swarm/backend-claude-code.ts +3 -6
  305. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  306. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  307. package/src/tools/AGENTS.md +6 -10
  308. package/src/tools/apps/executors.ts +17 -232
  309. package/src/tools/claude-code/claude-code.ts +2 -3
  310. package/src/tools/credentials/vault.ts +7 -12
  311. package/src/tools/host-filesystem/read.ts +13 -10
  312. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  313. package/src/tools/schedule/list.ts +2 -7
  314. package/src/tools/schema-transforms.ts +5 -0
  315. package/src/tools/shared/filesystem/format-diff.ts +4 -21
  316. package/src/tools/skills/execute.ts +1 -1
  317. package/src/tools/tool-manifest.ts +0 -6
  318. package/src/tools/ui-surface/definitions.ts +2 -2
  319. package/src/util/device-id.ts +28 -5
  320. package/src/util/platform.ts +6 -0
  321. package/src/util/pricing.ts +1 -0
  322. package/src/util/retry.ts +1 -3
  323. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  324. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  325. package/src/workspace/migrations/006-services-config.ts +5 -0
  326. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  327. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  328. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  329. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  330. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  331. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  332. package/src/workspace/migrations/registry.ts +10 -0
  333. package/src/workspace/top-level-renderer.ts +12 -0
  334. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  335. package/src/__tests__/asset-search-tool.test.ts +0 -536
  336. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  337. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  338. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  339. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  340. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  341. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  342. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  343. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  344. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  345. package/src/daemon/media-visibility-policy.ts +0 -59
  346. package/src/tools/assets/materialize.ts +0 -248
  347. package/src/tools/assets/search.ts +0 -400
@@ -15,7 +15,7 @@ import {
15
15
  type TurnChannelContext,
16
16
  type TurnInterfaceContext,
17
17
  } from "../channels/types.js";
18
- import { getAppsDir, listAppFiles } from "../memory/app-store.js";
18
+ import { getAppDirPath, listAppFiles } from "../memory/app-store.js";
19
19
  import type { Message } from "../providers/types.js";
20
20
  import type { ActorTrustContext } from "../runtime/actor-trust-resolver.js";
21
21
  import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
@@ -266,6 +266,8 @@ export interface ActiveSurfaceContext {
266
266
  /** When set, the surface is backed by a persisted app. */
267
267
  appId?: string;
268
268
  appName?: string;
269
+ /** Filesystem directory/slug for the app (used to construct file paths). */
270
+ appDirName?: string;
269
271
  appSchemaJson?: string;
270
272
  /** Additional pages keyed by filename (e.g. "settings.html" → HTML content). */
271
273
  appPages?: Record<string, string>;
@@ -297,19 +299,18 @@ export function injectActiveSurfaceContext(
297
299
 
298
300
  if (ctx.appId) {
299
301
  // ── App-backed surface ──
302
+ const slug = ctx.appDirName ?? ctx.appId;
300
303
  lines.push(
301
- `The user is viewing app "${ctx.appName ?? "Untitled"}" (app_id: "${ctx.appId}") in workspace mode.`,
304
+ `The user is viewing app "${ctx.appName ?? "Untitled"}" (app_id: "${ctx.appId}", slug: "${slug}") in workspace mode.`,
302
305
  "",
303
- 'PREREQUISITE: If `app_*` tools (e.g. `app_file_edit`, `app_file_write`) are not yet available, call `skill_load` with `id: "app-builder"` first to load them.',
306
+ 'PREREQUISITE: If `app_refresh` is not yet available, call `skill_load` with `id: "app-builder"` first to load it.',
304
307
  "",
305
308
  "RULES FOR WORKSPACE MODIFICATION:",
306
- `1. Use \`app_file_edit\` with app_id "${ctx.appId}" for surgical changes.`,
307
- " Provide old_string (exact match) and new_string (replacement).",
308
- ' Include a short `status` message describing what you\'re doing (e.g. "adding dark mode styles").',
309
- "2. Use `app_file_write` to create new files or fully rewrite files. Include `status`.",
310
- "3. Use `app_file_read` to read any file with line numbers before editing.",
311
- "4. Use `app_file_list` to see all files in the app.",
312
- "5. The surface refreshes automatically after file edits — do NOT call app_update, ui_show, or ui_update.",
309
+ `1. Use \`file_edit\` to make surgical changes to app files. The file path is \`~/.vellum/workspace/data/apps/${slug}/<path>\`.`,
310
+ "2. Use `file_write` to create new files or rewrite files.",
311
+ "3. Use `file_read` to read any file with line numbers before editing.",
312
+ "4. Use `bash ls` to see all files in the app directory.",
313
+ `5. Call \`app_refresh\` with app_id "${ctx.appId}" ONCE after all changes are complete.`,
313
314
  "6. NEVER respond with only text — the user expects a visual update.",
314
315
  "7. Make ONLY the changes the user requested. Preserve existing content/styling.",
315
316
  "8. Keep your text response to 1 brief sentence confirming what you changed.",
@@ -323,7 +324,7 @@ export function injectActiveSurfaceContext(
323
324
  for (const filePath of displayFiles) {
324
325
  let sizeLabel: string;
325
326
  try {
326
- const bytes = statSync(join(getAppsDir(), ctx.appId, filePath)).size;
327
+ const bytes = statSync(join(getAppDirPath(ctx.appId), filePath)).size;
327
328
  sizeLabel =
328
329
  bytes < 1000 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
329
330
  } catch {
@@ -634,6 +635,15 @@ export function buildTurnContextBlock(
634
635
  lines.push(`assistant_message_channel: ${assistant}`);
635
636
  lines.push(`conversation_origin_channel: ${origin}`);
636
637
  }
638
+ // Only inject response discretion for external channels (Slack, Telegram,
639
+ // etc.) where the assistant may receive thread replies not directed at it.
640
+ // The "vellum" channel is the web/desktop interface where every message is
641
+ // intentionally directed at the assistant.
642
+ if (user !== "vellum") {
643
+ lines.push(
644
+ `response_discretion: Not every message in a channel thread requires your response. If a message is clearly not directed at you (e.g. people talking among themselves, acknowledgements, reactions), output exactly <no_response/> as your entire reply to stay silent.`,
645
+ );
646
+ }
637
647
  }
638
648
 
639
649
  lines.push("</turn_context>");
@@ -1126,30 +1136,3 @@ export function applyRuntimeInjections(
1126
1136
 
1127
1137
  return result;
1128
1138
  }
1129
-
1130
- // ---------------------------------------------------------------------------
1131
- // Attachment detection
1132
- // ---------------------------------------------------------------------------
1133
-
1134
- /** Content block types that indicate user-uploaded attachments. */
1135
- const ATTACHMENT_CONTENT_TYPES = new Set(["image", "file"]);
1136
-
1137
- /**
1138
- * Scan conversation messages for user-uploaded attachment content blocks
1139
- * (image or file). Returns true as soon as any attachment is found.
1140
- *
1141
- * Used to set the one-way `hasAttachments` flag on Conversation so that asset
1142
- * tools (asset_search, asset_materialize) are included in tool definitions
1143
- * only when the conversation contains attachments.
1144
- */
1145
- export function messagesContainAttachments(messages: Message[]): boolean {
1146
- for (const message of messages) {
1147
- if (message.role !== "user") continue;
1148
- for (const block of message.content) {
1149
- if (ATTACHMENT_CONTENT_TYPES.has(block.type)) {
1150
- return true;
1151
- }
1152
- }
1153
- }
1154
- return false;
1155
- }
@@ -4,18 +4,14 @@ import { join } from "node:path";
4
4
 
5
5
  import QRCode from "qrcode";
6
6
 
7
+ import type { InterfaceId } from "../channels/types.js";
7
8
  import { getGatewayPort, getIngressPublicBaseUrl } from "../config/env.js";
8
- import { getConfig, loadRawConfig, saveRawConfig } from "../config/loader.js";
9
- import { setServiceField } from "../config/raw-config-utils.js";
10
- import {
11
- getConfiguredProviders,
12
- isProviderAvailable,
13
- } from "../providers/provider-availability.js";
14
- import { initializeProviders } from "../providers/registry.js";
9
+ import { getConfig } from "../config/loader.js";
10
+ import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
11
+ import { getConfiguredProviders } from "../providers/provider-availability.js";
15
12
  import { getLocalIPv4 } from "../util/network-info.js";
16
13
  import { getWorkspaceDir } from "../util/platform.js";
17
14
  import { silentlyWithLog } from "../util/silently.js";
18
- import { getAssistantName } from "./identity-helpers.js";
19
15
  import type { PairingStore } from "./pairing-store.js";
20
16
 
21
17
  export type SlashResolution =
@@ -45,160 +41,51 @@ export interface SlashContext {
45
41
  model: string;
46
42
  provider: string;
47
43
  estimatedCost: number;
44
+ userMessageInterface?: InterfaceId;
48
45
  }
49
46
 
50
- // ── /model command ───────────────────────────────────────────────────
51
-
52
- const AVAILABLE_MODELS = [
53
- "claude-opus-4-6",
54
- "claude-sonnet-4-6",
55
- "claude-haiku-4-5-20251001",
56
- ] as const;
57
-
58
- const MODEL_DISPLAY_NAMES: Record<string, string> = {
59
- "claude-opus-4-6": "Claude Opus 4.6",
60
- "claude-sonnet-4-6": "Claude Sonnet 4.6",
61
- "claude-haiku-4-5-20251001": "Claude Haiku 4.5",
62
- };
63
-
64
- const PROVIDER_MODEL_SHORTCUTS: Record<
65
- string,
66
- { provider: string; model: string; displayName: string }
67
- > = {
68
- // Anthropic
69
- opus: {
70
- provider: "anthropic",
71
- model: "claude-opus-4-6",
72
- displayName: "Claude Opus 4.6",
73
- },
74
- sonnet: {
75
- provider: "anthropic",
76
- model: "claude-sonnet-4-6",
77
- displayName: "Claude Sonnet 4.6",
78
- },
79
- haiku: {
80
- provider: "anthropic",
81
- model: "claude-haiku-4-5-20251001",
82
- displayName: "Claude Haiku 4.5",
83
- },
84
- "grok-beta": {
85
- provider: "openrouter",
86
- model: "x-ai/grok-4.20-beta",
87
- displayName: "Grok 4.20 Beta (OpenRouter)",
88
- },
89
- "grok-multi": {
90
- provider: "openrouter",
91
- model: "x-ai/grok-4.20-beta",
92
- displayName: "Grok 4.20 Beta (OpenRouter)",
93
- },
94
- };
95
-
96
- /** True when the trimmed content matches a provider shortcut like /opus, /gpt4, etc. */
97
- export function isProviderShortcut(content: string): boolean {
98
- const match = content.trim().match(/^\/([a-z0-9-]+)(\s|$)/i);
99
- if (!match) return false;
100
- return match[1].toLowerCase() in PROVIDER_MODEL_SHORTCUTS;
101
- }
102
-
103
- /** Reverse lookup: model ID → provider, derived from PROVIDER_MODEL_SHORTCUTS. */
104
- export const MODEL_TO_PROVIDER: Record<string, string> = Object.fromEntries(
105
- Object.values(PROVIDER_MODEL_SHORTCUTS).map(({ model, provider }) => [
106
- model,
107
- provider,
108
- ]),
109
- );
110
-
111
- /** Partial-match a user input like "opus", "sonnet", "haiku" to a full model ID. */
112
- function matchModel(input: string): string | undefined {
113
- const lower = input.toLowerCase().trim();
114
- // Exact match first
115
- const exact = AVAILABLE_MODELS.find((m) => m === lower);
116
- if (exact) return exact;
117
- // Partial match (e.g. "opus" → "claude-opus-4-6")
118
- return AVAILABLE_MODELS.find((m) => m.includes(lower));
119
- }
120
-
121
- async function resolveProviderModelCommand(
122
- content: string,
123
- ): Promise<SlashResolution | null> {
124
- const trimmed = content.trim();
125
- if (!trimmed.startsWith("/")) return null;
47
+ // ── Deprecated model-switching shortcuts ─────────────────────────────
126
48
 
127
- // Extract the command (e.g., "/gpt4" → "gpt4")
128
- const match = trimmed.match(/^\/([a-z0-9-]+)(\s|$)/i);
129
- if (!match) return null;
130
-
131
- const command = match[1].toLowerCase();
132
- const shortcut = PROVIDER_MODEL_SHORTCUTS[command];
133
- if (!shortcut) return null;
134
-
135
- const { provider, model, displayName } = shortcut;
136
- const config = getConfig();
137
- const name = getAssistantName();
138
-
139
- // Check if provider is available (secure key, env var, managed proxy, or no key needed)
140
- if (!(await isProviderAvailable(provider))) {
141
- return {
142
- kind: "unknown",
143
- message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`keys set ${provider} <your-key>\``,
144
- };
145
- }
146
-
147
- // Check if already using this provider+model
148
- if (
149
- config.services.inference.provider === provider &&
150
- config.services.inference.model === model
151
- ) {
152
- const alreadyMsg = name
153
- ? `${name} is already running on **${displayName}**.`
154
- : `Already using **${displayName}**.`;
155
- return {
156
- kind: "unknown",
157
- message: alreadyMsg,
158
- };
159
- }
160
-
161
- // Update config with both provider and model
162
- const raw = loadRawConfig();
163
- setServiceField(raw, "inference", "provider", provider);
164
- setServiceField(raw, "inference", "model", model);
165
- saveRawConfig(raw);
166
-
167
- // Re-initialize providers with new config
168
- const newConfig = getConfig();
169
- await initializeProviders(newConfig);
49
+ /**
50
+ * Former provider shortcut commands that switched models. These are now
51
+ * removed — model switching lives in Settings. We reject them explicitly
52
+ * so they don't fall through to the LLM as passthrough text.
53
+ */
54
+ const DEPRECATED_MODEL_SHORTCUTS = new Set([
55
+ "opus",
56
+ "sonnet",
57
+ "haiku",
58
+ "grok-beta",
59
+ "grok-multi",
60
+ ]);
170
61
 
171
- const switchedMsg = name
172
- ? `Switched ${name} to **${displayName}**. New conversations will use this model.`
173
- : `Switched to **${displayName}**. New conversations will use this model.`;
174
-
175
- return {
176
- kind: "unknown",
177
- message: switchedMsg,
178
- };
179
- }
62
+ // ── /models command ──────────────────────────────────────────────────
180
63
 
181
64
  async function resolveModelList(): Promise<SlashResolution> {
182
65
  const config = getConfig();
183
-
184
- // Build a set of providers that are usable (secure key, env var, or managed proxy).
185
66
  const configuredProviders = new Set<string>(await getConfiguredProviders());
186
67
 
187
68
  const lines = ["Available models:\n"];
188
69
 
189
- for (const [cmd, { provider, model, displayName }] of Object.entries(
190
- PROVIDER_MODEL_SHORTCUTS,
191
- )) {
70
+ for (const {
71
+ id: provider,
72
+ displayName: providerName,
73
+ models,
74
+ } of PROVIDER_CATALOG) {
192
75
  const hasKey = configuredProviders.has(provider);
193
- const isCurrent =
194
- config.services.inference.provider === provider &&
195
- config.services.inference.model === model;
196
76
  const status = hasKey ? "✓" : "✗";
197
- const current = isCurrent ? " **[current]**" : "";
198
- lines.push(`- **${displayName}** (/${cmd}) ${status}${current}`);
77
+ lines.push(`**${providerName}** ${status}`);
78
+ for (const { id, displayName } of models) {
79
+ const isCurrent =
80
+ config.services.inference.provider === provider &&
81
+ config.services.inference.model === id;
82
+ const current = isCurrent ? " **[current]**" : "";
83
+ lines.push(` - ${displayName} (\`${id}\`)${current}`);
84
+ }
85
+ lines.push("");
199
86
  }
200
87
 
201
- lines.push("\n✓ = API key configured, ✗ = not configured");
88
+ lines.push("✓ = API key configured, ✗ = not configured");
202
89
  lines.push("\nTip: Configure a provider with `keys set <provider> <key>`");
203
90
 
204
91
  return {
@@ -207,92 +94,6 @@ async function resolveModelList(): Promise<SlashResolution> {
207
94
  };
208
95
  }
209
96
 
210
- async function resolveModelCommand(
211
- content: string,
212
- ): Promise<SlashResolution | null> {
213
- const trimmed = content.trim();
214
- // Match /models → route to list
215
- if (trimmed === "/models") {
216
- return await resolveModelList();
217
- }
218
-
219
- if (!trimmed.startsWith("/model")) return null;
220
- // Ensure it's exactly "/model" or "/model " (not "/modelsomething")
221
- if (trimmed.length > 6 && trimmed[6] !== " ") return null;
222
-
223
- const args = trimmed.slice(6).trim();
224
- const name = getAssistantName();
225
-
226
- if (!args) {
227
- // Show current model
228
- const config = getConfig();
229
- const displayName =
230
- MODEL_DISPLAY_NAMES[config.services.inference.model] ??
231
- config.services.inference.model;
232
- const prefix = name ? `${name} is running on` : `Currently using`;
233
- return {
234
- kind: "unknown",
235
- message: `${prefix} **${displayName}** (\`${config.services.inference.model}\`).`,
236
- };
237
- }
238
-
239
- // Handle /model list
240
- if (args === "list") {
241
- return await resolveModelList();
242
- }
243
-
244
- // Try to match the model name
245
- const matched = matchModel(args);
246
- if (!matched) {
247
- const available = AVAILABLE_MODELS.map(
248
- (m) => `- **${MODEL_DISPLAY_NAMES[m]}** (\`${m}\`)`,
249
- ).join("\n");
250
- return {
251
- kind: "unknown",
252
- message: `Hmm, "${args}" doesn't match any available model. Here's what you can pick from:\n${available}`,
253
- };
254
- }
255
-
256
- // Check if already using this model
257
- const currentConfig = getConfig();
258
- if (currentConfig.services.inference.model === matched) {
259
- const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
260
- const alreadyMsg = name
261
- ? `${name} is already running on **${displayName}**.`
262
- : `Already on **${displayName}**.`;
263
- return {
264
- kind: "unknown",
265
- message: alreadyMsg,
266
- };
267
- }
268
-
269
- // Validate that Anthropic provider is available (secure key, env var, or managed proxy)
270
- if (!(await isProviderAvailable("anthropic"))) {
271
- const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
272
- return {
273
- kind: "unknown",
274
- message: `Cannot switch to ${displayName}. No API key configured for Anthropic.\n\nSet it with: \`keys set anthropic <your-key>\``,
275
- };
276
- }
277
-
278
- // Change model: save config and re-initialize providers
279
- const raw = loadRawConfig();
280
- setServiceField(raw, "inference", "provider", "anthropic"); // Ensure provider is set for Anthropic models
281
- setServiceField(raw, "inference", "model", matched);
282
- saveRawConfig(raw);
283
- const config = getConfig();
284
- await initializeProviders(config);
285
-
286
- const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
287
- const switchedMsg = name
288
- ? `Switched ${name} to **${displayName}**. New conversations will use this model.`
289
- : `Switched to **${displayName}**. New conversations will use this model.`;
290
- return {
291
- kind: "unknown",
292
- message: switchedMsg,
293
- };
294
- }
295
-
296
97
  function resolveStatusCommand(context: SlashContext): SlashResolution {
297
98
  const {
298
99
  inputTokens,
@@ -310,14 +111,13 @@ function resolveStatusCommand(context: SlashContext): SlashResolution {
310
111
  const filled = Math.round(pct / 5);
311
112
  const bar = "█".repeat(filled) + "░".repeat(20 - filled);
312
113
  const fmt = (n: number) => n.toLocaleString("en-US");
313
- const displayName = MODEL_DISPLAY_NAMES[model] ?? model;
314
114
 
315
115
  const lines = [
316
116
  "Conversation Status\n",
317
117
  `Context: ${bar} ${pct}% (${fmt(inputTokens)} / ${fmt(
318
118
  maxInputTokens,
319
119
  )} tokens)`,
320
- `Model: ${displayName} (${provider})`,
120
+ `Model: ${model} (${provider})`,
321
121
  `Messages: ${fmt(messageCount)}`,
322
122
  `Tokens: ${fmt(inputTokens)} in / ${fmt(outputTokens)} out`,
323
123
  `Cost: $${estimatedCost.toFixed(2)} (estimated)`,
@@ -326,28 +126,91 @@ function resolveStatusCommand(context: SlashContext): SlashResolution {
326
126
  return { kind: "unknown", message: lines.join("\n") };
327
127
  }
328
128
 
129
+ function resolveCommandsList(context?: SlashContext): string[] {
130
+ const fallbackLines = [
131
+ "/commands — List all available commands",
132
+ "/models — List all available models",
133
+ "/pair — Generate pairing info for connecting a mobile device",
134
+ ];
135
+ if (context) {
136
+ fallbackLines.push("/status — Show conversation status and context usage");
137
+ }
138
+
139
+ if (!context?.userMessageInterface) return fallbackLines;
140
+
141
+ if (context.userMessageInterface === "macos") {
142
+ return [
143
+ "/commands — List all available commands",
144
+ "/models — List all available models",
145
+ "/status — Show conversation status and context usage",
146
+ "/btw — Ask a side question while the assistant is working",
147
+ "/fork — Fork the current conversation into a new branch",
148
+ "/pair — Generate pairing info for connecting a mobile device",
149
+ ];
150
+ }
151
+
152
+ if (context.userMessageInterface === "ios") {
153
+ return [
154
+ "/commands — List all available commands",
155
+ "/models — List all available models",
156
+ "/status — Show conversation status and context usage",
157
+ "/btw — Ask a side question while the assistant is working",
158
+ "/fork — Fork the current conversation into a new branch",
159
+ ];
160
+ }
161
+
162
+ return [
163
+ "/commands — List all available commands",
164
+ "/models — List all available models",
165
+ "/status — Show conversation status and context usage",
166
+ "/btw — Ask a side question while the assistant is working",
167
+ ];
168
+ }
169
+
329
170
  /**
330
- * Resolve built-in slash commands (/model, /status, /commands, /pair, provider shortcuts).
171
+ * Resolve built-in slash commands (/models, /status, /commands, /pair).
331
172
  * Returns `unknown` with a deterministic message, or the (possibly rewritten) content.
332
173
  */
333
174
  export async function resolveSlash(
334
175
  content: string,
335
176
  context?: SlashContext,
336
177
  ): Promise<SlashResolution> {
337
- // Check provider shortcuts first (/gpt4, /opus, etc.)
338
- const providerResult = await resolveProviderModelCommand(content);
339
- if (providerResult) return providerResult;
178
+ // Handle deprecated model-switching commands direct users to Settings
179
+ const trimmed = content.trim();
180
+ if (
181
+ trimmed === "/model" ||
182
+ (trimmed.startsWith("/model ") && trimmed !== "/models")
183
+ ) {
184
+ return {
185
+ kind: "unknown",
186
+ message:
187
+ "The `/model` command has been removed. Use **Settings → Models & Services** to change your model and provider.",
188
+ };
189
+ }
340
190
 
341
- // Handle /model command
342
- const modelResult = await resolveModelCommand(content);
343
- if (modelResult) return modelResult;
191
+ // Reject deprecated provider shortcut commands (/opus, /sonnet, /haiku, etc.)
192
+ const shortcutMatch = trimmed.match(/^\/([a-z0-9-]+)(\s|$)/i);
193
+ if (
194
+ shortcutMatch &&
195
+ DEPRECATED_MODEL_SHORTCUTS.has(shortcutMatch[1].toLowerCase())
196
+ ) {
197
+ return {
198
+ kind: "unknown",
199
+ message: `The \`/${shortcutMatch[1]}\` shortcut has been removed. Use **Settings → Models & Services** to change your model and provider.`,
200
+ };
201
+ }
202
+
203
+ // Handle /models command (read-only listing)
204
+ if (trimmed === "/models") {
205
+ return await resolveModelList();
206
+ }
344
207
 
345
208
  // Handle /pair command
346
- const pairResult = resolvePairCommand(content);
209
+ const pairResult = resolvePairCommand(content, context);
347
210
  if (pairResult) return pairResult;
348
211
 
349
212
  // Handle /status command
350
- if (content.trim() === "/status") {
213
+ if (trimmed === "/status") {
351
214
  if (!context) {
352
215
  return {
353
216
  kind: "unknown",
@@ -358,19 +221,10 @@ export async function resolveSlash(
358
221
  }
359
222
 
360
223
  // Handle /commands command
361
- if (content.trim() === "/commands") {
362
- const lines = [
363
- "/commands — List all available commands",
364
- "/model — Show or switch the current model",
365
- "/models — List all available models",
366
- "/pair — Generate pairing info for connecting a mobile device",
367
- ];
368
- if (context) {
369
- lines.push("/status — Show conversation status and context usage");
370
- }
224
+ if (trimmed === "/commands") {
371
225
  return {
372
226
  kind: "unknown",
373
- message: lines.join("\n"),
227
+ message: resolveCommandsList(context).join("\n"),
374
228
  };
375
229
  }
376
230
 
@@ -406,9 +260,20 @@ async function savePairingQRCodePng(
406
260
  writeFileSync(qrPngPath, pngBuffer);
407
261
  }
408
262
 
409
- function resolvePairCommand(content: string): SlashResolution | null {
263
+ function resolvePairCommand(
264
+ content: string,
265
+ context?: SlashContext,
266
+ ): SlashResolution | null {
410
267
  if (content.trim() !== "/pair") return null;
411
268
 
269
+ if (context?.userMessageInterface && context.userMessageInterface !== "macos") {
270
+ return {
271
+ kind: "unknown",
272
+ message:
273
+ "The `/pair` command is only available in the macOS desktop app.",
274
+ };
275
+ }
276
+
412
277
  if (!pairingStoreRef) {
413
278
  return {
414
279
  kind: "unknown",