@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
@@ -120,6 +120,24 @@ describe("providers create — derived auth", () => {
120
120
  });
121
121
  });
122
122
 
123
+ test("forwards optional --base-url for ollama create", async () => {
124
+ await run([
125
+ "providers",
126
+ "create",
127
+ "ollama-home",
128
+ "--provider",
129
+ "ollama",
130
+ "--base-url",
131
+ "http://192.168.1.50:11434/v1",
132
+ ]);
133
+ expect(lastIpcCall()?.params?.body).toEqual({
134
+ name: "ollama-home",
135
+ provider: "ollama",
136
+ auth: { type: "none" },
137
+ base_url: "http://192.168.1.50:11434/v1",
138
+ });
139
+ });
140
+
123
141
  test("rejects a keyed provider without --credential before calling the daemon", async () => {
124
142
  const { exitCode } = await run([
125
143
  "providers",
@@ -332,6 +350,56 @@ describe("providers update", () => {
332
350
  base_url: "http://localhost:5678/v1",
333
351
  });
334
352
  });
353
+
354
+ test("ollama update forwards --base-url after GET", async () => {
355
+ mockIpcResult = {
356
+ ok: true,
357
+ result: {
358
+ ...CONNECTION_RESULT,
359
+ name: "ollama",
360
+ provider: "ollama",
361
+ auth: { type: "none" },
362
+ },
363
+ };
364
+ await run([
365
+ "providers",
366
+ "update",
367
+ "ollama",
368
+ "--base-url",
369
+ "http://192.168.1.50:11434/v1",
370
+ ]);
371
+ expect(ipcCalls.map((c) => c.method)).toEqual([
372
+ "inference_provider_connections_get",
373
+ "inference_provider_connections_update",
374
+ ]);
375
+ expect(lastIpcCall()?.params).toEqual({
376
+ pathParams: { name: "ollama" },
377
+ body: {
378
+ auth: { type: "none" },
379
+ base_url: "http://192.168.1.50:11434/v1",
380
+ },
381
+ });
382
+ });
383
+ });
384
+
385
+ describe("providers get output", () => {
386
+ test("prints base_url when the connection stores one", async () => {
387
+ mockIpcResult = {
388
+ ok: true,
389
+ result: {
390
+ name: "ollama",
391
+ provider: "ollama",
392
+ auth: { type: "none" },
393
+ baseUrl: "http://192.168.1.50:11434/v1",
394
+ createdAt: 0,
395
+ updatedAt: 0,
396
+ },
397
+ };
398
+ const { stdout } = await run(["providers", "get", "ollama"]);
399
+ expect(stdout).toContain("name: ollama");
400
+ expect(stdout).toContain("provider: ollama");
401
+ expect(stdout).toContain("base_url: http://192.168.1.50:11434/v1");
402
+ });
335
403
  });
336
404
 
337
405
  describe("providers list output", () => {
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Tests for the keys set CLI subcommand's agent-shell guard.
2
+ * Tests for the keys CLI subcommands: the set agent-shell guard and the
3
+ * delete force flag.
3
4
  *
4
5
  * Validates:
5
6
  * - inline set from an agent shell (__CONVERSATION_ID) is refused with a
@@ -8,6 +9,8 @@
8
9
  * - --generated bypasses the guard and stores via the daemon client
9
10
  * - a plain user-terminal invocation (no markers) stores via the daemon
10
11
  * client
12
+ * - delete forwards the caller's force choice to the daemon client
13
+ * - an in-use refusal points at the --force escape hatch
11
14
  */
12
15
 
13
16
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
@@ -18,7 +21,16 @@ import { Command } from "commander";
18
21
  // Mock state
19
22
  // ---------------------------------------------------------------------------
20
23
 
24
+ interface DeleteResultStub {
25
+ result: "deleted" | "not-found" | "error";
26
+ error?: string;
27
+ code?: string;
28
+ }
29
+
21
30
  let mockSetSecureKeyViaDaemon = mock(() => Promise.resolve({ ok: true }));
31
+ let mockDeleteSecureKeyViaDaemon = mock(
32
+ (): Promise<DeleteResultStub> => Promise.resolve({ result: "deleted" }),
33
+ );
22
34
 
23
35
  // ---------------------------------------------------------------------------
24
36
  // Mocks — must be declared before importing the module under test
@@ -26,11 +38,11 @@ let mockSetSecureKeyViaDaemon = mock(() => Promise.resolve({ ok: true }));
26
38
 
27
39
  mock.module("../../lib/daemon-credential-client.js", () => ({
28
40
  setSecureKeyViaDaemon: mockSetSecureKeyViaDaemon,
29
- deleteSecureKeyViaDaemon: mock(() =>
30
- Promise.resolve({ result: "deleted" as const }),
31
- ),
41
+ deleteSecureKeyViaDaemon: mockDeleteSecureKeyViaDaemon,
32
42
  }));
33
43
 
44
+ const loggedErrors: string[] = [];
45
+
34
46
  mock.module("../../../util/logger.js", () => ({
35
47
  getLogger: () => ({
36
48
  info: () => {},
@@ -41,7 +53,9 @@ mock.module("../../../util/logger.js", () => ({
41
53
  getCliLogger: () => ({
42
54
  info: () => {},
43
55
  warn: () => {},
44
- error: () => {},
56
+ error: (message: unknown) => {
57
+ loggedErrors.push(String(message));
58
+ },
45
59
  debug: () => {},
46
60
  }),
47
61
  }));
@@ -55,6 +69,10 @@ let savedSkillContextJson: string | undefined;
55
69
 
56
70
  beforeEach(() => {
57
71
  mockSetSecureKeyViaDaemon = mock(() => Promise.resolve({ ok: true }));
72
+ mockDeleteSecureKeyViaDaemon = mock(
73
+ (): Promise<DeleteResultStub> => Promise.resolve({ result: "deleted" }),
74
+ );
75
+ loggedErrors.length = 0;
58
76
  savedConversationId = process.env.__CONVERSATION_ID;
59
77
  savedSkillContextJson = process.env.__SKILL_CONTEXT_JSON;
60
78
  delete process.env.__CONVERSATION_ID;
@@ -83,9 +101,7 @@ afterEach(() => {
83
101
  async function runKeysCommand(...args: string[]) {
84
102
  mock.module("../../lib/daemon-credential-client.js", () => ({
85
103
  setSecureKeyViaDaemon: mockSetSecureKeyViaDaemon,
86
- deleteSecureKeyViaDaemon: mock(() =>
87
- Promise.resolve({ result: "deleted" as const }),
88
- ),
104
+ deleteSecureKeyViaDaemon: mockDeleteSecureKeyViaDaemon,
89
105
  }));
90
106
 
91
107
  const { registerKeysCommand } = await import("../keys.js");
@@ -187,3 +203,68 @@ describe("assistant keys set (agent-shell guard)", () => {
187
203
  expect(process.exitCode).toBe(0);
188
204
  });
189
205
  });
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // delete — in-use guard and --force
209
+ // ---------------------------------------------------------------------------
210
+
211
+ describe("assistant keys delete (in-use guard)", () => {
212
+ test("deletes without force by default", async () => {
213
+ // GIVEN a stored key no connection depends on
214
+ // WHEN the key is deleted without --force
215
+ await runKeysCommand("keys", "delete", "acme");
216
+
217
+ // THEN the daemon is asked for a non-forced delete
218
+ expect(mockDeleteSecureKeyViaDaemon).toHaveBeenCalledTimes(1);
219
+ const [type, provider, force] = mockDeleteSecureKeyViaDaemon.mock
220
+ .calls[0] as unknown as [string, string, boolean];
221
+ expect(type).toBe("api_key");
222
+ expect(provider).toBe("acme");
223
+ expect(force).toBe(false);
224
+ expect(process.exitCode).toBe(0);
225
+ });
226
+
227
+ test("--force carries explicit intent to the daemon", async () => {
228
+ // GIVEN a caller who accepts breaking dependent connections
229
+ // WHEN --force is passed
230
+ await runKeysCommand("keys", "delete", "acme", "--force");
231
+
232
+ // THEN the daemon receives the force flag
233
+ const [, , force] = mockDeleteSecureKeyViaDaemon.mock
234
+ .calls[0] as unknown as [string, string, boolean];
235
+ expect(force).toBe(true);
236
+ });
237
+
238
+ test("names dependent connections and points at --force when refused", async () => {
239
+ // GIVEN a key an LLM provider connection resolves its auth through
240
+ mockDeleteSecureKeyViaDaemon = mock(
241
+ (): Promise<DeleteResultStub> =>
242
+ Promise.resolve({
243
+ result: "error",
244
+ code: "CREDENTIAL_IN_USE",
245
+ error:
246
+ 'Credential credential/acme/api_key is in use by connection "acme-router".',
247
+ }),
248
+ );
249
+ const exitCalls: number[] = [];
250
+ const origExit = process.exit;
251
+ process.exit = ((code?: number) => {
252
+ exitCalls.push(code ?? 0);
253
+ throw new Error("__exit__");
254
+ }) as typeof process.exit;
255
+
256
+ // WHEN the delete is attempted without --force
257
+ try {
258
+ await runKeysCommand("keys", "delete", "acme");
259
+ } catch (err) {
260
+ expect((err as Error).message).toBe("__exit__");
261
+ } finally {
262
+ process.exit = origExit;
263
+ }
264
+
265
+ // THEN the refusal names the connection and suggests the escape hatch
266
+ expect(exitCalls).toEqual([1]);
267
+ expect(loggedErrors.join("\n")).toContain('connection "acme-router"');
268
+ expect(loggedErrors.join("\n")).toContain("keys delete acme --force");
269
+ });
270
+ });
@@ -16,6 +16,8 @@
16
16
  * - under --json the consent listing, the prompt, and the cancellation line
17
17
  * all go to stderr, leaving stdout a pure JSON document
18
18
  * - inspect renders the schedules surface block
19
+ * - install prints a setup-skill hint when `skills/setup` or
20
+ * `skills/<name>-setup` is present on the installed tree
19
21
  *
20
22
  * The installers are replaced by fakes that stage a fixture tree and run the
21
23
  * real `confirmStagedOrAbort` gate, so the command's consent callback is
@@ -76,6 +78,10 @@ let ipcResults: Array<{
76
78
 
77
79
  let inspectResult: PluginInspection | null = null;
78
80
 
81
+ /** On-disk tree returned as the fake install `target` so post-install walks work. */
82
+ let installTarget: string | null = null;
83
+ const installTargetDirs: string[] = [];
84
+
79
85
  // ---------------------------------------------------------------------------
80
86
  // Mocks
81
87
  // ---------------------------------------------------------------------------
@@ -123,7 +129,7 @@ async function runFakeInstall(
123
129
  await stageAndConfirm(name, deps?.confirmStaged);
124
130
  return {
125
131
  name,
126
- target: `/plugins/${name}`,
132
+ target: installTarget ?? `/plugins/${name}`,
127
133
  fileCount: 3,
128
134
  ref: "main",
129
135
  commit: "a".repeat(40),
@@ -253,6 +259,7 @@ beforeEach(() => {
253
259
  upgradePluginCalls = [];
254
260
  ipcResults = [];
255
261
  inspectResult = null;
262
+ installTarget = null;
256
263
  // Platform features on by default, so a plain name install takes the
257
264
  // platform-tarball branch.
258
265
  delete process.env.VELLUM_DISABLE_PLATFORM;
@@ -260,6 +267,10 @@ beforeEach(() => {
260
267
  });
261
268
 
262
269
  afterEach(() => {
270
+ for (const dir of installTargetDirs) {
271
+ rmSync(dir, { recursive: true, force: true });
272
+ }
273
+ installTargetDirs.length = 0;
263
274
  if (savedDisablePlatform === undefined) {
264
275
  delete process.env.VELLUM_DISABLE_PLATFORM;
265
276
  } else {
@@ -346,6 +357,7 @@ describe("plugins install - declared-schedules consent", () => {
346
357
  expect(confirmCalls.length).toBe(0);
347
358
  expect(r.stdout).not.toContain("declares");
348
359
  expect(r.stdout).not.toContain("schedule");
360
+ expect(r.stdout).not.toContain("to help set up this plugin");
349
361
  expect(r.stdout).toContain('Installed plugin "example"');
350
362
  expect(r.exitCode).toBe(0);
351
363
  });
@@ -690,3 +702,47 @@ describe("plugins inspect - schedules surface", () => {
690
702
  expect(r.exitCode).toBe(0);
691
703
  });
692
704
  });
705
+
706
+ describe("plugins install - setup skill hint", () => {
707
+ function writeSetupSkill(skillId: string): void {
708
+ const dir = mkdtempSync(join(tmpdir(), "plugins-cmd-target-"));
709
+ installTargetDirs.push(dir);
710
+ mkdirSync(join(dir, "skills", skillId), { recursive: true });
711
+ writeFileSync(
712
+ join(dir, "skills", skillId, "SKILL.md"),
713
+ "---\nname: setup\ndescription: Set up the plugin.\n---\n",
714
+ );
715
+ installTarget = dir;
716
+ }
717
+
718
+ test("points at skills/setup after a successful install", async () => {
719
+ writeSetupSkill("setup");
720
+
721
+ const r = await runCommand(["plugins", "install", "example"]);
722
+
723
+ expect(r.stdout).toContain('Installed plugin "example"');
724
+ expect(r.stdout).toContain(
725
+ "Load the setup skill to help set up this plugin",
726
+ );
727
+ expect(r.exitCode).toBe(0);
728
+ });
729
+
730
+ test("points at skills/<name>-setup when that is the shipped id", async () => {
731
+ writeSetupSkill("imessage-setup");
732
+
733
+ const r = await runCommand(["plugins", "install", "imessage"]);
734
+
735
+ expect(r.stdout).toContain(
736
+ "Load the imessage-setup skill to help set up this plugin",
737
+ );
738
+ expect(r.exitCode).toBe(0);
739
+ });
740
+
741
+ test("prints no setup hint when the plugin ships no setup skill", async () => {
742
+ const r = await runCommand(["plugins", "install", "example"]);
743
+
744
+ expect(r.stdout).toContain('Installed plugin "example"');
745
+ expect(r.stdout).not.toContain("to help set up this plugin");
746
+ expect(r.exitCode).toBe(0);
747
+ });
748
+ });
@@ -10,15 +10,16 @@ export const channelVerificationSessionsHelp: CliCommandHelp = {
10
10
  ],
11
11
  helpText: `
12
12
  Verification sessions are used to verify guardian bindings and trusted
13
- contacts across channels (telegram, phone, slack, email). Three flows exist:
13
+ contacts across channels (telegram, phone, slack, discord, email). Three
14
+ flows exist:
14
15
 
15
16
  1. Inbound challenge — the assistant generates a secret code and waits
16
17
  for the guardian to send it back on the channel. Used when the
17
18
  guardian can already message the assistant.
18
19
 
19
20
  2. Outbound verification — the assistant sends a verification code to
20
- a destination (Telegram handle, phone number, Slack user ID) and
21
- waits for confirmation. Used when bootstrapping a new channel.
21
+ a destination (Telegram handle, phone number, Slack or Discord user
22
+ ID) and waits for confirmation. Used when bootstrapping a new channel.
22
23
 
23
24
  3. Trusted contact verification — verifies a contact channel that
24
25
  already exists in the contact graph, sending a code to the channel
@@ -36,7 +37,7 @@ Examples:
36
37
  options: [
37
38
  {
38
39
  flags: "--channel <channel>",
39
- description: "Channel type (telegram, phone, slack, email)",
40
+ description: "Channel type (telegram, phone, slack, discord, email)",
40
41
  },
41
42
  {
42
43
  flags: "--destination <destination>",
@@ -72,8 +73,8 @@ Routes between three creation modes based on the provided options:
72
73
 
73
74
  2. Outbound: --channel <ch> --destination <dest>
74
75
  Sends a verification code to the given destination. Supports telegram
75
- (handle or chat ID), phone (E.164 number), slack (user ID), and email.
76
- Use --rebind to replace an existing guardian binding.
76
+ (handle or chat ID), phone (E.164 number), slack and discord (user ID),
77
+ and email. Use --rebind to replace an existing guardian binding.
77
78
 
78
79
  3. Inbound: --channel <ch> (no --destination)
79
80
  Generates a challenge secret for the guardian to send back on the
@@ -91,7 +92,8 @@ Examples:
91
92
  options: [
92
93
  {
93
94
  flags: "--channel <channel>",
94
- description: "Channel type (telegram, phone). Defaults to telegram.",
95
+ description:
96
+ "Channel type (telegram, phone, slack, discord, email). Defaults to telegram.",
95
97
  },
96
98
  ],
97
99
  helpText: `
@@ -113,7 +115,7 @@ Examples:
113
115
  options: [
114
116
  {
115
117
  flags: "--channel <channel>",
116
- description: "Channel type (telegram, phone, slack, email)",
118
+ description: "Channel type (telegram, phone, slack, discord, email)",
117
119
  required: true,
118
120
  },
119
121
  {
@@ -137,7 +139,7 @@ Examples:
137
139
  options: [
138
140
  {
139
141
  flags: "--channel <channel>",
140
- description: "Channel type (telegram, phone, slack, email)",
142
+ description: "Channel type (telegram, phone, slack, discord, email)",
141
143
  required: true,
142
144
  },
143
145
  ],
@@ -1,3 +1,12 @@
1
+ // `cli/no-daemon-internals` forbids hoisting `channels/types.js`, so the CLI
2
+ // reads the vocabulary straight from the contract package that file re-exports.
3
+ // Both sides then validate `--channel` against the same set.
4
+
5
+ import {
6
+ CHANNEL_IDS,
7
+ type ChannelId,
8
+ isChannelId,
9
+ } from "@vellumai/service-contracts/channels";
1
10
  import type { Command } from "commander";
2
11
 
3
12
  import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
@@ -6,25 +15,6 @@ import { registerCommand } from "../lib/register-command.js";
6
15
  import { writeOutput } from "../output.js";
7
16
  import { channelVerificationSessionsHelp } from "./channel-verification-sessions.help.js";
8
17
 
9
- // ---------------------------------------------------------------------------
10
- // Local channel validation (replaces daemon-internal channels/types.js import)
11
- // ---------------------------------------------------------------------------
12
-
13
- const VALID_CHANNEL_IDS = [
14
- "telegram",
15
- "phone",
16
- "vellum",
17
- "whatsapp",
18
- "slack",
19
- "email",
20
- "platform",
21
- ] as const;
22
- type ChannelId = (typeof VALID_CHANNEL_IDS)[number];
23
-
24
- function isChannelId(raw: string): raw is ChannelId {
25
- return (VALID_CHANNEL_IDS as readonly string[]).includes(raw);
26
- }
27
-
28
18
  /**
29
19
  * Validate the --channel option. Returns the validated ChannelId or writes an
30
20
  * error and returns `false`. When `required` is false an absent value is fine
@@ -49,7 +39,7 @@ function validateChannelOpt(
49
39
  if (required) {
50
40
  writeOutput(cmd, {
51
41
  ok: false,
52
- error: `The "channel" option is required. Valid values: ${VALID_CHANNEL_IDS.join(", ")}`,
42
+ error: `The "channel" option is required. Valid values: ${CHANNEL_IDS.join(", ")}`,
53
43
  });
54
44
  process.exitCode = 1;
55
45
  return false;
@@ -59,7 +49,7 @@ function validateChannelOpt(
59
49
  if (!isChannelId(raw)) {
60
50
  writeOutput(cmd, {
61
51
  ok: false,
62
- error: `Invalid channel "${raw}". Valid values: ${VALID_CHANNEL_IDS.join(", ")}`,
52
+ error: `Invalid channel "${raw}". Valid values: ${CHANNEL_IDS.join(", ")}`,
63
53
  });
64
54
  process.exitCode = 1;
65
55
  return false;
@@ -12,6 +12,7 @@ running.
12
12
 
13
13
  Examples:
14
14
  $ assistant conversations list
15
+ $ assistant conversations search "project planning"
15
16
  $ assistant conversations new "Project planning"
16
17
  $ assistant conversations export
17
18
  $ assistant conversations clear`,
@@ -168,6 +169,39 @@ include them.
168
169
  Examples:
169
170
  $ assistant conversations list
170
171
  $ assistant conversations list --include-archived`,
172
+ },
173
+ {
174
+ name: "search",
175
+ args: "<term>",
176
+ description: "Search conversations by title and message content",
177
+ options: [
178
+ {
179
+ flags: "--limit <n>",
180
+ description: "Maximum number of conversations to return (default 20)",
181
+ },
182
+ {
183
+ flags: "--json",
184
+ description: "Output result as machine-readable JSON",
185
+ },
186
+ ],
187
+ helpText: `
188
+ Arguments:
189
+ term Search query. Quote multi-word terms so the shell keeps them as one
190
+ argument. Matches conversation titles (substring) and message
191
+ content (full-text). Archived conversations are excluded, matching
192
+ 'assistant conversations list'.
193
+
194
+ Requires the assistant to be running. Communicates via IPC socket.
195
+
196
+ Results are grouped by conversation, ordered by most recently updated, and
197
+ capped at 20 conversations unless --limit is set. Each result includes the
198
+ conversation ID, title, relative timestamp, and up to three matching
199
+ message excerpts. Use the ID with rename, export, or wake.
200
+
201
+ Examples:
202
+ $ assistant conversations search "project planning"
203
+ $ assistant conversations search flux --limit 5
204
+ $ assistant conversations search "quarterly metrics" --json`,
171
205
  },
172
206
  {
173
207
  name: "new",
@@ -11,6 +11,7 @@ import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
11
11
  import { registerCommand } from "../lib/register-command.js";
12
12
  import { timeAgo } from "../lib/time-ago.js";
13
13
  import { log } from "../logger.js";
14
+ import { shouldOutputJson, writeError, writeOutput } from "../output.js";
14
15
  import { tryResolveConversationId } from "../utils/conversation-id.js";
15
16
  import { conversationsHelp } from "./conversations.help.js";
16
17
  import { registerConversationsDeferCommand } from "./conversations-defer.js";
@@ -29,6 +30,66 @@ type SlackDetachCliResult = {
29
30
  conversationId?: string;
30
31
  };
31
32
 
33
+ type ConversationSearchMessage = {
34
+ messageId: string;
35
+ role: string;
36
+ excerpt: string;
37
+ createdAt: number;
38
+ };
39
+
40
+ type ConversationSearchResult = {
41
+ conversationId: string;
42
+ conversationTitle: string | null;
43
+ conversationUpdatedAt: number;
44
+ matchingMessages: ConversationSearchMessage[];
45
+ };
46
+
47
+ type ConversationSearchResponse = {
48
+ query: string;
49
+ results: ConversationSearchResult[];
50
+ };
51
+
52
+ function parsePositiveLimit(
53
+ raw: string | undefined,
54
+ ): { ok: true; value: number | undefined } | { ok: false; error: string } {
55
+ if (raw === undefined) {
56
+ return { ok: true, value: undefined };
57
+ }
58
+ const trimmed = raw.trim();
59
+ if (!/^\d+$/.test(trimmed)) {
60
+ return {
61
+ ok: false,
62
+ error:
63
+ "Invalid --limit value. Must be a positive integer, e.g. --limit 10.",
64
+ };
65
+ }
66
+ const value = Number(trimmed);
67
+ if (!Number.isSafeInteger(value) || value < 1) {
68
+ return {
69
+ ok: false,
70
+ error:
71
+ "Invalid --limit value. Must be a positive integer, e.g. --limit 10.",
72
+ };
73
+ }
74
+ return { ok: true, value };
75
+ }
76
+
77
+ function formatSearchResultLines(
78
+ results: ConversationSearchResult[],
79
+ ): string[] {
80
+ const lines: string[] = [];
81
+ for (const result of results) {
82
+ lines.push(
83
+ ` ${result.conversationId} ${result.conversationTitle ?? "Untitled"} ${timeAgo(result.conversationUpdatedAt)}`,
84
+ );
85
+ for (const message of result.matchingMessages) {
86
+ const excerpt = message.excerpt.replace(/\s+/g, " ").trim();
87
+ lines.push(` ${message.role} ${excerpt}`);
88
+ }
89
+ }
90
+ return lines;
91
+ }
92
+
32
93
  function outputSlackDetachError(message: string, json?: boolean): void {
33
94
  if (json) {
34
95
  process.stdout.write(JSON.stringify({ ok: false, error: message }) + "\n");
@@ -200,6 +261,70 @@ export function registerConversationsCommand(program: Command): void {
200
261
  },
201
262
  );
202
263
 
264
+ // -------------------------------------------------------------------
265
+ // search
266
+ // -------------------------------------------------------------------
267
+
268
+ subcommand(conversations, "search").action(
269
+ async (term: string, opts: { limit?: string }, cmd: Command) => {
270
+ const trimmed = term.trim();
271
+ if (!trimmed) {
272
+ writeError(
273
+ cmd,
274
+ 'Search term must be a non-empty string. Quote multi-word queries, e.g. assistant conversations search "project planning".',
275
+ );
276
+ process.exitCode = 1;
277
+ return;
278
+ }
279
+
280
+ const parsedLimit = parsePositiveLimit(opts.limit);
281
+ if (!parsedLimit.ok) {
282
+ writeError(cmd, parsedLimit.error);
283
+ process.exitCode = 1;
284
+ return;
285
+ }
286
+
287
+ const queryParams: Record<string, string> = { q: trimmed };
288
+ if (parsedLimit.value !== undefined) {
289
+ queryParams.limit = String(parsedLimit.value);
290
+ }
291
+
292
+ const result = await cliIpcCall<ConversationSearchResponse>(
293
+ "conversations_search",
294
+ { queryParams },
295
+ );
296
+
297
+ if (!result.ok) {
298
+ if (shouldOutputJson(cmd)) {
299
+ writeOutput(cmd, {
300
+ ok: false,
301
+ error: result.error ?? "Conversation search failed",
302
+ });
303
+ process.exitCode = exitCodeFromIpcResult(result);
304
+ return;
305
+ }
306
+ return exitFromIpcResult(result);
307
+ }
308
+
309
+ const payload = result.result!;
310
+ if (shouldOutputJson(cmd)) {
311
+ writeOutput(cmd, payload);
312
+ return;
313
+ }
314
+
315
+ if (payload.results.length === 0) {
316
+ process.stdout.write(
317
+ `No conversations matched "${payload.query}"\n`,
318
+ );
319
+ return;
320
+ }
321
+
322
+ process.stdout.write(
323
+ formatSearchResultLines(payload.results).join("\n") + "\n",
324
+ );
325
+ },
326
+ );
327
+
203
328
  // -------------------------------------------------------------------
204
329
  // new
205
330
  // -------------------------------------------------------------------
@@ -38,6 +38,7 @@ interface ProviderConnection {
38
38
  name: string;
39
39
  provider: string;
40
40
  auth: AuthInfo;
41
+ baseUrl?: string | null;
41
42
  createdAt: string;
42
43
  updatedAt: string;
43
44
  }
@@ -129,10 +130,11 @@ function collectRepeatable(value: string, previous: string[]): string[] {
129
130
  }
130
131
 
131
132
  /**
132
- * Build the openai-compatible custom-provider fields (`base_url`, `models`)
133
- * from CLI flags, forwarded to the connection route under its exact field
134
- * names. The daemon stays authoritative on whether they are required/allowed;
135
- * the CLI only shape-forwards what the user passed.
133
+ * Build the custom-provider fields (`base_url`, `models`) from CLI flags,
134
+ * forwarded to the connection route under its exact field names. The daemon
135
+ * stays authoritative on whether they are required or allowed (openai-compatible
136
+ * requires both; ollama accepts an optional base URL); the CLI only
137
+ * shape-forwards what the user passed.
136
138
  */
137
139
  function buildCustomProviderFields(opts: {
138
140
  baseUrl?: string;
@@ -219,6 +221,9 @@ function attachGetSubcommand(parent: Command): void {
219
221
  process.stdout.write(`name: ${conn.name}\n`);
220
222
  process.stdout.write(`provider: ${conn.provider}\n`);
221
223
  process.stdout.write(`auth: ${formatAuth(conn.auth)}\n`);
224
+ if (conn.baseUrl) {
225
+ process.stdout.write(`base_url: ${conn.baseUrl}\n`);
226
+ }
222
227
  process.stdout.write(
223
228
  `created: ${new Date(conn.createdAt).toISOString()}\n`,
224
229
  );