@vellumai/assistant 0.11.4 → 0.11.5-staging.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 (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { audienceForReader } from "../message-audience.js";
4
+
5
+ describe("audienceForReader", () => {
6
+ test("restricts a reply in a Slack channel to the named reader", () => {
7
+ expect(audienceForReader("slack", "C0ROOM", "U0READER")).toEqual({
8
+ kind: "oneReader",
9
+ userId: "U0READER",
10
+ });
11
+ });
12
+
13
+ test("leaves a Slack DM unrestricted", () => {
14
+ // Nobody else can read a DM, so restricting buys no privacy and costs
15
+ // durability: an ephemeral message does not survive a reload.
16
+ expect(audienceForReader("slack", "D0DIRECT", "U0READER")).toBeUndefined();
17
+ });
18
+
19
+ test("restricts a private group, which several readers share", () => {
20
+ // A `G` id is a shared room like a `C` one. Testing for a DM rather than
21
+ // for a channel id is what keeps this from leaking into the group.
22
+ expect(audienceForReader("slack", "G0PRIVATE", "U0READER")).toEqual({
23
+ kind: "oneReader",
24
+ userId: "U0READER",
25
+ });
26
+ });
27
+
28
+ test("leaves every other channel unrestricted", () => {
29
+ for (const channel of ["telegram", "discord", "whatsapp", "email"]) {
30
+ expect(
31
+ audienceForReader(channel, "C0ROOM", "U0READER"),
32
+ `${channel} has no way to show one reader a message in a shared room`,
33
+ ).toBeUndefined();
34
+ }
35
+ });
36
+
37
+ test("stays unrestricted when there is no reader to name", () => {
38
+ expect(audienceForReader("slack", "C0ROOM", undefined)).toBeUndefined();
39
+ expect(audienceForReader("slack", "C0ROOM", null)).toBeUndefined();
40
+ });
41
+
42
+ test("tolerates an absent channel or room rather than throwing", () => {
43
+ // Several callers hold values that were never narrowed, so the answer for
44
+ // an unknown channel has to be the safe one rather than an exception.
45
+ expect(audienceForReader(undefined, "C0ROOM", "U0READER")).toBeUndefined();
46
+ expect(audienceForReader(null, "C0ROOM", "U0READER")).toBeUndefined();
47
+ expect(audienceForReader("slack", null, "U0READER")).toBeUndefined();
48
+ });
49
+ });
@@ -17,9 +17,10 @@ describe("INTERFACE_IDS", () => {
17
17
  ).toBe(true);
18
18
  });
19
19
 
20
- test("still includes macos and other existing interfaces", () => {
20
+ test("includes desktop and existing interfaces", () => {
21
21
  for (const id of [
22
22
  "macos",
23
+ "windows",
23
24
  "ios",
24
25
  "cli",
25
26
  "telegram",
@@ -41,8 +42,9 @@ describe("INTERACTIVE_INTERFACES", () => {
41
42
  expect(INTERACTIVE_INTERFACES.has("chrome-extension" as never)).toBe(false);
42
43
  });
43
44
 
44
- test("still includes macos", () => {
45
+ test("includes desktop clients", () => {
45
46
  expect(INTERACTIVE_INTERFACES.has("macos")).toBe(true);
47
+ expect(INTERACTIVE_INTERFACES.has("windows")).toBe(true);
46
48
  });
47
49
  });
48
50
 
@@ -51,8 +53,9 @@ describe("isInterfaceId", () => {
51
53
  expect(isInterfaceId("chrome-extension")).toBe(true);
52
54
  });
53
55
 
54
- test("returns true for macos", () => {
56
+ test("returns true for desktop interfaces", () => {
55
57
  expect(isInterfaceId("macos")).toBe(true);
58
+ expect(isInterfaceId("windows")).toBe(true);
56
59
  });
57
60
 
58
61
  test("returns false for unknown interface", () => {
@@ -68,6 +71,7 @@ describe("parseInterfaceId", () => {
68
71
  test("returns canonical ID for valid interface", () => {
69
72
  expect(parseInterfaceId("web")).toBe("web");
70
73
  expect(parseInterfaceId("macos")).toBe("macos");
74
+ expect(parseInterfaceId("windows")).toBe("windows");
71
75
  });
72
76
 
73
77
  test("normalizes legacy 'vellum' alias to 'web'", () => {
@@ -103,6 +107,16 @@ describe("supportsHostProxy", () => {
103
107
  expect(supportsHostProxy("macos", "host_browser")).toBe(true);
104
108
  });
105
109
 
110
+ test("windows exposes its implemented host capabilities", () => {
111
+ expect(supportsHostProxy("windows")).toBe(true);
112
+ expect(supportsHostProxy("windows", "host_bash")).toBe(true);
113
+ expect(supportsHostProxy("windows", "host_file")).toBe(true);
114
+ expect(supportsHostProxy("windows", "host_cu")).toBe(true);
115
+ expect(supportsHostProxy("windows", "host_browser")).toBe(true);
116
+ expect(supportsHostProxy("windows", "host_ui_snapshot")).toBe(true);
117
+ expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
118
+ });
119
+
106
120
  // ── chrome-extension: only host_browser. ──
107
121
  test("chrome-extension returns false (no capability)", () => {
108
122
  // Chrome extension does not support "any host proxy at all" — it only
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Who a reply should be visible to, decided once for every producer.
3
+ */
4
+
5
+ import type { MessageAudience } from "@vellumai/gateway-client";
6
+
7
+ import { isSlackDmConversation } from "../messaging/providers/slack/message-metadata.js";
8
+
9
+ /**
10
+ * The audience a reply addressed at `userId` should carry, or `undefined` when
11
+ * the whole room may see it.
12
+ *
13
+ * Restricting a message only means something where someone else could read it.
14
+ * A room with one reader gets no restriction: a Slack DM is already private,
15
+ * and marking it restricted there costs durability, because an ephemeral
16
+ * message does not survive a reload.
17
+ *
18
+ * Slack is the only channel that can show one reader a message in a room it
19
+ * shares with others. The test is for a DM rather than for a channel id, so
20
+ * every other room shape counts as shared: a private group holds several
21
+ * readers even though its id begins with `G` rather than `C`.
22
+ *
23
+ * `sourceChannel` and `chatId` are loosely typed because the only questions
24
+ * asked of them are whether the channel is Slack and whether the room is a
25
+ * DM, and several callers hold values that were never narrowed. An unknown
26
+ * channel answers with no restriction rather than throwing.
27
+ */
28
+ export function audienceForReader(
29
+ sourceChannel: string | null | undefined,
30
+ chatId: string | null | undefined,
31
+ userId: string | null | undefined,
32
+ ): MessageAudience | undefined {
33
+ if (sourceChannel !== "slack" || !userId || !chatId) {
34
+ return undefined;
35
+ }
36
+ if (isSlackDmConversation(chatId)) {
37
+ return undefined;
38
+ }
39
+ return { kind: "oneReader", userId };
40
+ }
@@ -166,6 +166,7 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
166
166
 
167
167
  export const INTERFACE_IDS = [
168
168
  "macos",
169
+ "windows",
169
170
  "ios",
170
171
  "cli",
171
172
  "telegram",
@@ -264,6 +265,7 @@ export function parseClientOs(value: unknown): ClientOs | null {
264
265
  */
265
266
  export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
266
267
  "macos",
268
+ "windows",
267
269
  "ios",
268
270
  "cli",
269
271
  "web",
@@ -274,9 +276,9 @@ export function isInteractiveInterface(id: InterfaceId): boolean {
274
276
  }
275
277
 
276
278
  /**
277
- * Host proxy capabilities that an interface can support. The macOS client
278
- * supports all of them; the chrome-extension interface only supports
279
- * host_browser (via the Chrome DevTools Protocol proxy).
279
+ * Host proxy capabilities that an interface can support. macOS supports all
280
+ * of them, Windows withholds app control, and chrome-extension supports only
281
+ * host_browser through the Chrome DevTools Protocol proxy.
280
282
  */
281
283
  export const HOST_PROXY_CAPABILITIES = [
282
284
  "host_bash",
@@ -290,29 +292,28 @@ export const HOST_PROXY_CAPABILITIES = [
290
292
  export type HostProxyCapability = (typeof HOST_PROXY_CAPABILITIES)[number];
291
293
 
292
294
  /**
293
- * Interfaces that support the full desktop host-proxy set (every
294
- * `HostProxyCapability` value). This is the capability-level identity used
295
- * by the discriminated transport metadata union and by the
295
+ * Interfaces that support desktop host-proxy tools. This identity is used by
296
+ * the discriminated transport metadata union and by the
296
297
  * `supportsHostProxy(id)` type predicate.
297
298
  *
298
299
  * Extend this literal type AND the `supportsHostProxy` implementation
299
- * below in lock-step when adding a new host-capable client (e.g. a native
300
- * Linux or Windows desktop).
300
+ * below in lock-step when adding a new host-capable client such as native
301
+ * Linux.
301
302
  */
302
- export type HostProxyInterfaceId = "macos";
303
+ export type HostProxyInterfaceId = "macos" | "windows";
303
304
 
304
305
  /**
305
306
  * Whether the interface supports a host proxy capability.
306
307
  *
307
308
  * The no-arg form `supportsHostProxy(id)` asks "is this interface a desktop
308
- * host-proxy client?" — it returns `true` only for macOS and is the type
309
+ * host-proxy client?" It returns `true` for native desktop clients and is the
309
310
  * predicate that narrows `InterfaceId` to `HostProxyInterfaceId`. It returns
310
311
  * `false` for chrome-extension because chrome-extension only supports
311
312
  * `host_browser`, and the no-arg form is the gate that legacy desktop-only
312
313
  * call sites use (e.g. preactivating computer-use, restoring host proxies
313
- * in the drain queue). Callers that want to check a single capability —
314
+ * in the drain queue). Callers that want to check a single capability,
314
315
  * for example, to decide whether to keep `hostBrowserProxy` available for
315
- * chrome-extension — should pass the capability explicitly:
316
+ * chrome-extension, should pass the capability explicitly:
316
317
  * `supportsHostProxy(id, "host_browser")`.
317
318
  */
318
319
  export function supportsHostProxy(id: InterfaceId): id is HostProxyInterfaceId;
@@ -331,6 +332,9 @@ export function supportsHostProxy(
331
332
  if (id === "macos") {
332
333
  return true;
333
334
  }
335
+ if (id === "windows") {
336
+ return capability == null || capability !== "host_app_control";
337
+ }
334
338
  if (id === "chrome-extension" && capability === "host_browser") {
335
339
  return true;
336
340
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Memory injection indexes CLI help via {@link buildCliCommandHelpContent}.
3
+ * Setup prompts like "Setup <app> for me" should retrieve `plugins search` /
4
+ * `skills search` before the model falls back to web search.
5
+ *
6
+ * These assertions lock the retrieval phrases in the indexed blob so a later
7
+ * wording change cannot silently drop them.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import { buildCliCommandHelpContent } from "../../plugins/defaults/memory/substrate/cli-command-content.js";
12
+ import { pluginsHelp } from "../commands/plugins.help.js";
13
+ import { skillsHelp } from "../commands/skills.help.js";
14
+
15
+ function searchHelp(
16
+ help: { subcommands?: Array<{ name: string; helpText?: string }> },
17
+ ): { name: string; helpText?: string } {
18
+ const search = help.subcommands?.find((sub) => sub.name === "search");
19
+ if (!search) {
20
+ throw new Error("expected a search subcommand");
21
+ }
22
+ return search;
23
+ }
24
+
25
+ describe("catalog search help for setup-intent retrieval", () => {
26
+ test("plugins help indexes setup, superpowers, and web-search fallback", () => {
27
+ const indexed = buildCliCommandHelpContent(pluginsHelp);
28
+ const search = searchHelp(pluginsHelp);
29
+
30
+ expect(search.helpText).toBeDefined();
31
+ expect(indexed.toLowerCase()).toContain("superpowers");
32
+ expect(indexed).toContain("Setup <app> for me");
33
+ expect(indexed.toLowerCase()).toContain("before searching the web");
34
+ expect(indexed).toContain("assistant plugins search");
35
+ expect(indexed).toContain("assistant skills search");
36
+ expect(indexed.toLowerCase()).not.toContain("empty query");
37
+ });
38
+
39
+ test("skills help indexes setup as the second stop before web search", () => {
40
+ const indexed = buildCliCommandHelpContent(skillsHelp);
41
+ const search = searchHelp(skillsHelp);
42
+
43
+ expect(search.helpText).toBeDefined();
44
+ expect(indexed).toContain("Second stop after plugin search");
45
+ expect(indexed).toContain("Setup <app> for me");
46
+ expect(indexed.toLowerCase()).toContain("before searching the web");
47
+ expect(indexed).toContain("assistant skills search");
48
+ expect(indexed).toContain("try web search");
49
+ });
50
+ });
@@ -13,10 +13,12 @@
13
13
  * - `revoke` with no flags (channel: undefined)
14
14
  * - `revoke --channel phone` sends channel param
15
15
  * - `create --channel fax` → invalid channel, exits with code 1, IPC not called
16
+ * - the accepted `--channel` set is the canonical CHANNEL_IDS vocabulary
16
17
  */
17
18
 
18
19
  import { beforeEach, describe, expect, mock, test } from "bun:test";
19
20
 
21
+ import { CHANNEL_IDS } from "@vellumai/service-contracts/channels";
20
22
  import { Command } from "commander";
21
23
 
22
24
  // ---------------------------------------------------------------------------
@@ -242,6 +244,35 @@ describe("channel-verification-sessions create", () => {
242
244
  expect(lastIpcCall).toBeNull();
243
245
  });
244
246
 
247
+ test("--channel discord reaches IPC (canonical vocabulary, not a local copy)", async () => {
248
+ const { exitCode } = await runCommand([
249
+ "channel-verification-sessions",
250
+ "create",
251
+ "--channel",
252
+ "discord",
253
+ ]);
254
+
255
+ expect(exitCode).toBe(0);
256
+ expect(lastIpcCall!.method).toBe("channel_verification_sessions_create");
257
+ expect(lastIpcCall!.params).toMatchObject({
258
+ body: { channel: "discord", purpose: "guardian" },
259
+ });
260
+ });
261
+
262
+ test("every canonical channel id is accepted", async () => {
263
+ for (const channel of CHANNEL_IDS) {
264
+ const { exitCode } = await runCommand([
265
+ "channel-verification-sessions",
266
+ "create",
267
+ "--channel",
268
+ channel,
269
+ ]);
270
+
271
+ expect(exitCode).toBe(0);
272
+ expect(lastIpcCall!.params).toMatchObject({ body: { channel } });
273
+ }
274
+ });
275
+
245
276
  test("IPC error results in exit code 1", async () => {
246
277
  mockIpcResult = { ok: false, error: "Could not connect" };
247
278
 
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Tests for `assistant conversations search`.
3
+ *
4
+ * Validates IPC plumbing onto `conversations_search`, human vs `--json`
5
+ * output, empty-result copy, and validation of the search term / --limit.
6
+ */
7
+
8
+ import * as nodeFs from "node:fs";
9
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ import { Command } from "commander";
12
+
13
+ let lastIpcCall: { method: string; params?: Record<string, unknown> } | null =
14
+ null;
15
+
16
+ let mockIpcResult: {
17
+ ok: boolean;
18
+ result?: unknown;
19
+ error?: string;
20
+ statusCode?: number;
21
+ } = { ok: true, result: { query: "flux", results: [] } };
22
+
23
+ mock.module("../../../ipc/cli-client.js", () => ({
24
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
25
+ lastIpcCall = { method, params };
26
+ return mockIpcResult;
27
+ },
28
+ exitCodeFromIpcResult: (r: { statusCode?: number }) =>
29
+ r.statusCode === undefined
30
+ ? 10
31
+ : r.statusCode >= 500
32
+ ? 3
33
+ : r.statusCode >= 400
34
+ ? 2
35
+ : 1,
36
+ exitFromIpcResult: () => {
37
+ process.exitCode = 1;
38
+ },
39
+ }));
40
+
41
+ const fakeLogger = {
42
+ info: () => {},
43
+ warn: () => {},
44
+ error: () => {},
45
+ debug: () => {},
46
+ };
47
+
48
+ mock.module("../../../util/logger.js", () => ({
49
+ getLogger: () => fakeLogger,
50
+ getCliLogger: () => fakeLogger,
51
+ initLogger: () => {},
52
+ truncateForLog: (v: string) => v,
53
+ pruneOldLogFiles: () => 0,
54
+ LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
55
+ getCurrentLogFilePath: () => "/tmp/test-assistant.log",
56
+ }));
57
+
58
+ const realFs = { ...nodeFs };
59
+ mock.module("node:fs", () => ({ ...realFs }));
60
+
61
+ const { registerConversationsCommand } = await import("../conversations.js");
62
+
63
+ const THREE_HOURS_AGO = Date.now() - 3 * 60 * 60 * 1000;
64
+
65
+ const matchingResult = {
66
+ conversationId: "conv-xyz",
67
+ conversationTitle: "Quarterly metrics",
68
+ conversationUpdatedAt: THREE_HOURS_AGO,
69
+ matchingMessages: [
70
+ {
71
+ messageId: "msg-1",
72
+ role: "user",
73
+ excerpt: "let's review the flux capacitor numbers",
74
+ createdAt: THREE_HOURS_AGO,
75
+ },
76
+ {
77
+ messageId: "msg-2",
78
+ role: "assistant",
79
+ excerpt: "the Q3 flux capacitor spend is on track",
80
+ createdAt: THREE_HOURS_AGO,
81
+ },
82
+ ],
83
+ };
84
+
85
+ async function runSearch(
86
+ args: string[],
87
+ ): Promise<{ code: number; stdout: string; stderr: string }> {
88
+ const stdoutChunks: string[] = [];
89
+ const stderrChunks: string[] = [];
90
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
91
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
92
+ process.stdout.write = ((chunk: unknown) => {
93
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
94
+ return true;
95
+ }) as typeof process.stdout.write;
96
+ process.stderr.write = ((chunk: unknown) => {
97
+ stderrChunks.push(typeof chunk === "string" ? chunk : String(chunk));
98
+ return true;
99
+ }) as typeof process.stderr.write;
100
+
101
+ const program = new Command();
102
+ program.exitOverride();
103
+ program.configureOutput({
104
+ writeErr: (s) => {
105
+ stderrChunks.push(s);
106
+ },
107
+ writeOut: (s) => {
108
+ stdoutChunks.push(s);
109
+ },
110
+ });
111
+ registerConversationsCommand(program);
112
+ try {
113
+ await program.parseAsync(["node", "assistant", "conversations", ...args]);
114
+ } catch (err) {
115
+ const isHelp =
116
+ typeof err === "object" &&
117
+ err !== null &&
118
+ "code" in err &&
119
+ (err as { code?: string }).code === "commander.helpDisplayed";
120
+ if (!isHelp && (process.exitCode === 0 || process.exitCode === undefined)) {
121
+ process.exitCode = 1;
122
+ }
123
+ if (
124
+ !isHelp &&
125
+ err instanceof Error &&
126
+ !stderrChunks.some((s) => s.includes(err.message))
127
+ ) {
128
+ stderrChunks.push(err.message);
129
+ }
130
+ } finally {
131
+ process.stdout.write = originalStdoutWrite;
132
+ process.stderr.write = originalStderrWrite;
133
+ }
134
+ const code = Number(process.exitCode ?? 0);
135
+ process.exitCode = 0;
136
+ return {
137
+ code,
138
+ stdout: stdoutChunks.join(""),
139
+ stderr: stderrChunks.join(""),
140
+ };
141
+ }
142
+
143
+ beforeEach(() => {
144
+ lastIpcCall = null;
145
+ process.exitCode = 0;
146
+ mockIpcResult = { ok: true, result: { query: "flux", results: [] } };
147
+ });
148
+
149
+ afterEach(() => {
150
+ process.exitCode = 0;
151
+ });
152
+
153
+ describe("conversations search", () => {
154
+ test("forwards the term to conversations_search as queryParams.q", async () => {
155
+ mockIpcResult = {
156
+ ok: true,
157
+ result: { query: "flux capacitor", results: [matchingResult] },
158
+ };
159
+ const { code } = await runSearch(["search", "flux capacitor"]);
160
+ expect(code).toBe(0);
161
+ expect(lastIpcCall?.method).toBe("conversations_search");
162
+ expect(lastIpcCall?.params).toEqual({
163
+ queryParams: { q: "flux capacitor" },
164
+ });
165
+ });
166
+
167
+ test("passes --limit as a query param string", async () => {
168
+ mockIpcResult = {
169
+ ok: true,
170
+ result: { query: "flux", results: [] },
171
+ };
172
+ const { code } = await runSearch(["search", "flux", "--limit", "5"]);
173
+ expect(code).toBe(0);
174
+ expect(lastIpcCall?.params).toEqual({
175
+ queryParams: { q: "flux", limit: "5" },
176
+ });
177
+ });
178
+
179
+ test("prints conversation rows and excerpts in human mode", async () => {
180
+ mockIpcResult = {
181
+ ok: true,
182
+ result: { query: "flux", results: [matchingResult] },
183
+ };
184
+ const { code, stdout } = await runSearch(["search", "flux"]);
185
+ expect(code).toBe(0);
186
+ expect(stdout).toContain("conv-xyz");
187
+ expect(stdout).toContain("Quarterly metrics");
188
+ expect(stdout).toContain("3h ago");
189
+ expect(stdout).toContain("user let's review the flux capacitor numbers");
190
+ expect(stdout).toContain(
191
+ "assistant the Q3 flux capacitor spend is on track",
192
+ );
193
+ });
194
+
195
+ test("prints Untitled when the conversation title is null", async () => {
196
+ mockIpcResult = {
197
+ ok: true,
198
+ result: {
199
+ query: "flux",
200
+ results: [
201
+ {
202
+ ...matchingResult,
203
+ conversationTitle: null,
204
+ matchingMessages: [],
205
+ },
206
+ ],
207
+ },
208
+ };
209
+ const { code, stdout } = await runSearch(["search", "flux"]);
210
+ expect(code).toBe(0);
211
+ expect(stdout).toContain("Untitled");
212
+ });
213
+
214
+ test("prints a no-match line when the daemon returns no results", async () => {
215
+ mockIpcResult = {
216
+ ok: true,
217
+ result: { query: "no-such-term", results: [] },
218
+ };
219
+ const { code, stdout } = await runSearch(["search", "no-such-term"]);
220
+ expect(code).toBe(0);
221
+ expect(lastIpcCall?.method).toBe("conversations_search");
222
+ expect(stdout).toBe('No conversations matched "no-such-term"\n');
223
+ });
224
+
225
+ test("--json emits the daemon payload", async () => {
226
+ mockIpcResult = {
227
+ ok: true,
228
+ result: { query: "flux", results: [matchingResult] },
229
+ };
230
+ const { code, stdout } = await runSearch(["search", "flux", "--json"]);
231
+ expect(code).toBe(0);
232
+ expect(JSON.parse(stdout)).toEqual({
233
+ query: "flux",
234
+ results: [matchingResult],
235
+ });
236
+ });
237
+
238
+ test("--json reports IPC failure as { ok: false } and exits non-zero", async () => {
239
+ mockIpcResult = { ok: false, error: "assistant not running" };
240
+ const { code, stdout } = await runSearch(["search", "flux", "--json"]);
241
+ expect(code).toBe(10);
242
+ expect(JSON.parse(stdout)).toEqual({
243
+ ok: false,
244
+ error: "assistant not running",
245
+ });
246
+ });
247
+
248
+ test("rejects a whitespace-only term before IPC", async () => {
249
+ const { code, stderr } = await runSearch(["search", " "]);
250
+ expect(code).toBe(1);
251
+ expect(lastIpcCall).toBeNull();
252
+ expect(stderr).toContain("Search term must be a non-empty string");
253
+ });
254
+
255
+ test("--json rejects a whitespace-only term as { ok: false }", async () => {
256
+ const { code, stdout } = await runSearch(["search", " ", "--json"]);
257
+ expect(code).toBe(1);
258
+ expect(lastIpcCall).toBeNull();
259
+ expect(JSON.parse(stdout)).toMatchObject({
260
+ ok: false,
261
+ error: expect.stringContaining("Search term must be a non-empty string"),
262
+ });
263
+ });
264
+
265
+ test("rejects a non-numeric --limit before IPC", async () => {
266
+ const { code, stderr } = await runSearch([
267
+ "search",
268
+ "flux",
269
+ "--limit",
270
+ "all",
271
+ ]);
272
+ expect(code).toBe(1);
273
+ expect(lastIpcCall).toBeNull();
274
+ expect(stderr).toContain("Invalid --limit value");
275
+ });
276
+
277
+ test("search --help documents the term, --limit, and examples", async () => {
278
+ const { code, stdout } = await runSearch(["search", "--help"]);
279
+ expect(code).toBe(0);
280
+ expect(stdout).toContain(
281
+ "Search conversations by title and message content",
282
+ );
283
+ expect(stdout).toContain("--limit");
284
+ expect(stdout).toContain(
285
+ 'assistant conversations search "project planning"',
286
+ );
287
+ expect(stdout).toContain("--json");
288
+ });
289
+ });
@@ -98,6 +98,7 @@ mock.module("../../../ipc/gateway-client.js", () => ({
98
98
  }));
99
99
 
100
100
  mock.module("../../../messaging/providers/slack/send.js", () => ({
101
+ updateSlackMessage: async () => ({ ok: true, ts: "ts" }),
101
102
  sendSlackReply: async (
102
103
  chatId: string,
103
104
  text: string,