@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,1080 @@
1
+ import * as net from 'node:net';
2
+ import { loadRawConfig, saveRawConfig } from '../../config/loader.js';
3
+ import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
4
+ import { upsertCredentialMetadata, deleteCredentialMetadata } from '../../tools/credentials/metadata-store.js';
5
+ import { readHttpToken } from '../../util/platform.js';
6
+ import {
7
+ hasTwilioCredentials,
8
+ listIncomingPhoneNumbers,
9
+ searchAvailableNumbers,
10
+ provisionPhoneNumber,
11
+ releasePhoneNumber,
12
+ fetchMessageStatus,
13
+ getPhoneNumberSid,
14
+ getTollFreeVerificationStatus,
15
+ getTollFreeVerificationBySid,
16
+ submitTollFreeVerification,
17
+ updateTollFreeVerification,
18
+ deleteTollFreeVerification,
19
+ type TollFreeVerificationSubmitParams,
20
+ } from '../../calls/twilio-rest.js';
21
+ import type { IngressConfig } from '../../inbound/public-ingress-urls.js';
22
+ import { syncTwilioWebhooks } from './config-ingress.js';
23
+ import { getReadinessService } from './config-channels.js';
24
+ import type { TwilioConfigRequest } from '../ipc-protocol.js';
25
+ import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
26
+ import { getGatewayInternalBaseUrl } from '../../config/env.js';
27
+
28
+ /** In-memory store for the last SMS send test result. Shared between sms_send_test and sms_doctor. */
29
+ let _lastTestResult: {
30
+ messageSid: string;
31
+ to: string;
32
+ initialStatus: string;
33
+ finalStatus: string;
34
+ errorCode?: string;
35
+ errorMessage?: string;
36
+ timestamp: number;
37
+ } | undefined;
38
+
39
+ /** Map a Twilio error code to a human-readable remediation suggestion. */
40
+ function mapTwilioErrorRemediation(errorCode: string | undefined): string | undefined {
41
+ if (!errorCode) return undefined;
42
+ const map: Record<string, string> = {
43
+ '30003': 'Unreachable destination. The handset may be off or out of service.',
44
+ '30004': 'Message blocked by carrier or recipient.',
45
+ '30005': 'Unknown destination phone number. Verify the number is valid.',
46
+ '30006': 'Landline or unreachable carrier. SMS cannot be delivered to this number.',
47
+ '30007': 'Message flagged as spam by carrier. Adjust content or register for A2P.',
48
+ '30008': 'Unknown error from the carrier network.',
49
+ '21610': 'Recipient has opted out (STOP). Cannot send until they opt back in.',
50
+ };
51
+ return map[errorCode];
52
+ }
53
+
54
+ const TWILIO_USE_CASE_ALIASES: Record<string, string> = {
55
+ ACCOUNT_NOTIFICATION: 'ACCOUNT_NOTIFICATIONS',
56
+ DELIVERY_NOTIFICATION: 'DELIVERY_NOTIFICATIONS',
57
+ FRAUD_ALERT: 'FRAUD_ALERT_MESSAGING',
58
+ POLLING_AND_VOTING: 'POLLING_AND_VOTING_NON_POLITICAL',
59
+ };
60
+
61
+ const TWILIO_VALID_USE_CASE_CATEGORIES = [
62
+ 'TWO_FACTOR_AUTHENTICATION',
63
+ 'ACCOUNT_NOTIFICATIONS',
64
+ 'CUSTOMER_CARE',
65
+ 'CHARITY_NONPROFIT',
66
+ 'DELIVERY_NOTIFICATIONS',
67
+ 'FRAUD_ALERT_MESSAGING',
68
+ 'EVENTS',
69
+ 'HIGHER_EDUCATION',
70
+ 'K12',
71
+ 'MARKETING',
72
+ 'POLLING_AND_VOTING_NON_POLITICAL',
73
+ 'POLITICAL_ELECTION_CAMPAIGNS',
74
+ 'PUBLIC_SERVICE_ANNOUNCEMENT',
75
+ 'SECURITY_ALERT',
76
+ ] as const;
77
+
78
+ function normalizeUseCaseCategories(rawCategories: string[]): string[] {
79
+ const normalized = rawCategories.map((value) => TWILIO_USE_CASE_ALIASES[value] ?? value);
80
+ return Array.from(new Set(normalized));
81
+ }
82
+
83
+ export async function handleTwilioConfig(
84
+ msg: TwilioConfigRequest,
85
+ socket: net.Socket,
86
+ ctx: HandlerContext,
87
+ ): Promise<void> {
88
+ try {
89
+ if (msg.action === 'get') {
90
+ const hasCredentials = hasTwilioCredentials();
91
+ const raw = loadRawConfig();
92
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
93
+ // When assistantId is provided, look up in assistantPhoneNumbers first,
94
+ // fall back to the legacy phoneNumber field
95
+ let phoneNumber: string;
96
+ if (msg.assistantId) {
97
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
98
+ phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
99
+ } else {
100
+ phoneNumber = (sms.phoneNumber as string) ?? '';
101
+ }
102
+ ctx.send(socket, {
103
+ type: 'twilio_config_response',
104
+ success: true,
105
+ hasCredentials,
106
+ phoneNumber: phoneNumber || undefined,
107
+ });
108
+ } else if (msg.action === 'set_credentials') {
109
+ if (!msg.accountSid || !msg.authToken) {
110
+ ctx.send(socket, {
111
+ type: 'twilio_config_response',
112
+ success: false,
113
+ hasCredentials: hasTwilioCredentials(),
114
+ error: 'accountSid and authToken are required for set_credentials action',
115
+ });
116
+ return;
117
+ }
118
+
119
+ // Validate credentials by calling the Twilio API
120
+ const authHeader = 'Basic ' + Buffer.from(`${msg.accountSid}:${msg.authToken}`).toString('base64');
121
+ try {
122
+ const res = await fetch(
123
+ `https://api.twilio.com/2010-04-01/Accounts/${msg.accountSid}.json`,
124
+ {
125
+ method: 'GET',
126
+ headers: { Authorization: authHeader },
127
+ },
128
+ );
129
+ if (!res.ok) {
130
+ const body = await res.text();
131
+ ctx.send(socket, {
132
+ type: 'twilio_config_response',
133
+ success: false,
134
+ hasCredentials: hasTwilioCredentials(),
135
+ error: `Twilio API validation failed (${res.status}): ${body}`,
136
+ });
137
+ return;
138
+ }
139
+ } catch (err) {
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ ctx.send(socket, {
142
+ type: 'twilio_config_response',
143
+ success: false,
144
+ hasCredentials: hasTwilioCredentials(),
145
+ error: `Failed to validate Twilio credentials: ${message}`,
146
+ });
147
+ return;
148
+ }
149
+
150
+ // Store credentials securely
151
+ const sidStored = setSecureKey('credential:twilio:account_sid', msg.accountSid);
152
+ if (!sidStored) {
153
+ ctx.send(socket, {
154
+ type: 'twilio_config_response',
155
+ success: false,
156
+ hasCredentials: false,
157
+ error: 'Failed to store Account SID in secure storage',
158
+ });
159
+ return;
160
+ }
161
+
162
+ const tokenStored = setSecureKey('credential:twilio:auth_token', msg.authToken);
163
+ if (!tokenStored) {
164
+ // Roll back the Account SID
165
+ deleteSecureKey('credential:twilio:account_sid');
166
+ ctx.send(socket, {
167
+ type: 'twilio_config_response',
168
+ success: false,
169
+ hasCredentials: false,
170
+ error: 'Failed to store Auth Token in secure storage',
171
+ });
172
+ return;
173
+ }
174
+
175
+ upsertCredentialMetadata('twilio', 'account_sid', {});
176
+ upsertCredentialMetadata('twilio', 'auth_token', {});
177
+
178
+ ctx.send(socket, {
179
+ type: 'twilio_config_response',
180
+ success: true,
181
+ hasCredentials: true,
182
+ });
183
+ } else if (msg.action === 'clear_credentials') {
184
+ // Only clear authentication credentials (Account SID and Auth Token).
185
+ // Preserve the phone number in both config (sms.phoneNumber) and secure
186
+ // key (credential:twilio:phone_number) so that re-entering credentials
187
+ // resumes working without needing to reassign the number.
188
+ deleteSecureKey('credential:twilio:account_sid');
189
+ deleteSecureKey('credential:twilio:auth_token');
190
+ deleteCredentialMetadata('twilio', 'account_sid');
191
+ deleteCredentialMetadata('twilio', 'auth_token');
192
+
193
+ ctx.send(socket, {
194
+ type: 'twilio_config_response',
195
+ success: true,
196
+ hasCredentials: false,
197
+ });
198
+ } else if (msg.action === 'provision_number') {
199
+ if (!hasTwilioCredentials()) {
200
+ ctx.send(socket, {
201
+ type: 'twilio_config_response',
202
+ success: false,
203
+ hasCredentials: false,
204
+ error: 'Twilio credentials not configured. Set credentials first.',
205
+ });
206
+ return;
207
+ }
208
+
209
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
210
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
211
+ const country = msg.country ?? 'US';
212
+
213
+ // Search for an available number
214
+ const available = await searchAvailableNumbers(accountSid, authToken, country, msg.areaCode);
215
+ if (available.length === 0) {
216
+ ctx.send(socket, {
217
+ type: 'twilio_config_response',
218
+ success: false,
219
+ hasCredentials: true,
220
+ error: `No available phone numbers found for country=${country}${msg.areaCode ? ` areaCode=${msg.areaCode}` : ''}`,
221
+ });
222
+ return;
223
+ }
224
+
225
+ // Purchase the first available number
226
+ const purchased = await provisionPhoneNumber(accountSid, authToken, available[0].phoneNumber);
227
+
228
+ // Auto-assign: persist the purchased number in secure storage and config
229
+ // (same persistence as assign_number for consistency)
230
+ const phoneStored = setSecureKey('credential:twilio:phone_number', purchased.phoneNumber);
231
+ if (!phoneStored) {
232
+ ctx.send(socket, {
233
+ type: 'twilio_config_response',
234
+ success: false,
235
+ hasCredentials: hasTwilioCredentials(),
236
+ phoneNumber: purchased.phoneNumber,
237
+ error: `Phone number ${purchased.phoneNumber} was purchased but could not be saved. Use assign_number to assign it manually.`,
238
+ });
239
+ return;
240
+ }
241
+
242
+ const raw = loadRawConfig();
243
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
244
+ // When assistantId is provided, only set the legacy global phoneNumber
245
+ // if it's not already set — this prevents multi-assistant assignments
246
+ // from clobbering each other's outbound SMS number.
247
+ if (msg.assistantId) {
248
+ if (!sms.phoneNumber) {
249
+ sms.phoneNumber = purchased.phoneNumber;
250
+ }
251
+ } else {
252
+ sms.phoneNumber = purchased.phoneNumber;
253
+ }
254
+ // When assistantId is provided, also persist into the per-assistant mapping
255
+ if (msg.assistantId) {
256
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
257
+ mapping[msg.assistantId] = purchased.phoneNumber;
258
+ sms.assistantPhoneNumbers = mapping;
259
+ }
260
+
261
+ const wasSuppressed = ctx.suppressConfigReload;
262
+ ctx.setSuppressConfigReload(true);
263
+ try {
264
+ saveRawConfig({ ...raw, sms });
265
+ } catch (err) {
266
+ ctx.setSuppressConfigReload(wasSuppressed);
267
+ throw err;
268
+ }
269
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
270
+
271
+ // Best-effort webhook configuration — non-fatal so the number is
272
+ // still usable even if ingress isn't configured yet.
273
+ const webhookResult = await syncTwilioWebhooks(
274
+ purchased.phoneNumber,
275
+ accountSid,
276
+ authToken,
277
+ loadRawConfig() as IngressConfig,
278
+ );
279
+
280
+ ctx.send(socket, {
281
+ type: 'twilio_config_response',
282
+ success: true,
283
+ hasCredentials: true,
284
+ phoneNumber: purchased.phoneNumber,
285
+ warning: webhookResult.warning,
286
+ });
287
+ } else if (msg.action === 'assign_number') {
288
+ if (!msg.phoneNumber) {
289
+ ctx.send(socket, {
290
+ type: 'twilio_config_response',
291
+ success: false,
292
+ hasCredentials: hasTwilioCredentials(),
293
+ error: 'phoneNumber is required for assign_number action',
294
+ });
295
+ return;
296
+ }
297
+
298
+ // Persist the phone number in the secure credential store so the
299
+ // active Twilio runtime can read it via credential:twilio:phone_number
300
+ const phoneStored = setSecureKey('credential:twilio:phone_number', msg.phoneNumber);
301
+ if (!phoneStored) {
302
+ ctx.send(socket, {
303
+ type: 'twilio_config_response',
304
+ success: false,
305
+ hasCredentials: hasTwilioCredentials(),
306
+ error: 'Failed to store phone number in secure storage',
307
+ });
308
+ return;
309
+ }
310
+
311
+ // Also persist in assistant config (non-secret) for the UI
312
+ const raw = loadRawConfig();
313
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
314
+ // When assistantId is provided, only set the legacy global phoneNumber
315
+ // if it's not already set — this prevents multi-assistant assignments
316
+ // from clobbering each other's outbound SMS number.
317
+ if (msg.assistantId) {
318
+ if (!sms.phoneNumber) {
319
+ sms.phoneNumber = msg.phoneNumber;
320
+ }
321
+ } else {
322
+ sms.phoneNumber = msg.phoneNumber;
323
+ }
324
+ // When assistantId is provided, also persist into the per-assistant mapping
325
+ if (msg.assistantId) {
326
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
327
+ mapping[msg.assistantId] = msg.phoneNumber;
328
+ sms.assistantPhoneNumbers = mapping;
329
+ }
330
+
331
+ const wasSuppressed = ctx.suppressConfigReload;
332
+ ctx.setSuppressConfigReload(true);
333
+ try {
334
+ saveRawConfig({ ...raw, sms });
335
+ } catch (err) {
336
+ ctx.setSuppressConfigReload(wasSuppressed);
337
+ throw err;
338
+ }
339
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
340
+
341
+ // Best-effort webhook configuration when credentials are available
342
+ let webhookWarning: string | undefined;
343
+ if (hasTwilioCredentials()) {
344
+ const acctSid = getSecureKey('credential:twilio:account_sid')!;
345
+ const acctToken = getSecureKey('credential:twilio:auth_token')!;
346
+ const webhookResult = await syncTwilioWebhooks(
347
+ msg.phoneNumber,
348
+ acctSid,
349
+ acctToken,
350
+ loadRawConfig() as IngressConfig,
351
+ );
352
+ webhookWarning = webhookResult.warning;
353
+ }
354
+
355
+ ctx.send(socket, {
356
+ type: 'twilio_config_response',
357
+ success: true,
358
+ hasCredentials: hasTwilioCredentials(),
359
+ phoneNumber: msg.phoneNumber,
360
+ warning: webhookWarning,
361
+ });
362
+ } else if (msg.action === 'list_numbers') {
363
+ if (!hasTwilioCredentials()) {
364
+ ctx.send(socket, {
365
+ type: 'twilio_config_response',
366
+ success: false,
367
+ hasCredentials: false,
368
+ error: 'Twilio credentials not configured. Set credentials first.',
369
+ });
370
+ return;
371
+ }
372
+
373
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
374
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
375
+ const numbers = await listIncomingPhoneNumbers(accountSid, authToken);
376
+
377
+ ctx.send(socket, {
378
+ type: 'twilio_config_response',
379
+ success: true,
380
+ hasCredentials: true,
381
+ numbers,
382
+ });
383
+ } else if (msg.action === 'sms_compliance_status') {
384
+ if (!hasTwilioCredentials()) {
385
+ ctx.send(socket, {
386
+ type: 'twilio_config_response',
387
+ success: false,
388
+ hasCredentials: false,
389
+ error: 'Twilio credentials not configured. Set credentials first.',
390
+ });
391
+ return;
392
+ }
393
+
394
+ const raw = loadRawConfig();
395
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
396
+ let phoneNumber: string;
397
+ if (msg.assistantId) {
398
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
399
+ phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
400
+ } else {
401
+ phoneNumber = (sms.phoneNumber as string) ?? '';
402
+ }
403
+
404
+ if (!phoneNumber) {
405
+ ctx.send(socket, {
406
+ type: 'twilio_config_response',
407
+ success: false,
408
+ hasCredentials: true,
409
+ error: 'No phone number assigned. Assign a number first.',
410
+ });
411
+ return;
412
+ }
413
+
414
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
415
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
416
+
417
+ // Determine number type from prefix
418
+ const tollFreePrefixes = ['+1800', '+1833', '+1844', '+1855', '+1866', '+1877', '+1888'];
419
+ const isTollFree = tollFreePrefixes.some((prefix) => phoneNumber.startsWith(prefix));
420
+ const numberType = isTollFree ? 'toll_free' : 'local_10dlc';
421
+
422
+ if (!isTollFree) {
423
+ // Non-toll-free numbers don't need toll-free verification
424
+ ctx.send(socket, {
425
+ type: 'twilio_config_response',
426
+ success: true,
427
+ hasCredentials: true,
428
+ phoneNumber,
429
+ compliance: { numberType },
430
+ });
431
+ return;
432
+ }
433
+
434
+ // Look up the phone number SID and check verification status
435
+ const phoneSid = await getPhoneNumberSid(accountSid, authToken, phoneNumber);
436
+ if (!phoneSid) {
437
+ ctx.send(socket, {
438
+ type: 'twilio_config_response',
439
+ success: false,
440
+ hasCredentials: true,
441
+ phoneNumber,
442
+ error: `Phone number ${phoneNumber} not found on Twilio account`,
443
+ });
444
+ return;
445
+ }
446
+
447
+ const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneSid);
448
+
449
+ ctx.send(socket, {
450
+ type: 'twilio_config_response',
451
+ success: true,
452
+ hasCredentials: true,
453
+ phoneNumber,
454
+ compliance: {
455
+ numberType,
456
+ verificationSid: verification?.sid,
457
+ verificationStatus: verification?.status,
458
+ rejectionReason: verification?.rejectionReason,
459
+ rejectionReasons: verification?.rejectionReasons,
460
+ errorCode: verification?.errorCode,
461
+ editAllowed: verification?.editAllowed,
462
+ editExpiration: verification?.editExpiration,
463
+ },
464
+ });
465
+ } else if (msg.action === 'sms_submit_tollfree_verification') {
466
+ if (!hasTwilioCredentials()) {
467
+ ctx.send(socket, {
468
+ type: 'twilio_config_response',
469
+ success: false,
470
+ hasCredentials: false,
471
+ error: 'Twilio credentials not configured. Set credentials first.',
472
+ });
473
+ return;
474
+ }
475
+
476
+ const vp = msg.verificationParams;
477
+ if (!vp) {
478
+ ctx.send(socket, {
479
+ type: 'twilio_config_response',
480
+ success: false,
481
+ hasCredentials: true,
482
+ error: 'verificationParams is required for sms_submit_tollfree_verification action',
483
+ });
484
+ return;
485
+ }
486
+
487
+ // Validate required fields
488
+ const requiredFields: Array<[string, unknown]> = [
489
+ ['tollfreePhoneNumberSid', vp.tollfreePhoneNumberSid],
490
+ ['businessName', vp.businessName],
491
+ ['businessWebsite', vp.businessWebsite],
492
+ ['notificationEmail', vp.notificationEmail],
493
+ ['useCaseCategories', vp.useCaseCategories],
494
+ ['useCaseSummary', vp.useCaseSummary],
495
+ ['productionMessageSample', vp.productionMessageSample],
496
+ ['optInImageUrls', vp.optInImageUrls],
497
+ ['optInType', vp.optInType],
498
+ ['messageVolume', vp.messageVolume],
499
+ ];
500
+
501
+ const missing = requiredFields
502
+ .filter(([, v]) => v == null || v === '' || (Array.isArray(v) && v.length === 0))
503
+ .map(([name]) => name);
504
+
505
+ if (missing.length > 0) {
506
+ ctx.send(socket, {
507
+ type: 'twilio_config_response',
508
+ success: false,
509
+ hasCredentials: true,
510
+ error: `Missing required verification fields: ${missing.join(', ')}`,
511
+ });
512
+ return;
513
+ }
514
+
515
+ // Validate enum values
516
+ const normalizedUseCaseCategories = normalizeUseCaseCategories(vp.useCaseCategories ?? []);
517
+ const invalidCategories = normalizedUseCaseCategories.filter(
518
+ (c) => !TWILIO_VALID_USE_CASE_CATEGORIES.includes(c as (typeof TWILIO_VALID_USE_CASE_CATEGORIES)[number]),
519
+ );
520
+ if (invalidCategories.length > 0) {
521
+ ctx.send(socket, {
522
+ type: 'twilio_config_response',
523
+ success: false,
524
+ hasCredentials: true,
525
+ error: `Invalid useCaseCategories: ${invalidCategories.join(', ')}. Valid values: ${TWILIO_VALID_USE_CASE_CATEGORIES.join(', ')}`,
526
+ });
527
+ return;
528
+ }
529
+
530
+ const validOptInTypes = ['VERBAL', 'WEB_FORM', 'PAPER_FORM', 'VIA_TEXT', 'MOBILE_QR_CODE'];
531
+ if (!validOptInTypes.includes(vp.optInType!)) {
532
+ ctx.send(socket, {
533
+ type: 'twilio_config_response',
534
+ success: false,
535
+ hasCredentials: true,
536
+ error: `Invalid optInType: ${vp.optInType}. Valid values: ${validOptInTypes.join(', ')}`,
537
+ });
538
+ return;
539
+ }
540
+
541
+ const validMessageVolumes = [
542
+ '10', '100', '1,000', '10,000', '100,000', '250,000',
543
+ '500,000', '750,000', '1,000,000', '5,000,000', '10,000,000+',
544
+ ];
545
+ if (!validMessageVolumes.includes(vp.messageVolume!)) {
546
+ ctx.send(socket, {
547
+ type: 'twilio_config_response',
548
+ success: false,
549
+ hasCredentials: true,
550
+ error: `Invalid messageVolume: ${vp.messageVolume}. Valid values: ${validMessageVolumes.join(', ')}`,
551
+ });
552
+ return;
553
+ }
554
+
555
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
556
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
557
+
558
+ const submitParams: TollFreeVerificationSubmitParams = {
559
+ tollfreePhoneNumberSid: vp.tollfreePhoneNumberSid!,
560
+ businessName: vp.businessName!,
561
+ businessWebsite: vp.businessWebsite!,
562
+ notificationEmail: vp.notificationEmail!,
563
+ useCaseCategories: normalizedUseCaseCategories,
564
+ useCaseSummary: vp.useCaseSummary!,
565
+ productionMessageSample: vp.productionMessageSample!,
566
+ optInImageUrls: vp.optInImageUrls!,
567
+ optInType: vp.optInType!,
568
+ messageVolume: vp.messageVolume!,
569
+ businessType: vp.businessType ?? 'SOLE_PROPRIETOR',
570
+ customerProfileSid: vp.customerProfileSid,
571
+ };
572
+
573
+ const verification = await submitTollFreeVerification(accountSid, authToken, submitParams);
574
+
575
+ ctx.send(socket, {
576
+ type: 'twilio_config_response',
577
+ success: true,
578
+ hasCredentials: true,
579
+ compliance: {
580
+ numberType: 'toll_free',
581
+ verificationSid: verification.sid,
582
+ verificationStatus: verification.status,
583
+ },
584
+ });
585
+ } else if (msg.action === 'sms_update_tollfree_verification') {
586
+ if (!hasTwilioCredentials()) {
587
+ ctx.send(socket, {
588
+ type: 'twilio_config_response',
589
+ success: false,
590
+ hasCredentials: false,
591
+ error: 'Twilio credentials not configured. Set credentials first.',
592
+ });
593
+ return;
594
+ }
595
+
596
+ if (!msg.verificationSid) {
597
+ ctx.send(socket, {
598
+ type: 'twilio_config_response',
599
+ success: false,
600
+ hasCredentials: true,
601
+ error: 'verificationSid is required for sms_update_tollfree_verification action',
602
+ });
603
+ return;
604
+ }
605
+
606
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
607
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
608
+
609
+ const currentVerification = await getTollFreeVerificationBySid(accountSid, authToken, msg.verificationSid);
610
+ if (!currentVerification) {
611
+ ctx.send(socket, {
612
+ type: 'twilio_config_response',
613
+ success: false,
614
+ hasCredentials: true,
615
+ error: `Verification ${msg.verificationSid} was not found on this Twilio account.`,
616
+ });
617
+ return;
618
+ }
619
+
620
+ if (currentVerification.status === 'TWILIO_REJECTED') {
621
+ const expirationMillis = currentVerification.editExpiration
622
+ ? Date.parse(currentVerification.editExpiration)
623
+ : Number.NaN;
624
+ const editExpired = Number.isFinite(expirationMillis) && Date.now() > expirationMillis;
625
+ if (currentVerification.editAllowed === false || editExpired) {
626
+ const detail = editExpired
627
+ ? `edit_expiration=${currentVerification.editExpiration}`
628
+ : 'edit_allowed=false';
629
+ ctx.send(socket, {
630
+ type: 'twilio_config_response',
631
+ success: false,
632
+ hasCredentials: true,
633
+ error: `Verification ${msg.verificationSid} cannot be updated (${detail}). Delete and resubmit instead.`,
634
+ compliance: {
635
+ numberType: 'toll_free',
636
+ verificationSid: currentVerification.sid,
637
+ verificationStatus: currentVerification.status,
638
+ editAllowed: currentVerification.editAllowed,
639
+ editExpiration: currentVerification.editExpiration,
640
+ },
641
+ });
642
+ return;
643
+ }
644
+ }
645
+
646
+ const updateParams = { ...(msg.verificationParams ?? {}) };
647
+ if (updateParams.useCaseCategories) {
648
+ updateParams.useCaseCategories = normalizeUseCaseCategories(updateParams.useCaseCategories);
649
+ }
650
+
651
+ const verification = await updateTollFreeVerification(
652
+ accountSid,
653
+ authToken,
654
+ msg.verificationSid,
655
+ updateParams,
656
+ );
657
+
658
+ ctx.send(socket, {
659
+ type: 'twilio_config_response',
660
+ success: true,
661
+ hasCredentials: true,
662
+ compliance: {
663
+ numberType: 'toll_free',
664
+ verificationSid: verification.sid,
665
+ verificationStatus: verification.status,
666
+ editAllowed: verification.editAllowed,
667
+ editExpiration: verification.editExpiration,
668
+ },
669
+ });
670
+ } else if (msg.action === 'sms_delete_tollfree_verification') {
671
+ if (!hasTwilioCredentials()) {
672
+ ctx.send(socket, {
673
+ type: 'twilio_config_response',
674
+ success: false,
675
+ hasCredentials: false,
676
+ error: 'Twilio credentials not configured. Set credentials first.',
677
+ });
678
+ return;
679
+ }
680
+
681
+ if (!msg.verificationSid) {
682
+ ctx.send(socket, {
683
+ type: 'twilio_config_response',
684
+ success: false,
685
+ hasCredentials: true,
686
+ error: 'verificationSid is required for sms_delete_tollfree_verification action',
687
+ });
688
+ return;
689
+ }
690
+
691
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
692
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
693
+
694
+ await deleteTollFreeVerification(accountSid, authToken, msg.verificationSid);
695
+
696
+ ctx.send(socket, {
697
+ type: 'twilio_config_response',
698
+ success: true,
699
+ hasCredentials: true,
700
+ warning: 'Toll-free verification deleted. Re-submitting may reset your position in the review queue.',
701
+ });
702
+ } else if (msg.action === 'release_number') {
703
+ if (!hasTwilioCredentials()) {
704
+ ctx.send(socket, {
705
+ type: 'twilio_config_response',
706
+ success: false,
707
+ hasCredentials: false,
708
+ error: 'Twilio credentials not configured. Set credentials first.',
709
+ });
710
+ return;
711
+ }
712
+
713
+ const raw = loadRawConfig();
714
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
715
+ let phoneNumber: string;
716
+ if (msg.phoneNumber) {
717
+ phoneNumber = msg.phoneNumber;
718
+ } else if (msg.assistantId) {
719
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
720
+ phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
721
+ } else {
722
+ phoneNumber = (sms.phoneNumber as string) ?? '';
723
+ }
724
+
725
+ if (!phoneNumber) {
726
+ ctx.send(socket, {
727
+ type: 'twilio_config_response',
728
+ success: false,
729
+ hasCredentials: true,
730
+ error: 'No phone number to release. Specify phoneNumber or ensure one is assigned.',
731
+ });
732
+ return;
733
+ }
734
+
735
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
736
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
737
+
738
+ await releasePhoneNumber(accountSid, authToken, phoneNumber);
739
+
740
+ // Clear the number from config and secure key store
741
+ if (sms.phoneNumber === phoneNumber) {
742
+ delete sms.phoneNumber;
743
+ }
744
+ const assistantPhoneNumbers = sms.assistantPhoneNumbers as Record<string, string> | undefined;
745
+ if (assistantPhoneNumbers) {
746
+ for (const [id, num] of Object.entries(assistantPhoneNumbers)) {
747
+ if (num === phoneNumber) {
748
+ delete assistantPhoneNumbers[id];
749
+ }
750
+ }
751
+ if (Object.keys(assistantPhoneNumbers).length === 0) {
752
+ delete sms.assistantPhoneNumbers;
753
+ }
754
+ }
755
+
756
+ const wasSuppressed = ctx.suppressConfigReload;
757
+ ctx.setSuppressConfigReload(true);
758
+ try {
759
+ saveRawConfig({ ...raw, sms });
760
+ } catch (err) {
761
+ ctx.setSuppressConfigReload(wasSuppressed);
762
+ throw err;
763
+ }
764
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
765
+
766
+ // Clear the phone number from secure key store if it matches
767
+ const storedPhone = getSecureKey('credential:twilio:phone_number');
768
+ if (storedPhone === phoneNumber) {
769
+ deleteSecureKey('credential:twilio:phone_number');
770
+ }
771
+
772
+ ctx.send(socket, {
773
+ type: 'twilio_config_response',
774
+ success: true,
775
+ hasCredentials: true,
776
+ warning: 'Phone number released from Twilio. Any associated toll-free verification context is lost.',
777
+ });
778
+ } else if (msg.action === 'sms_send_test') {
779
+ // ── SMS send test ────────────────────────────────────────────────
780
+ if (!hasTwilioCredentials()) {
781
+ ctx.send(socket, {
782
+ type: 'twilio_config_response',
783
+ success: false,
784
+ hasCredentials: false,
785
+ error: 'Twilio credentials not configured. Set credentials first.',
786
+ });
787
+ return;
788
+ }
789
+
790
+ const to = msg.phoneNumber;
791
+ if (!to) {
792
+ ctx.send(socket, {
793
+ type: 'twilio_config_response',
794
+ success: false,
795
+ hasCredentials: true,
796
+ error: 'phoneNumber is required for sms_send_test action.',
797
+ });
798
+ return;
799
+ }
800
+
801
+ const raw = loadRawConfig();
802
+ const smsSection = (raw?.sms ?? {}) as Record<string, unknown>;
803
+ let from = '';
804
+ // When assistantId is provided, check assistant-scoped phone mapping first
805
+ if (msg.assistantId) {
806
+ const mapping = (smsSection.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
807
+ from = mapping[msg.assistantId] ?? '';
808
+ }
809
+ // Fall back to global phone number
810
+ if (!from) {
811
+ from = (smsSection.phoneNumber as string | undefined)
812
+ || getSecureKey('credential:twilio:phone_number')
813
+ || '';
814
+ }
815
+ if (!from) {
816
+ ctx.send(socket, {
817
+ type: 'twilio_config_response',
818
+ success: false,
819
+ hasCredentials: true,
820
+ error: 'No phone number assigned. Run the twilio-setup skill to assign a number.',
821
+ });
822
+ return;
823
+ }
824
+
825
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
826
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
827
+ const text = msg.text || 'Test SMS from your Vellum assistant';
828
+
829
+ // Send via gateway's /deliver/sms endpoint
830
+ const bearerToken = readHttpToken();
831
+ const gatewayUrl = getGatewayInternalBaseUrl();
832
+
833
+ const sendResp = await fetch(`${gatewayUrl}/deliver/sms`, {
834
+ method: 'POST',
835
+ headers: {
836
+ 'Content-Type': 'application/json',
837
+ ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
838
+ },
839
+ body: JSON.stringify({ to, text, ...(msg.assistantId ? { assistantId: msg.assistantId } : {}) }),
840
+ signal: AbortSignal.timeout(30_000),
841
+ });
842
+
843
+ if (!sendResp.ok) {
844
+ const errBody = await sendResp.text().catch(() => '<unreadable>');
845
+ ctx.send(socket, {
846
+ type: 'twilio_config_response',
847
+ success: false,
848
+ hasCredentials: true,
849
+ error: `SMS send failed (${sendResp.status}): ${errBody}`,
850
+ });
851
+ return;
852
+ }
853
+
854
+ const sendData = await sendResp.json().catch(() => ({})) as {
855
+ messageSid?: string;
856
+ status?: string;
857
+ };
858
+ const messageSid = sendData.messageSid || '';
859
+ const initialStatus = sendData.status || 'unknown';
860
+
861
+ // Poll Twilio for final status (up to 3 times, 2s apart)
862
+ let finalStatus = initialStatus;
863
+ let errorCode: string | undefined;
864
+ let errorMessage: string | undefined;
865
+
866
+ if (messageSid) {
867
+ for (let i = 0; i < 3; i++) {
868
+ await new Promise((r) => setTimeout(r, 2000));
869
+ try {
870
+ const pollResult = await fetchMessageStatus(accountSid, authToken, messageSid);
871
+ finalStatus = pollResult.status;
872
+ errorCode = pollResult.errorCode;
873
+ errorMessage = pollResult.errorMessage;
874
+ // Stop polling if we've reached a terminal status
875
+ if (['delivered', 'undelivered', 'failed'].includes(finalStatus)) break;
876
+ } catch {
877
+ // Polling failure is non-fatal; we'll use the last known status
878
+ break;
879
+ }
880
+ }
881
+ }
882
+
883
+ const testResult = {
884
+ messageSid,
885
+ to,
886
+ initialStatus,
887
+ finalStatus,
888
+ ...(errorCode ? { errorCode } : {}),
889
+ ...(errorMessage ? { errorMessage } : {}),
890
+ };
891
+
892
+ // Store for sms_doctor
893
+ _lastTestResult = { ...testResult, timestamp: Date.now() };
894
+
895
+ ctx.send(socket, {
896
+ type: 'twilio_config_response',
897
+ success: true,
898
+ hasCredentials: true,
899
+ testResult,
900
+ });
901
+
902
+ } else if (msg.action === 'sms_doctor') {
903
+ // ── SMS doctor diagnostic ────────────────────────────────────────
904
+ const hasCredentials = hasTwilioCredentials();
905
+
906
+ // 1. Channel readiness check
907
+ let readinessReady = false;
908
+ const readinessIssues: string[] = [];
909
+ try {
910
+ const readinessService = getReadinessService();
911
+ const snapshots = await readinessService.getReadiness('sms', false, msg.assistantId);
912
+ const snapshot = snapshots[0];
913
+ if (snapshot) {
914
+ readinessReady = snapshot.ready;
915
+ for (const r of snapshot.reasons) {
916
+ readinessIssues.push(r.text);
917
+ }
918
+ } else {
919
+ readinessIssues.push('No readiness snapshot returned for SMS channel');
920
+ }
921
+ } catch (err) {
922
+ readinessIssues.push(`Readiness check failed: ${err instanceof Error ? err.message : String(err)}`);
923
+ }
924
+
925
+ // 2. Compliance status
926
+ let complianceStatus = 'unknown';
927
+ let complianceDetail: string | undefined;
928
+ let complianceRemediation: string | undefined;
929
+ if (hasCredentials) {
930
+ try {
931
+ const raw = loadRawConfig();
932
+ const smsSection = (raw?.sms ?? {}) as Record<string, unknown>;
933
+ let phoneNumber = '';
934
+ if (msg.assistantId) {
935
+ const mapping = (smsSection.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
936
+ phoneNumber = mapping[msg.assistantId] ?? '';
937
+ }
938
+ if (!phoneNumber) {
939
+ phoneNumber = (smsSection.phoneNumber as string | undefined) || getSecureKey('credential:twilio:phone_number') || '';
940
+ }
941
+ if (phoneNumber) {
942
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
943
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
944
+ // Determine number type and verification status
945
+ const isTollFree = phoneNumber.startsWith('+1') && ['800','888','877','866','855','844','833'].some(
946
+ (p) => phoneNumber.startsWith(`+1${p}`),
947
+ );
948
+ if (isTollFree) {
949
+ try {
950
+ const phoneSid = await getPhoneNumberSid(accountSid, authToken, phoneNumber);
951
+ if (!phoneSid) {
952
+ complianceStatus = 'check_failed';
953
+ complianceDetail = `Assigned number ${phoneNumber} was not found on the Twilio account`;
954
+ complianceRemediation = 'Reassign the number in twilio-setup or update credentials to the matching account.';
955
+ } else {
956
+ const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneSid);
957
+ if (verification) {
958
+ const status = verification.status;
959
+ complianceStatus = status;
960
+ complianceDetail = `Toll-free verification: ${status}`;
961
+ if (status === 'TWILIO_APPROVED') {
962
+ complianceRemediation = undefined;
963
+ } else if (status === 'PENDING_REVIEW' || status === 'IN_REVIEW') {
964
+ complianceRemediation = 'Toll-free verification is pending. Messaging may have limited throughput until approved.';
965
+ } else if (status === 'TWILIO_REJECTED') {
966
+ if (verification.editAllowed) {
967
+ complianceRemediation = verification.editExpiration
968
+ ? `Toll-free verification was rejected but can still be edited until ${verification.editExpiration}. Update and resubmit it.`
969
+ : 'Toll-free verification was rejected but can still be edited. Update and resubmit it.';
970
+ } else {
971
+ complianceRemediation = 'Toll-free verification was rejected and is no longer editable. Delete and resubmit it.';
972
+ }
973
+ } else {
974
+ complianceRemediation = 'Submit a toll-free verification to enable full messaging throughput.';
975
+ }
976
+ } else {
977
+ complianceStatus = 'unverified';
978
+ complianceDetail = 'Toll-free number without verification';
979
+ complianceRemediation = 'Submit a toll-free verification request to avoid filtering.';
980
+ }
981
+ }
982
+ } catch {
983
+ complianceStatus = 'check_failed';
984
+ complianceDetail = 'Could not retrieve toll-free verification status';
985
+ }
986
+ } else {
987
+ complianceStatus = 'local_10dlc';
988
+ complianceDetail = 'Local/10DLC number — carrier registration handled externally';
989
+ }
990
+ } else {
991
+ complianceStatus = 'no_number';
992
+ complianceDetail = 'No phone number assigned';
993
+ complianceRemediation = 'Assign a phone number via the twilio-setup skill.';
994
+ }
995
+ } catch {
996
+ complianceStatus = 'check_failed';
997
+ complianceDetail = 'Could not determine compliance status';
998
+ }
999
+ } else {
1000
+ complianceStatus = 'no_credentials';
1001
+ complianceDetail = 'Twilio credentials are not configured';
1002
+ complianceRemediation = 'Set Twilio credentials via the twilio-setup skill.';
1003
+ }
1004
+
1005
+ // 3. Last send test result
1006
+ let lastSend: { status: string; errorCode?: string; remediation?: string } | undefined;
1007
+ if (_lastTestResult) {
1008
+ lastSend = {
1009
+ status: _lastTestResult.finalStatus,
1010
+ ...((_lastTestResult.errorCode) ? { errorCode: _lastTestResult.errorCode } : {}),
1011
+ ...((_lastTestResult.errorCode) ? { remediation: mapTwilioErrorRemediation(_lastTestResult.errorCode) } : {}),
1012
+ };
1013
+ }
1014
+
1015
+ // 4. Determine overall status
1016
+ const actionItems: string[] = [];
1017
+ let overallStatus: 'healthy' | 'degraded' | 'broken' = 'healthy';
1018
+
1019
+ if (!hasCredentials) {
1020
+ overallStatus = 'broken';
1021
+ actionItems.push('Configure Twilio credentials.');
1022
+ }
1023
+ if (!readinessReady) {
1024
+ overallStatus = 'broken';
1025
+ for (const issue of readinessIssues) actionItems.push(issue);
1026
+ }
1027
+ if (complianceStatus === 'unverified' || complianceStatus === 'PENDING_REVIEW' || complianceStatus === 'IN_REVIEW') {
1028
+ if (overallStatus === 'healthy') overallStatus = 'degraded';
1029
+ if (complianceRemediation) actionItems.push(complianceRemediation);
1030
+ }
1031
+ if (complianceStatus === 'TWILIO_REJECTED' || complianceStatus === 'no_number') {
1032
+ overallStatus = 'broken';
1033
+ if (complianceRemediation) actionItems.push(complianceRemediation);
1034
+ }
1035
+ if (_lastTestResult && ['failed', 'undelivered'].includes(_lastTestResult.finalStatus)) {
1036
+ if (overallStatus === 'healthy') overallStatus = 'degraded';
1037
+ const remediation = mapTwilioErrorRemediation(_lastTestResult.errorCode);
1038
+ actionItems.push(remediation || `Last test SMS ${_lastTestResult.finalStatus}. Check Twilio logs for details.`);
1039
+ }
1040
+
1041
+ ctx.send(socket, {
1042
+ type: 'twilio_config_response',
1043
+ success: true,
1044
+ hasCredentials,
1045
+ diagnostics: {
1046
+ readiness: { ready: readinessReady, issues: readinessIssues },
1047
+ compliance: {
1048
+ status: complianceStatus,
1049
+ ...(complianceDetail ? { detail: complianceDetail } : {}),
1050
+ ...(complianceRemediation ? { remediation: complianceRemediation } : {}),
1051
+ },
1052
+ ...(lastSend ? { lastSend } : {}),
1053
+ overallStatus,
1054
+ actionItems,
1055
+ },
1056
+ });
1057
+
1058
+ } else {
1059
+ ctx.send(socket, {
1060
+ type: 'twilio_config_response',
1061
+ success: false,
1062
+ hasCredentials: hasTwilioCredentials(),
1063
+ error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
1064
+ });
1065
+ }
1066
+ } catch (err) {
1067
+ const message = err instanceof Error ? err.message : String(err);
1068
+ log.error({ err }, 'Failed to handle Twilio config');
1069
+ ctx.send(socket, {
1070
+ type: 'twilio_config_response',
1071
+ success: false,
1072
+ hasCredentials: hasTwilioCredentials(),
1073
+ error: message,
1074
+ });
1075
+ }
1076
+ }
1077
+
1078
+ export const twilioHandlers = defineHandlers({
1079
+ twilio_config: handleTwilioConfig,
1080
+ });