@vellumai/assistant 0.11.7 → 0.11.8-staging.1

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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -2,6 +2,10 @@
2
2
 
3
3
  import type { CliCommandHelp } from "../lib/cli-command-help.js";
4
4
 
5
+ /** Shared tail for the three commands that park on a form in the app. */
6
+ const FORM_NOTE = `The form opens in the guardian's desktop or web app. With no app connected
7
+ nobody can answer, and the command exits when the wait elapses.`;
8
+
5
9
  export const contactsHelp: CliCommandHelp = {
6
10
  name: "contacts",
7
11
  description: "Manage and query the contact graph",
@@ -15,9 +19,21 @@ can be linked to external identifiers — phone numbers,
15
19
  Telegram IDs, email addresses — via channel memberships. The contact graph
16
20
  is the source of truth for identity resolution across all channels.
17
21
 
22
+ Writes are split in two, and both need the guardian at the app:
23
+
24
+ create/update/delete the record: who someone is (name, notes). Opens a
25
+ form in the guardian's app; nothing is written until
26
+ they submit it. A record has no channel, so it grants
27
+ no access on its own.
28
+ prompt the channel: an address the guardian types, bound to
29
+ a contact. This is what makes someone reachable.
30
+
31
+ Granting someone access to message the assistant is 'contacts invites create'.
32
+
18
33
  Examples:
19
34
  $ assistant contacts list
20
35
  $ assistant contacts get abc-123
36
+ $ assistant contacts create --name "Alice"
21
37
  $ assistant contacts invites list`,
22
38
  subcommands: [
23
39
  {
@@ -83,10 +99,139 @@ assistant metadata is included.
83
99
  Examples:
84
100
  $ assistant contacts get 7a3b1c2d-4e5f-6789-abcd-ef0123456789
85
101
  $ assistant contacts get abc-123 --json`,
102
+ },
103
+ {
104
+ name: "create",
105
+ description: "Propose a new contact for the guardian to confirm",
106
+ options: [
107
+ {
108
+ flags: "--name <name>",
109
+ description: "Proposed display name, prefilled into the form",
110
+ },
111
+ {
112
+ flags: "--notes <notes>",
113
+ description: "Proposed notes, prefilled into the form",
114
+ },
115
+ {
116
+ flags: "--label <label>",
117
+ description: "Display label shown on the form",
118
+ },
119
+ {
120
+ flags: "--description <description>",
121
+ description: "Longer description shown on the form",
122
+ },
123
+ {
124
+ flags: "--timeout <ms>",
125
+ description:
126
+ "How long the form stays open (ms). The command waits for it to close.",
127
+ defaultValue: String(300_000),
128
+ },
129
+ ],
130
+ helpText: `
131
+ Opens an add-contact form in the guardian's app, prefilled with --name and
132
+ --notes. The guardian can edit either field before submitting, and the values
133
+ they submit are what gets written. Nothing is written if they dismiss the form.
134
+
135
+ The new contact has no channels, so it cannot message the assistant and the
136
+ assistant cannot reach it. It is a name to hang notes on and to address an
137
+ invite to. Use 'contacts prompt' to bind an address, or
138
+ 'contacts invites create' to let someone bind their own.
139
+
140
+ ${FORM_NOTE}
141
+
142
+ Examples:
143
+ $ assistant contacts create --name "Alice"
144
+ $ assistant contacts create --name "Alice" --notes "Dentist, referred by Bob"
145
+ $ assistant contacts create --name "Alice" --json`,
146
+ },
147
+ {
148
+ name: "update",
149
+ args: "<id>",
150
+ description: "Propose a contact edit for the guardian to confirm",
151
+ options: [
152
+ {
153
+ flags: "--name <name>",
154
+ description: "Proposed display name, prefilled into the form",
155
+ },
156
+ {
157
+ flags: "--notes <notes>",
158
+ description: "Proposed notes, prefilled into the form",
159
+ },
160
+ {
161
+ flags: "--label <label>",
162
+ description: "Display label shown on the form",
163
+ },
164
+ {
165
+ flags: "--description <description>",
166
+ description: "Longer description shown on the form",
167
+ },
168
+ {
169
+ flags: "--timeout <ms>",
170
+ description:
171
+ "How long the form stays open (ms). The command waits for it to close.",
172
+ defaultValue: String(300_000),
173
+ },
174
+ ],
175
+ helpText: `
176
+ Arguments:
177
+ id UUID of the contact to edit. Run 'assistant contacts list' to find IDs.
178
+
179
+ Opens an edit form in the guardian's app showing the contact's current name
180
+ alongside the proposed change. At least one of --name or --notes is required.
181
+ The guardian can edit either field before submitting; what they submit is what
182
+ gets written.
183
+
184
+ Edits the record only. Channel access is unaffected: use
185
+ 'contacts channels update-status' to revoke or block a channel.
186
+
187
+ ${FORM_NOTE}
188
+
189
+ Examples:
190
+ $ assistant contacts update 7a3b1c2d-4e5f-6789-abcd-ef0123456789 --name "Alice Chen"
191
+ $ assistant contacts update abc-123 --notes "Moved to Berlin"`,
192
+ },
193
+ {
194
+ name: "delete",
195
+ args: "<id>",
196
+ description: "Propose deleting a contact for the guardian to confirm",
197
+ options: [
198
+ {
199
+ flags: "--label <label>",
200
+ description: "Display label shown on the confirmation",
201
+ },
202
+ {
203
+ flags: "--description <description>",
204
+ description: "Longer description shown on the confirmation",
205
+ },
206
+ {
207
+ flags: "--timeout <ms>",
208
+ description:
209
+ "How long the confirmation stays open (ms). The command waits for it to close.",
210
+ defaultValue: String(300_000),
211
+ },
212
+ ],
213
+ helpText: `
214
+ Arguments:
215
+ id UUID of the contact to delete. Run 'assistant contacts list' to find IDs.
216
+
217
+ Opens a confirmation in the guardian's app showing the contact and its
218
+ channels. Nothing is deleted unless they confirm.
219
+
220
+ Deleting a contact deletes its channels with it, so anyone reaching the
221
+ assistant through those channels loses access. A guardian contact cannot be
222
+ deleted. To merge a duplicate instead of deleting it, use the contact_merge
223
+ tool, which keeps the surviving contact's channels.
224
+
225
+ ${FORM_NOTE}
226
+
227
+ Examples:
228
+ $ assistant contacts delete 7a3b1c2d-4e5f-6789-abcd-ef0123456789
229
+ $ assistant contacts delete abc-123 --json`,
86
230
  },
87
231
  {
88
232
  name: "prompt",
89
- description: "Prompt user to register a contact channel via the app UI",
233
+ description:
234
+ "Ask the guardian to enter a channel address and bind it to a contact",
90
235
  options: [
91
236
  {
92
237
  flags: "--channel <channel>",
@@ -118,27 +263,44 @@ Examples:
118
263
  {
119
264
  flags: "--timeout <ms>",
120
265
  description:
121
- "How long to wait for the user to submit (ms). Defaults to match the server-side prompt timeout.",
122
- defaultValue: String(310_000),
266
+ "How long the form stays open (ms). The command waits for it to close.",
267
+ defaultValue: String(300_000),
123
268
  },
124
269
  {
125
270
  flags: "--verify",
126
271
  description:
127
- "Mark the submitted channel verified (same attest as Contacts Verify me)",
272
+ "Pre-check the form's 'mark verified' box. The guardian decides: unchecked, the channel stays unverified.",
128
273
  },
129
274
  ],
130
275
  helpText: `
131
- Opens a contact address prompt in the user's app. The user enters a channel
132
- address (phone number, email, Telegram ID, etc.). The address is saved with
133
- status "unverified" unless --verify is set, which attests the channel the
134
- same way Contacts Verify me does.
276
+ Opens an address form in the guardian's app. They enter a channel address
277
+ (phone number, email, Telegram ID, etc.) and it is bound as a channel.
278
+
279
+ Which contact it binds to depends on --role:
280
+
281
+ --role guardian Binds to the guardian contact. An address already bound
282
+ to a different contact is rejected as a conflict.
283
+ anything else Looks up the submitted (channel type, address). A match
284
+ reuses that contact and channel; no match creates a new
285
+ contact named after the address. Other --role values are
286
+ form hints only: the contact is created with role
287
+ "contact" either way.
288
+
289
+ This cannot add a channel to an arbitrary existing contact. To name a contact
290
+ first, run 'contacts create'; to let someone bind their own address, use
291
+ 'contacts invites create'.
292
+
293
+ The channel is saved unverified unless the guardian leaves the form's "mark
294
+ verified" box checked, which attests it the same way Contacts Verify me does.
295
+ An attested channel is admitted, so --verify only proposes the box state.
296
+
297
+ ${FORM_NOTE}
135
298
 
136
299
  Examples:
137
300
  $ assistant contacts prompt --channel phone --role guardian
301
+ $ assistant contacts prompt --channel email --label "Alice's email address"
138
302
  $ assistant contacts prompt --channel imessage --role guardian --verify \\
139
- --label "Your iMessage number" --placeholder "+15551234567"
140
-
141
- Run \`assistant contacts prompt --help\` for full option details.`,
303
+ --label "Your iMessage number" --placeholder "+15555550142"`,
142
304
  },
143
305
  {
144
306
  name: "channels",
@@ -1,6 +1,11 @@
1
1
  import type { Command } from "commander";
2
2
 
3
3
  import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
4
+ import {
5
+ GUARDIAN_FORM_DEFAULT_TIMEOUT_MS,
6
+ GUARDIAN_FORM_MAX_TIMEOUT_MS,
7
+ guardianFormCallBudgetMs,
8
+ } from "../../util/guardian-form-timeouts.js";
4
9
  import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
5
10
  import { registerCommand } from "../lib/register-command.js";
6
11
  import { shouldOutputJson, writeError, writeOutput } from "../output.js";
@@ -30,7 +35,9 @@ interface ContactWithChannels {
30
35
  displayName: string;
31
36
  role?: string;
32
37
  contactType: string;
33
- notes?: string;
38
+ // Nullable on the wire (`contactSchema`, and the gateway's ContactRead):
39
+ // a contact with no notes reads as null, not as an absent field.
40
+ notes?: string | null;
34
41
  principalId?: string;
35
42
  createdAt: string | number;
36
43
  updatedAt: string | number;
@@ -51,6 +58,8 @@ interface ContactPromptResult {
51
58
  address?: string;
52
59
  channelId?: string;
53
60
  contactId?: string;
61
+ /** Whether the channel is attested, as the guardian's checkbox left it. */
62
+ verified?: boolean;
54
63
  }
55
64
 
56
65
  // ---------------------------------------------------------------------------
@@ -156,6 +165,213 @@ function formatContactDetail(
156
165
  return lines.join("\n");
157
166
  }
158
167
 
168
+ /**
169
+ * Parse `--timeout`, which bounds how long the form stays open. Returns null
170
+ * (after reporting) when the value is out of range, so a bad number fails here
171
+ * rather than as a schema rejection after the round trip.
172
+ */
173
+ function parseFormTimeout(
174
+ raw: string | undefined,
175
+ cmd: Command,
176
+ ): number | null {
177
+ if (raw === undefined) {
178
+ return GUARDIAN_FORM_DEFAULT_TIMEOUT_MS;
179
+ }
180
+ // The whole token has to be digits: parseInt takes a valid prefix, so
181
+ // "1e3" would pass as 1 and "100.5" as 100, quietly giving a form a
182
+ // fraction of the wait it was asked for.
183
+ const parsed = /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
184
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
185
+ writeError(
186
+ cmd,
187
+ `Invalid --timeout "${raw}": expected a positive whole number of milliseconds`,
188
+ );
189
+ process.exitCode = 1;
190
+ return null;
191
+ }
192
+ if (parsed > GUARDIAN_FORM_MAX_TIMEOUT_MS) {
193
+ writeError(
194
+ cmd,
195
+ `Invalid --timeout "${raw}": the longest a form stays open is ${GUARDIAN_FORM_MAX_TIMEOUT_MS}ms`,
196
+ );
197
+ process.exitCode = 1;
198
+ return null;
199
+ }
200
+ return parsed;
201
+ }
202
+
203
+ interface ContactRecordPromptBody {
204
+ operation: "create" | "update" | "delete";
205
+ contactId?: string;
206
+ currentDisplayName?: string;
207
+ currentNotes?: string;
208
+ channels?: Array<{ type: string; address: string }>;
209
+ displayName?: string;
210
+ notes?: string;
211
+ notesProposed?: boolean;
212
+ label?: string;
213
+ description?: string;
214
+ }
215
+
216
+ /**
217
+ * Read a contact before proposing a write against it, so a bad id fails here
218
+ * rather than after the guardian has answered a form about the wrong person.
219
+ * The read is the gateway-relayed one, so it reflects the source of truth.
220
+ */
221
+ async function readContactForPrompt(
222
+ id: string,
223
+ cmd: Command,
224
+ /**
225
+ * Whether a contact the read cannot see should still reach the form. A
226
+ * dual-write gap can leave a contact in the assistant mirror that this
227
+ * gateway-backed read misses, and deleting one of those is supported, so a
228
+ * delete carries on with a bare record rather than refusing an id the
229
+ * guardian can see. An update cannot: it needs the stored values to know
230
+ * what the guardian actually changed.
231
+ */
232
+ allowUnreadable = false,
233
+ ): Promise<ContactWithChannels | null> {
234
+ const r = await cliIpcCall<{ ok: boolean; contact: ContactWithChannels }>(
235
+ "getContact",
236
+ { pathParams: { id } },
237
+ );
238
+ if (r.ok) {
239
+ return r.result!.contact;
240
+ }
241
+ if (allowUnreadable && r.statusCode === 404) {
242
+ return {
243
+ id,
244
+ displayName: id,
245
+ contactType: "human",
246
+ createdAt: 0,
247
+ updatedAt: 0,
248
+ interactionCount: null,
249
+ channels: [],
250
+ };
251
+ }
252
+
253
+ exitFromIpcResult(
254
+ r as { ok: false; error?: string; statusCode?: number },
255
+ cmd,
256
+ );
257
+ return null;
258
+ }
259
+
260
+ /**
261
+ * Park on the guardian's answer to a contact-record form, then report what was
262
+ * actually written. The submitted values can differ from the proposed ones, so
263
+ * the result is re-read rather than echoed back from the request.
264
+ */
265
+ async function runRecordPrompt(
266
+ body: ContactRecordPromptBody,
267
+ timeout: string | undefined,
268
+ cmd: Command,
269
+ ): Promise<void> {
270
+ const timeoutMs = parseFormTimeout(timeout, cmd);
271
+ if (timeoutMs === null) {
272
+ return;
273
+ }
274
+ const r = await cliIpcCall<{
275
+ ok: boolean;
276
+ error?: string;
277
+ contactId?: string;
278
+ notesSaved?: boolean;
279
+ nothingWritten?: boolean;
280
+ }>(
281
+ "contacts_record_prompt",
282
+ { body: { ...body, timeoutMs } },
283
+ { timeoutMs: guardianFormCallBudgetMs(timeoutMs) },
284
+ );
285
+
286
+ if (!r.ok) {
287
+ return exitFromIpcResult(
288
+ r as { ok: false; error?: string; statusCode?: number },
289
+ cmd,
290
+ );
291
+ }
292
+
293
+ if (!r.result?.ok) {
294
+ writeError(cmd, r.result?.error ?? "Contact form failed");
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+
299
+ const contactId = r.result.contactId;
300
+ // Notes are stored apart from the rest of the record, so they can be the one
301
+ // part that does not land. Whether that leaves nothing behind is the
302
+ // gateway's to say: the guardian submits only the fields they changed, so
303
+ // what this command proposed is no guide to what was written.
304
+ const notesLost = r.result.notesSaved === false;
305
+ const nothingWritten = r.result.nothingWritten === true;
306
+
307
+ if (body.operation === "delete") {
308
+ const deleted = body.currentDisplayName ?? contactId ?? body.contactId;
309
+ if (shouldOutputJson(cmd)) {
310
+ writeOutput(cmd, { ok: true, deleted: true, contactId });
311
+ } else {
312
+ process.stdout.write(`Deleted contact: ${deleted} (${body.contactId})\n`);
313
+ }
314
+ return;
315
+ }
316
+
317
+ if (!contactId) {
318
+ writeError(cmd, "Contact form returned no contact id");
319
+ process.exitCode = 1;
320
+ return;
321
+ }
322
+
323
+ const verb = body.operation === "create" ? "Created" : "Updated";
324
+
325
+ // The write is already done and the guardian has already answered. This read
326
+ // is only for what to print, so a failure here reports the write with less
327
+ // detail rather than reporting the command as failed: an exit code that says
328
+ // otherwise invites a retry, and retrying a create makes a second contact.
329
+ const read = await cliIpcCall<{ ok: boolean; contact: ContactWithChannels }>(
330
+ "getContact",
331
+ { pathParams: { id: contactId } },
332
+ );
333
+ const written = read.ok ? read.result?.contact : undefined;
334
+
335
+ if (nothingWritten) {
336
+ // Nothing the caller asked for landed, so this is a failure however it is
337
+ // read: one object, and a nonzero exit.
338
+ if (shouldOutputJson(cmd)) {
339
+ writeOutput(cmd, {
340
+ ok: false,
341
+ error: "The contact's notes could not be saved",
342
+ contactId,
343
+ notesSaved: false,
344
+ });
345
+ } else {
346
+ writeError(cmd, "The contact's notes could not be saved");
347
+ }
348
+ process.exitCode = 1;
349
+ return;
350
+ }
351
+
352
+ if (shouldOutputJson(cmd)) {
353
+ // One object, whatever happened: a second one saying otherwise reads as a
354
+ // failure to anything parsing this, and a retried create makes a duplicate.
355
+ writeOutput(cmd, {
356
+ ok: true,
357
+ ...(written ? { contact: written } : { contactId }),
358
+ ...(notesLost ? { notesSaved: false } : {}),
359
+ });
360
+ return;
361
+ }
362
+
363
+ if (written) {
364
+ process.stdout.write(`${verb} contact:\n`);
365
+ process.stdout.write(formatContactDetail(written) + "\n");
366
+ } else {
367
+ process.stdout.write(`${verb} contact: ${contactId}\n`);
368
+ writeError(cmd, "Could not read the contact back to display it");
369
+ }
370
+ if (notesLost) {
371
+ writeError(cmd, "The contact was saved, but its notes were not");
372
+ }
373
+ }
374
+
159
375
  export function registerContactsCommand(program: Command): void {
160
376
  registerCommand(program, {
161
377
  name: contactsHelp.name,
@@ -250,6 +466,123 @@ export function registerContactsCommand(program: Command): void {
250
466
  },
251
467
  );
252
468
 
469
+ // -----------------------------------------------------------------------
470
+ // create / update / delete
471
+ //
472
+ // Each opens a form in the guardian's app and blocks on their answer.
473
+ // The daemon writes nothing: the guardian's client posts the confirmed
474
+ // record straight to the gateway, so an unattended assistant cannot
475
+ // change the contact graph.
476
+ // -----------------------------------------------------------------------
477
+
478
+ subcommand(contacts, "create").action(
479
+ async (
480
+ opts: {
481
+ name?: string;
482
+ notes?: string;
483
+ label?: string;
484
+ description?: string;
485
+ timeout?: string;
486
+ },
487
+ cmd: Command,
488
+ ) => {
489
+ await runRecordPrompt(
490
+ {
491
+ operation: "create",
492
+ displayName: opts.name,
493
+ notes: opts.notes,
494
+ label: opts.label,
495
+ description: opts.description,
496
+ },
497
+ opts.timeout,
498
+ cmd,
499
+ );
500
+ },
501
+ );
502
+
503
+ subcommand(contacts, "update").action(
504
+ async (
505
+ id: string,
506
+ opts: {
507
+ name?: string;
508
+ notes?: string;
509
+ label?: string;
510
+ description?: string;
511
+ timeout?: string;
512
+ },
513
+ cmd: Command,
514
+ ) => {
515
+ if (!opts.name && opts.notes === undefined) {
516
+ writeError(
517
+ cmd,
518
+ "At least one of --name or --notes must be provided",
519
+ );
520
+ process.exitCode = 1;
521
+ return;
522
+ }
523
+ const current = await readContactForPrompt(id, cmd);
524
+ if (!current) {
525
+ return;
526
+ }
527
+ await runRecordPrompt(
528
+ {
529
+ operation: "update",
530
+ contactId: id,
531
+ currentDisplayName: current.displayName,
532
+ // What is stored, so the form can tell an accepted proposal from
533
+ // a field the guardian left alone.
534
+ currentNotes: current.notes ?? undefined,
535
+ displayName: opts.name,
536
+ // Seed the form's notes field with what the contact already has,
537
+ // so a name-only edit does not show an empty box the guardian
538
+ // then submits over their notes. `??` keeps an explicit
539
+ // `--notes ""` as the deliberate clear it is, and collapses the
540
+ // null a notes-less contact reads as into an omitted field.
541
+ notes: opts.notes ?? current.notes ?? undefined,
542
+ // An explicit --notes is a change the guardian confirms, even
543
+ // when it matches what is stored: comparison alone cannot tell
544
+ // that from stored notes the read could not see.
545
+ notesProposed: opts.notes !== undefined,
546
+ label: opts.label,
547
+ description: opts.description,
548
+ },
549
+ opts.timeout,
550
+ cmd,
551
+ );
552
+ },
553
+ );
554
+
555
+ subcommand(contacts, "delete").action(
556
+ async (
557
+ id: string,
558
+ opts: { label?: string; description?: string; timeout?: string },
559
+ cmd: Command,
560
+ ) => {
561
+ const current = await readContactForPrompt(id, cmd, true);
562
+ if (!current) {
563
+ return;
564
+ }
565
+ await runRecordPrompt(
566
+ {
567
+ operation: "delete",
568
+ contactId: id,
569
+ currentDisplayName: current.displayName,
570
+ // Two contacts can share a name, and deleting one takes its
571
+ // channels with it. The confirmation has to say which contact
572
+ // this is and what access is about to be lost.
573
+ channels: current.channels.map((ch) => ({
574
+ type: ch.type,
575
+ address: ch.address,
576
+ })),
577
+ label: opts.label,
578
+ description: opts.description,
579
+ },
580
+ opts.timeout,
581
+ cmd,
582
+ );
583
+ },
584
+ );
585
+
253
586
  // -----------------------------------------------------------------------
254
587
  // prompt
255
588
  // -----------------------------------------------------------------------
@@ -268,7 +601,10 @@ export function registerContactsCommand(program: Command): void {
268
601
  },
269
602
  cmd: Command,
270
603
  ) => {
271
- const timeoutMs = opts.timeout ? parseInt(opts.timeout, 10) : 310_000;
604
+ const timeoutMs = parseFormTimeout(opts.timeout, cmd);
605
+ if (timeoutMs === null) {
606
+ return;
607
+ }
272
608
  const r = await cliIpcCall<ContactPromptResult>(
273
609
  "contacts_prompt",
274
610
  {
@@ -280,9 +616,10 @@ export function registerContactsCommand(program: Command): void {
280
616
  label: opts.label,
281
617
  description: opts.description,
282
618
  verify: opts.verify === true,
619
+ timeoutMs,
283
620
  },
284
621
  },
285
- { timeoutMs },
622
+ { timeoutMs: guardianFormCallBudgetMs(timeoutMs) },
286
623
  );
287
624
 
288
625
  if (!r.ok) {
@@ -306,7 +643,9 @@ export function registerContactsCommand(program: Command): void {
306
643
  `Registered ${result.channelType} channel: ${result.address}\n` +
307
644
  ` Channel ID: ${result.channelId}\n` +
308
645
  ` Contact ID: ${result.contactId}\n` +
309
- ` Status: ${opts.verify ? "verified" : "unverified"}\n`,
646
+ // The guardian's checkbox decides this, so report what the
647
+ // channel is rather than what the flag asked for.
648
+ ` Status: ${result.verified ? "verified" : "unverified"}\n`,
310
649
  );
311
650
  }
312
651
  },
@@ -48,6 +48,7 @@ function findHoldingPids(paths: string[]): number[] {
48
48
  encoding: "utf8",
49
49
  timeout: 5000,
50
50
  stdio: ["pipe", "pipe", "pipe"],
51
+ windowsHide: true,
51
52
  });
52
53
  for (const line of output.trim().split("\n")) {
53
54
  const pid = parseInt(line, 10);
@@ -778,8 +778,10 @@ form for any service other than the provider's default.
778
778
 
779
779
  The body (-d) is JSON by default. Give a non-JSON Content-Type and the body
780
780
  travels to the provider exactly as written, which is what multipart, XML, and
781
- form-encoded payloads need. Body files (-d @<path>) are read as UTF-8 text, so
782
- a payload that is not valid UTF-8 cannot be sent this way.
781
+ form-encoded payloads need. Body files (-d @<path>) and stdin (-d @-) are
782
+ read as raw bytes. Valid UTF-8 stays text (or JSON when the Content-Type is
783
+ JSON). Anything else, including PDFs and other binary files, is sent to the
784
+ provider byte for byte.
783
785
 
784
786
  Note: The Authorization header is set automatically. User-supplied
785
787
  -H "Authorization: ..." will be overridden by the OAuth bearer token.
@@ -790,7 +792,8 @@ Examples:
790
792
  $ assistant oauth request --provider twitter -X POST -d '{"text":"Hello"}' https://api.x.com/2/tweets
791
793
  $ assistant oauth request --provider google https://www.googleapis.com/calendar/v3/calendars/primary/events
792
794
  $ assistant oauth request --provider slack -H "Content-Type: application/json" -d '{"channel":"C123"}' /api/chat.postMessage --json
793
- $ assistant oauth request --provider google -X POST -H "Content-Type: multipart/related; boundary=b" -d @upload.txt "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"`,
795
+ $ assistant oauth request --provider google -X POST -H "Content-Type: multipart/related; boundary=b" -d @upload.txt "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"
796
+ $ assistant oauth request --provider google -X POST -H "Content-Type: application/pdf" -d @report.pdf "https://www.googleapis.com/upload/drive/v3/files?uploadType=media"`,
794
797
  },
795
798
  {
796
799
  name: "disconnect",