@vellumai/assistant 0.3.5 → 0.3.7

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 (487) hide show
  1. package/README.md +51 -0
  2. package/eslint.config.mjs +31 -0
  3. package/package.json +1 -1
  4. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  5. package/scripts/ipc/generate-swift.ts +18 -2
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
  7. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  8. package/src/__tests__/browser-manager.test.ts +1 -0
  9. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  10. package/src/__tests__/call-orchestrator.test.ts +752 -271
  11. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  12. package/src/__tests__/call-recovery.test.ts +3 -0
  13. package/src/__tests__/call-routes-http.test.ts +5 -0
  14. package/src/__tests__/call-store.test.ts +3 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +1260 -85
  16. package/src/__tests__/channel-approval.test.ts +37 -0
  17. package/src/__tests__/channel-approvals.test.ts +4 -65
  18. package/src/__tests__/channel-guardian.test.ts +556 -0
  19. package/src/__tests__/channel-readiness-service.test.ts +74 -7
  20. package/src/__tests__/checker.test.ts +14 -7
  21. package/src/__tests__/clarification-resolver.test.ts +44 -24
  22. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  23. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  24. package/src/__tests__/config-schema.test.ts +12 -7
  25. package/src/__tests__/context-window-manager.test.ts +30 -2
  26. package/src/__tests__/contradiction-checker.test.ts +20 -5
  27. package/src/__tests__/credential-security-invariants.test.ts +6 -2
  28. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  29. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  30. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  31. package/src/__tests__/guardian-action-store.test.ts +123 -0
  32. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  33. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  34. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  35. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  36. package/src/__tests__/handlers-twilio-config.test.ts +126 -0
  37. package/src/__tests__/intent-routing.test.ts +2 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +228 -1
  39. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  40. package/src/__tests__/model-intents.test.ts +96 -0
  41. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  42. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  43. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  44. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  45. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  46. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  47. package/src/__tests__/qdrant-manager.test.ts +27 -20
  48. package/src/__tests__/relay-server.test.ts +779 -40
  49. package/src/__tests__/run-orchestrator-assistant-events.test.ts +2 -0
  50. package/src/__tests__/run-orchestrator.test.ts +20 -4
  51. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  52. package/src/__tests__/runtime-runs.test.ts +16 -0
  53. package/src/__tests__/schedule-store.test.ts +18 -4
  54. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  55. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  56. package/src/__tests__/session-agent-loop.test.ts +857 -0
  57. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  58. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  59. package/src/__tests__/session-profile-injection.test.ts +6 -0
  60. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  61. package/src/__tests__/session-queue.test.ts +6 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +237 -13
  63. package/src/__tests__/session-slash-known.test.ts +6 -0
  64. package/src/__tests__/session-slash-queue.test.ts +6 -0
  65. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  66. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  67. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  68. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  69. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  70. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  71. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  72. package/src/__tests__/skills.test.ts +2 -0
  73. package/src/__tests__/sms-messaging-provider.test.ts +2 -1
  74. package/src/__tests__/starter-task-flow.test.ts +2 -0
  75. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  76. package/src/__tests__/system-prompt.test.ts +2 -0
  77. package/src/__tests__/task-management-tools.test.ts +2 -2
  78. package/src/__tests__/task-runner.test.ts +14 -4
  79. package/src/__tests__/terminal-tools.test.ts +25 -19
  80. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  81. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  82. package/src/__tests__/tool-executor.test.ts +23 -24
  83. package/src/__tests__/trust-store.test.ts +3 -3
  84. package/src/__tests__/twilio-rest.test.ts +29 -0
  85. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  86. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  87. package/src/__tests__/twilio-routes.test.ts +141 -21
  88. package/src/__tests__/user-reference.test.ts +2 -0
  89. package/src/__tests__/voice-quality.test.ts +222 -0
  90. package/src/__tests__/web-search.test.ts +45 -29
  91. package/src/agent/loop.ts +1 -1
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  93. package/src/amazon/client.ts +1418 -0
  94. package/src/amazon/request-extractor.ts +135 -0
  95. package/src/amazon/session.ts +109 -0
  96. package/src/autonomy/autonomy-store.ts +5 -5
  97. package/src/browser-extension-relay/client.ts +124 -0
  98. package/src/browser-extension-relay/protocol.ts +63 -0
  99. package/src/browser-extension-relay/server.ts +177 -0
  100. package/src/bundler/app-bundler.ts +3 -3
  101. package/src/bundler/bundle-signer.ts +1 -1
  102. package/src/bundler/signature-verifier.ts +1 -1
  103. package/src/calls/call-conversation-messages.ts +33 -0
  104. package/src/calls/call-domain.ts +106 -5
  105. package/src/calls/call-orchestrator.ts +252 -54
  106. package/src/calls/call-pointer-messages.ts +53 -0
  107. package/src/calls/call-recovery.ts +3 -8
  108. package/src/calls/call-store.ts +69 -87
  109. package/src/calls/elevenlabs-config.ts +3 -2
  110. package/src/calls/guardian-action-sweep.ts +105 -0
  111. package/src/calls/guardian-dispatch.ts +203 -0
  112. package/src/calls/guardian-question-copy.ts +133 -0
  113. package/src/calls/relay-server.ts +466 -8
  114. package/src/calls/speaker-identification.ts +1 -1
  115. package/src/calls/twilio-config.ts +7 -5
  116. package/src/calls/twilio-provider.ts +6 -4
  117. package/src/calls/twilio-rest.ts +40 -15
  118. package/src/calls/twilio-routes.ts +60 -45
  119. package/src/calls/types.ts +3 -1
  120. package/src/channels/types.ts +25 -0
  121. package/src/cli/amazon.ts +815 -0
  122. package/src/cli/config-commands.ts +2 -2
  123. package/src/cli/core-commands.ts +4 -3
  124. package/src/cli/influencer.ts +244 -0
  125. package/src/cli/map.ts +89 -6
  126. package/src/cli.ts +1 -1
  127. package/src/config/agent-schema.ts +171 -0
  128. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  129. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  130. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  131. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  132. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  133. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  134. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  135. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  136. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  137. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  138. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  139. package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
  140. package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
  141. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  142. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  143. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  144. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  145. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  146. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  147. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  148. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +7 -9
  149. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  150. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
  151. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
  152. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
  153. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
  154. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
  155. package/src/config/bundled-skills/messaging/SKILL.md +12 -2
  156. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  157. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  158. package/src/config/bundled-skills/phone-calls/SKILL.md +86 -21
  159. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  160. package/src/config/bundled-tool-registry.ts +310 -0
  161. package/src/config/calls-schema.ts +181 -0
  162. package/src/config/core-schema.ts +309 -0
  163. package/src/config/defaults.ts +27 -3
  164. package/src/config/env-registry.ts +169 -0
  165. package/src/config/env.ts +175 -0
  166. package/src/config/loader.ts +6 -6
  167. package/src/config/memory-schema.ts +528 -0
  168. package/src/config/sandbox-schema.ts +55 -0
  169. package/src/config/schema.ts +157 -1138
  170. package/src/config/skill-state.ts +1 -1
  171. package/src/config/skills-schema.ts +32 -0
  172. package/src/config/skills.ts +35 -24
  173. package/src/config/system-prompt.ts +107 -56
  174. package/src/config/templates/SOUL.md +1 -1
  175. package/src/config/types.ts +1 -0
  176. package/src/config/user-reference.ts +4 -9
  177. package/src/config/vellum-skills/catalog.json +0 -7
  178. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  179. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
  180. package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
  181. package/src/context/window-manager.ts +27 -7
  182. package/src/daemon/approval-generators.ts +186 -0
  183. package/src/daemon/approved-devices-store.ts +140 -0
  184. package/src/daemon/assistant-attachments.ts +1 -1
  185. package/src/daemon/classifier.ts +35 -32
  186. package/src/daemon/config-watcher.ts +1 -1
  187. package/src/daemon/daemon-control.ts +254 -0
  188. package/src/daemon/handlers/apps.ts +2 -3
  189. package/src/daemon/handlers/config-channels.ts +158 -0
  190. package/src/daemon/handlers/config-inbox.ts +540 -0
  191. package/src/daemon/handlers/config-ingress.ts +231 -0
  192. package/src/daemon/handlers/config-integrations.ts +258 -0
  193. package/src/daemon/handlers/config-model.ts +143 -0
  194. package/src/daemon/handlers/config-parental.ts +163 -0
  195. package/src/daemon/handlers/config-scheduling.ts +172 -0
  196. package/src/daemon/handlers/config-slack.ts +92 -0
  197. package/src/daemon/handlers/config-telegram.ts +301 -0
  198. package/src/daemon/handlers/config-tools.ts +177 -0
  199. package/src/daemon/handlers/config-trust.ts +104 -0
  200. package/src/daemon/handlers/config-twilio.ts +1080 -0
  201. package/src/daemon/handlers/config.ts +53 -2463
  202. package/src/daemon/handlers/diagnostics.ts +1 -1
  203. package/src/daemon/handlers/dictation.ts +4 -6
  204. package/src/daemon/handlers/documents.ts +18 -32
  205. package/src/daemon/handlers/index.ts +9 -0
  206. package/src/daemon/handlers/misc.ts +3 -5
  207. package/src/daemon/handlers/pairing.ts +98 -0
  208. package/src/daemon/handlers/sessions.ts +74 -5
  209. package/src/daemon/handlers/shared.ts +3 -1
  210. package/src/daemon/handlers/skills.ts +1 -1
  211. package/src/daemon/handlers/twitter-auth.ts +2 -0
  212. package/src/daemon/handlers/work-items.ts +2 -2
  213. package/src/daemon/handlers/workspace-files.ts +4 -3
  214. package/src/daemon/install-cli-launchers.ts +113 -0
  215. package/src/daemon/ipc-contract/apps.ts +356 -0
  216. package/src/daemon/ipc-contract/browser.ts +74 -0
  217. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  218. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  219. package/src/daemon/ipc-contract/documents.ts +74 -0
  220. package/src/daemon/ipc-contract/inbox.ts +209 -0
  221. package/src/daemon/ipc-contract/integrations.ts +284 -0
  222. package/src/daemon/ipc-contract/memory.ts +48 -0
  223. package/src/daemon/ipc-contract/messages.ts +211 -0
  224. package/src/daemon/ipc-contract/pairing.ts +45 -0
  225. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  226. package/src/daemon/ipc-contract/schedules.ts +97 -0
  227. package/src/daemon/ipc-contract/sessions.ts +321 -0
  228. package/src/daemon/ipc-contract/shared.ts +42 -0
  229. package/src/daemon/ipc-contract/skills.ts +120 -0
  230. package/src/daemon/ipc-contract/subagents.ts +58 -0
  231. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  232. package/src/daemon/ipc-contract/trust.ts +60 -0
  233. package/src/daemon/ipc-contract/work-items.ts +225 -0
  234. package/src/daemon/ipc-contract/workspace.ts +113 -0
  235. package/src/daemon/ipc-contract-inventory.json +62 -0
  236. package/src/daemon/ipc-contract-inventory.ts +55 -29
  237. package/src/daemon/ipc-contract.ts +227 -2527
  238. package/src/daemon/ipc-protocol.ts +1 -1
  239. package/src/daemon/ipc-validate.ts +7 -0
  240. package/src/daemon/lifecycle.ts +97 -379
  241. package/src/daemon/pairing-store.ts +177 -0
  242. package/src/daemon/providers-setup.ts +43 -0
  243. package/src/daemon/ride-shotgun-handler.ts +67 -2
  244. package/src/daemon/server.ts +60 -44
  245. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  246. package/src/daemon/session-agent-loop.ts +113 -275
  247. package/src/daemon/session-dynamic-profile.ts +1 -1
  248. package/src/daemon/session-history.ts +1 -1
  249. package/src/daemon/session-media-retry.ts +1 -1
  250. package/src/daemon/session-messaging.ts +37 -2
  251. package/src/daemon/session-notifiers.ts +5 -25
  252. package/src/daemon/session-process.ts +99 -59
  253. package/src/daemon/session-queue-manager.ts +98 -4
  254. package/src/daemon/session-runtime-assembly.ts +149 -15
  255. package/src/daemon/session-surfaces.ts +26 -4
  256. package/src/daemon/session-tool-setup.ts +28 -30
  257. package/src/daemon/session-workspace.ts +1 -1
  258. package/src/daemon/session.ts +24 -1
  259. package/src/daemon/shutdown-handlers.ts +122 -0
  260. package/src/daemon/trace-emitter.ts +1 -1
  261. package/src/daemon/watch-handler.ts +36 -33
  262. package/src/doordash/cart-queries.ts +787 -0
  263. package/src/doordash/client.ts +144 -127
  264. package/src/doordash/order-queries.ts +85 -0
  265. package/src/doordash/queries.ts +10 -1308
  266. package/src/doordash/search-queries.ts +203 -0
  267. package/src/doordash/session.ts +3 -2
  268. package/src/doordash/store-queries.ts +246 -0
  269. package/src/doordash/types.ts +367 -0
  270. package/src/email/providers/agentmail.ts +2 -1
  271. package/src/email/providers/index.ts +3 -2
  272. package/src/email/service.ts +3 -2
  273. package/src/errors.ts +43 -0
  274. package/src/home-base/prebuilt/seed.ts +1 -1
  275. package/src/hooks/cli.ts +6 -5
  276. package/src/hooks/config.ts +6 -8
  277. package/src/hooks/discovery.ts +6 -5
  278. package/src/hooks/manager.ts +4 -3
  279. package/src/hooks/runner.ts +2 -2
  280. package/src/hooks/templates.ts +5 -5
  281. package/src/inbound/public-ingress-urls.ts +3 -1
  282. package/src/index.ts +4 -2
  283. package/src/influencer/client.ts +1104 -0
  284. package/src/instrument.ts +4 -3
  285. package/src/logfire.ts +4 -3
  286. package/src/memory/admin.ts +25 -35
  287. package/src/memory/attachments-store.ts +4 -7
  288. package/src/memory/channel-delivery-store.ts +30 -1
  289. package/src/memory/channel-guardian-store.ts +200 -1
  290. package/src/memory/clarification-resolver.ts +37 -33
  291. package/src/memory/conflict-store.ts +67 -61
  292. package/src/memory/contradiction-checker.ts +141 -117
  293. package/src/memory/conversation-store.ts +335 -51
  294. package/src/memory/db-connection.ts +27 -4
  295. package/src/memory/db-init.ts +121 -4
  296. package/src/memory/db.ts +14 -1
  297. package/src/memory/embedding-backend.ts +27 -5
  298. package/src/memory/embedding-ollama.ts +2 -1
  299. package/src/memory/entity-extractor.ts +38 -35
  300. package/src/memory/guardian-action-store.ts +430 -0
  301. package/src/memory/inbox-escalation-projection.ts +59 -0
  302. package/src/memory/inbox-thread-store.ts +218 -0
  303. package/src/memory/ingress-invite-store.ts +338 -0
  304. package/src/memory/ingress-member-store.ts +350 -0
  305. package/src/memory/items-extractor.ts +91 -97
  306. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  307. package/src/memory/job-handlers/media-processing.ts +11 -42
  308. package/src/memory/job-handlers/summarization.ts +32 -26
  309. package/src/memory/job-utils.ts +3 -10
  310. package/src/memory/jobs-store.ts +6 -9
  311. package/src/memory/jobs-worker.ts +51 -36
  312. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  313. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  314. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  315. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  316. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  317. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  318. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  319. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  320. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  321. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  322. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  323. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  324. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  325. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  326. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  327. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  328. package/src/memory/migrations/017-memory-items-indexes.ts +12 -0
  329. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  330. package/src/memory/migrations/index.ts +24 -0
  331. package/src/memory/migrations/registry.ts +79 -0
  332. package/src/memory/migrations/validate-migration-state.ts +69 -0
  333. package/src/memory/qdrant-manager.ts +49 -8
  334. package/src/memory/query-builder.ts +1 -1
  335. package/src/memory/raw-query.ts +119 -0
  336. package/src/memory/recall-cache.ts +4 -1
  337. package/src/memory/retriever.ts +163 -47
  338. package/src/memory/schema-migration.ts +25 -984
  339. package/src/memory/schema.ts +130 -7
  340. package/src/memory/search/entity.ts +10 -19
  341. package/src/memory/search/lexical.ts +81 -52
  342. package/src/memory/search/ranking.ts +21 -22
  343. package/src/memory/search/semantic.ts +157 -19
  344. package/src/memory/shared-app-links-store.ts +4 -5
  345. package/src/memory/validation.ts +19 -0
  346. package/src/messaging/draft-store.ts +5 -6
  347. package/src/messaging/providers/sms/adapter.ts +3 -6
  348. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  349. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  350. package/src/messaging/providers/whatsapp/client.ts +67 -0
  351. package/src/messaging/style-analyzer.ts +5 -4
  352. package/src/messaging/thread-summarizer.ts +61 -69
  353. package/src/messaging/triage-engine.ts +62 -71
  354. package/src/migrations/config-merge.ts +53 -0
  355. package/src/migrations/data-layout.ts +68 -0
  356. package/src/migrations/data-merge.ts +33 -0
  357. package/src/migrations/hooks-merge.ts +90 -0
  358. package/src/migrations/index.ts +6 -0
  359. package/src/migrations/log.ts +23 -0
  360. package/src/migrations/skills-merge.ts +33 -0
  361. package/src/migrations/workspace-layout.ts +79 -0
  362. package/src/permissions/checker.ts +126 -11
  363. package/src/permissions/prompter.ts +14 -0
  364. package/src/permissions/shell-identity.ts +31 -1
  365. package/src/permissions/trust-store.ts +21 -1
  366. package/src/providers/anthropic/client.ts +4 -4
  367. package/src/providers/failover.ts +2 -2
  368. package/src/providers/model-intents.ts +70 -0
  369. package/src/providers/ollama/client.ts +2 -1
  370. package/src/providers/provider-send-message.ts +176 -0
  371. package/src/providers/registry.ts +71 -30
  372. package/src/providers/retry.ts +35 -1
  373. package/src/providers/types.ts +12 -1
  374. package/src/runtime/approval-conversation-turn.ts +97 -0
  375. package/src/runtime/approval-message-composer.ts +115 -5
  376. package/src/runtime/assistant-event-hub.ts +3 -1
  377. package/src/runtime/channel-approval-parser.ts +36 -2
  378. package/src/runtime/channel-approvals.ts +0 -21
  379. package/src/runtime/channel-guardian-service.ts +48 -7
  380. package/src/runtime/channel-readiness-service.ts +160 -34
  381. package/src/runtime/channel-readiness-types.ts +10 -4
  382. package/src/runtime/channel-retry-sweep.ts +184 -0
  383. package/src/runtime/guardian-context-resolver.ts +108 -0
  384. package/src/runtime/http-server.ts +289 -745
  385. package/src/runtime/http-types.ts +56 -3
  386. package/src/runtime/middleware/auth.ts +116 -0
  387. package/src/runtime/middleware/error-handler.ts +33 -0
  388. package/src/runtime/middleware/twilio-validation.ts +127 -0
  389. package/src/runtime/routes/app-routes.ts +1 -1
  390. package/src/runtime/routes/call-routes.ts +49 -6
  391. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  392. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  393. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  394. package/src/runtime/routes/channel-route-shared.ts +144 -0
  395. package/src/runtime/routes/channel-routes.ts +32 -1634
  396. package/src/runtime/routes/conversation-routes.ts +50 -7
  397. package/src/runtime/routes/events-routes.ts +2 -2
  398. package/src/runtime/routes/identity-routes.ts +126 -0
  399. package/src/runtime/routes/pairing-routes.ts +144 -0
  400. package/src/runtime/routes/run-routes.ts +15 -1
  401. package/src/runtime/run-orchestrator.ts +52 -34
  402. package/src/schedule/schedule-store.ts +36 -32
  403. package/src/schedule/scheduler.ts +3 -3
  404. package/src/security/encrypted-store.ts +5 -7
  405. package/src/security/oauth2.ts +45 -15
  406. package/src/security/parental-control-store.ts +183 -0
  407. package/src/security/secret-allowlist.ts +4 -3
  408. package/src/security/secret-scanner.ts +5 -5
  409. package/src/security/secure-keys.ts +1 -1
  410. package/src/security/token-manager.ts +3 -2
  411. package/src/services/vercel-deploy.ts +6 -2
  412. package/src/skills/tool-manifest.ts +3 -3
  413. package/src/skills/vellum-catalog-remote.ts +75 -16
  414. package/src/slack/slack-webhook.ts +2 -1
  415. package/src/swarm/orchestrator.ts +92 -1
  416. package/src/swarm/router-planner.ts +6 -9
  417. package/src/swarm/worker-prompts.ts +9 -12
  418. package/src/tasks/task-compiler.ts +19 -28
  419. package/src/tasks/task-runner.ts +1 -1
  420. package/src/tools/assets/search.ts +15 -14
  421. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  422. package/src/tools/browser/auto-navigate.ts +1 -0
  423. package/src/tools/browser/browser-execution.ts +13 -1
  424. package/src/tools/browser/browser-manager.ts +119 -4
  425. package/src/tools/browser/network-recorder.ts +5 -0
  426. package/src/tools/credentials/broker.ts +11 -2
  427. package/src/tools/credentials/metadata-store.ts +18 -14
  428. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  429. package/src/tools/credentials/vault.ts +49 -23
  430. package/src/tools/executor.ts +80 -18
  431. package/src/tools/host-terminal/cli-discover.ts +1 -1
  432. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  433. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  434. package/src/tools/network/script-proxy/server.ts +1 -1
  435. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  436. package/src/tools/network/web-fetch.ts +18 -2
  437. package/src/tools/network/web-search.ts +7 -3
  438. package/src/tools/reminder/reminder-store.ts +14 -15
  439. package/src/tools/schedule/create.ts +1 -0
  440. package/src/tools/schedule/list.ts +2 -1
  441. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  442. package/src/tools/skills/skill-script-runner.ts +24 -9
  443. package/src/tools/skills/skill-tool-factory.ts +1 -0
  444. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  445. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  446. package/src/tools/terminal/parser.ts +50 -0
  447. package/src/tools/watcher/delete.ts +6 -0
  448. package/src/tools/weather/service.ts +1 -1
  449. package/src/twitter/client.ts +190 -24
  450. package/src/twitter/session.ts +4 -3
  451. package/src/util/clipboard.ts +1 -1
  452. package/src/util/errors.ts +65 -8
  453. package/src/util/fs.ts +40 -0
  454. package/src/util/json.ts +10 -0
  455. package/src/util/log-redact.ts +189 -0
  456. package/src/util/logger.ts +25 -18
  457. package/src/util/object.ts +3 -0
  458. package/src/util/platform.ts +72 -365
  459. package/src/util/pricing.ts +1 -1
  460. package/src/util/promise-guard.ts +1 -1
  461. package/src/util/retry.ts +19 -0
  462. package/src/util/row-mapper.ts +79 -0
  463. package/src/util/silently.ts +21 -0
  464. package/src/watcher/engine.ts +5 -1
  465. package/src/watcher/provider-types.ts +20 -0
  466. package/src/watcher/providers/github.ts +156 -0
  467. package/src/watcher/providers/gmail.ts +1 -0
  468. package/src/watcher/providers/google-calendar.ts +1 -0
  469. package/src/watcher/providers/linear.ts +460 -0
  470. package/src/watcher/providers/slack.ts +1 -0
  471. package/src/work-items/work-item-runner.ts +1 -1
  472. package/src/workspace/git-service.ts +1 -1
  473. package/src/workspace/provider-commit-message-generator.ts +51 -22
  474. package/src/__tests__/call-bridge.test.ts +0 -517
  475. package/src/__tests__/session-process-bridge.test.ts +0 -244
  476. package/src/calls/call-bridge.ts +0 -168
  477. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +0 -137
  478. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
  479. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
  480. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
  481. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
  482. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
  483. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
  484. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
  485. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
  486. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
  487. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,231 @@
1
+ import * as net from 'node:net';
2
+ import { loadRawConfig, saveRawConfig } from '../../config/loader.js';
3
+ import { getSecureKey } from '../../security/secure-keys.js';
4
+ import { readHttpToken } from '../../util/platform.js';
5
+ import {
6
+ hasTwilioCredentials,
7
+ updatePhoneNumberWebhooks,
8
+ } from '../../calls/twilio-rest.js';
9
+ import {
10
+ getTwilioVoiceWebhookUrl,
11
+ getTwilioStatusCallbackUrl,
12
+ getTwilioSmsWebhookUrl,
13
+ type IngressConfig,
14
+ } from '../../inbound/public-ingress-urls.js';
15
+ import type { IngressConfigRequest } from '../ipc-protocol.js';
16
+ import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
17
+ import {
18
+ getGatewayInternalBaseUrl,
19
+ getIngressPublicBaseUrl,
20
+ setIngressPublicBaseUrl,
21
+ } from '../../config/env.js';
22
+
23
+ // Lazily capture the env-provided INGRESS_PUBLIC_BASE_URL on first access
24
+ // rather than at module load time. The daemon loads ~/.vellum/.env inside
25
+ // runDaemon() (see lifecycle.ts), which runs AFTER static ES module imports
26
+ // resolve. A module-level snapshot would miss dotenv-provided values.
27
+ let _originalIngressEnvCaptured = false;
28
+ let _originalIngressEnv: string | undefined;
29
+ function getOriginalIngressEnv(): string | undefined {
30
+ if (!_originalIngressEnvCaptured) {
31
+ _originalIngressEnv = getIngressPublicBaseUrl();
32
+ _originalIngressEnvCaptured = true;
33
+ }
34
+ return _originalIngressEnv;
35
+ }
36
+
37
+ export function computeGatewayTarget(): string {
38
+ return getGatewayInternalBaseUrl();
39
+ }
40
+
41
+ /**
42
+ * Best-effort call to the gateway's internal reconcile endpoint so that
43
+ * Telegram webhook registration is updated immediately when the ingress
44
+ * URL changes, without requiring a gateway restart.
45
+ */
46
+ export function triggerGatewayReconcile(ingressPublicBaseUrl: string | undefined): void {
47
+ const gatewayBase = computeGatewayTarget();
48
+ const token = readHttpToken();
49
+ if (!token) {
50
+ log.debug('Skipping gateway reconcile trigger: no HTTP bearer token available');
51
+ return;
52
+ }
53
+
54
+ const url = `${gatewayBase}/internal/telegram/reconcile`;
55
+ const body = JSON.stringify({ ingressPublicBaseUrl: ingressPublicBaseUrl ?? '' });
56
+
57
+ fetch(url, {
58
+ method: 'POST',
59
+ headers: {
60
+ 'Content-Type': 'application/json',
61
+ 'Authorization': `Bearer ${token}`,
62
+ },
63
+ body,
64
+ signal: AbortSignal.timeout(5_000),
65
+ }).then((res) => {
66
+ if (res.ok) {
67
+ log.info('Gateway Telegram webhook reconcile triggered successfully');
68
+ } else {
69
+ log.warn({ status: res.status }, 'Gateway Telegram webhook reconcile returned non-OK status');
70
+ }
71
+ }).catch((err) => {
72
+ log.debug({ err }, 'Gateway Telegram webhook reconcile failed (gateway may not be running)');
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Best-effort Twilio webhook sync helper.
78
+ *
79
+ * Computes the voice, status-callback, and SMS webhook URLs from the current
80
+ * ingress config and pushes them to the Twilio IncomingPhoneNumber API.
81
+ *
82
+ * Returns `{ success, warning }`. When the update fails, `success` is false
83
+ * and `warning` contains a human-readable message. Callers should treat
84
+ * failure as non-fatal so that the primary operation (provision, assign,
85
+ * ingress save) still succeeds.
86
+ */
87
+ export async function syncTwilioWebhooks(
88
+ phoneNumber: string,
89
+ accountSid: string,
90
+ authToken: string,
91
+ ingressConfig: IngressConfig,
92
+ ): Promise<{ success: boolean; warning?: string }> {
93
+ try {
94
+ const voiceUrl = getTwilioVoiceWebhookUrl(ingressConfig);
95
+ const statusCallbackUrl = getTwilioStatusCallbackUrl(ingressConfig);
96
+ const smsUrl = getTwilioSmsWebhookUrl(ingressConfig);
97
+ await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
98
+ voiceUrl,
99
+ statusCallbackUrl,
100
+ smsUrl,
101
+ });
102
+ log.info({ phoneNumber }, 'Twilio webhooks configured successfully');
103
+ return { success: true };
104
+ } catch (err) {
105
+ const message = err instanceof Error ? err.message : String(err);
106
+ log.warn({ err, phoneNumber }, `Webhook configuration skipped: ${message}`);
107
+ return { success: false, warning: `Webhook configuration skipped: ${message}` };
108
+ }
109
+ }
110
+
111
+ export async function handleIngressConfig(
112
+ msg: IngressConfigRequest,
113
+ socket: net.Socket,
114
+ ctx: HandlerContext,
115
+ ): Promise<void> {
116
+ const localGatewayTarget = computeGatewayTarget();
117
+ try {
118
+ if (msg.action === 'get') {
119
+ const raw = loadRawConfig();
120
+ const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
121
+ const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
122
+ // Backward compatibility: if `enabled` was never explicitly set,
123
+ // infer from whether a publicBaseUrl is configured so existing users
124
+ // who predate the toggle aren't silently disabled.
125
+ const enabled = (ingress.enabled as boolean | undefined) ?? (publicBaseUrl ? true : false);
126
+ ctx.send(socket, { type: 'ingress_config_response', enabled, publicBaseUrl, localGatewayTarget, success: true });
127
+ } else if (msg.action === 'set') {
128
+ const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
129
+ // Ensure we capture the original env value before any mutation below
130
+ getOriginalIngressEnv();
131
+ const raw = loadRawConfig();
132
+
133
+ // Update ingress.publicBaseUrl — this is the single source of truth for
134
+ // the canonical public ingress URL. The gateway receives this value via
135
+ // the INGRESS_PUBLIC_BASE_URL env var at spawn time (see hatch.ts).
136
+ // The gateway also validates Twilio signatures against forwarded public
137
+ // URL headers, so local tunnel updates generally apply without restarts.
138
+ const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
139
+ ingress.publicBaseUrl = value || undefined;
140
+ if (msg.enabled !== undefined) {
141
+ ingress.enabled = msg.enabled;
142
+ }
143
+
144
+ const wasSuppressed = ctx.suppressConfigReload;
145
+ ctx.setSuppressConfigReload(true);
146
+ try {
147
+ saveRawConfig({ ...raw, ingress });
148
+ } catch (err) {
149
+ ctx.setSuppressConfigReload(wasSuppressed);
150
+ throw err;
151
+ }
152
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
153
+
154
+ // Propagate to the gateway's process environment so it picks up the
155
+ // new URL when it is restarted. For the local-deployment path the
156
+ // gateway runs as a child process that inherited the assistant's env,
157
+ // so updating process.env here ensures the value is visible when the
158
+ // gateway is restarted (e.g. by the self-upgrade skill or a manual
159
+ // `pkill -f gateway`).
160
+ // Only export the URL when ingress is enabled; clearing it when
161
+ // disabled ensures the gateway stops accepting inbound webhooks.
162
+ const isEnabled = (ingress.enabled as boolean | undefined) ?? (value ? true : false);
163
+ if (value && isEnabled) {
164
+ setIngressPublicBaseUrl(value);
165
+ } else if (isEnabled && getOriginalIngressEnv() !== undefined) {
166
+ // Ingress is enabled but the user cleared the URL — fall back to the
167
+ // env var that was present when the process started.
168
+ setIngressPublicBaseUrl(getOriginalIngressEnv()!);
169
+ } else {
170
+ // Ingress is disabled or no URL is configured and no startup env var
171
+ // exists — remove the env var so the gateway stops accepting webhooks.
172
+ setIngressPublicBaseUrl(undefined);
173
+ }
174
+
175
+ ctx.send(socket, { type: 'ingress_config_response', enabled: isEnabled, publicBaseUrl: value, localGatewayTarget, success: true });
176
+
177
+ // Trigger immediate Telegram webhook reconcile on the gateway so
178
+ // that changing the ingress URL takes effect without a restart.
179
+ // Called unconditionally so the gateway clears its in-memory URL
180
+ // when ingress is disabled, preventing stale re-registration on
181
+ // credential rotation.
182
+ // Use the effective URL from process.env (which accounts for the
183
+ // fallback branch above) rather than the raw `value` from the UI.
184
+ const effectiveUrl = isEnabled ? getIngressPublicBaseUrl() : undefined;
185
+ triggerGatewayReconcile(effectiveUrl);
186
+
187
+ // Best-effort Twilio webhook reconciliation: when ingress is being
188
+ // enabled/updated and Twilio numbers are assigned with valid credentials,
189
+ // push the new webhook URLs to Twilio so calls and SMS route correctly.
190
+ if (isEnabled && hasTwilioCredentials()) {
191
+ const currentConfig = loadRawConfig();
192
+ const smsConfig = (currentConfig?.sms ?? {}) as Record<string, unknown>;
193
+ const assignedNumbers = new Set<string>();
194
+ const legacyNumber = (smsConfig.phoneNumber as string) ?? '';
195
+ if (legacyNumber) assignedNumbers.add(legacyNumber);
196
+
197
+ const assistantPhoneNumbers = smsConfig.assistantPhoneNumbers;
198
+ if (assistantPhoneNumbers && typeof assistantPhoneNumbers === 'object' && !Array.isArray(assistantPhoneNumbers)) {
199
+ for (const number of Object.values(assistantPhoneNumbers as Record<string, unknown>)) {
200
+ if (typeof number === 'string' && number) {
201
+ assignedNumbers.add(number);
202
+ }
203
+ }
204
+ }
205
+
206
+ if (assignedNumbers.size > 0) {
207
+ const acctSid = getSecureKey('credential:twilio:account_sid')!;
208
+ const acctToken = getSecureKey('credential:twilio:auth_token')!;
209
+ // Fire-and-forget: webhook sync failure must not block the ingress save.
210
+ // Reconcile every assigned number so assistant-scoped mappings do not
211
+ // retain stale Twilio webhook URLs after ingress URL changes.
212
+ for (const assignedNumber of assignedNumbers) {
213
+ syncTwilioWebhooks(assignedNumber, acctSid, acctToken, currentConfig as IngressConfig)
214
+ .catch(() => {
215
+ // Already logged inside syncTwilioWebhooks
216
+ });
217
+ }
218
+ }
219
+ }
220
+ } else {
221
+ ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
222
+ }
223
+ } catch (err) {
224
+ const message = err instanceof Error ? err.message : String(err);
225
+ ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: message });
226
+ }
227
+ }
228
+
229
+ export const ingressHandlers = defineHandlers({
230
+ ingress_config: handleIngressConfig,
231
+ });
@@ -0,0 +1,258 @@
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, getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
5
+ import type {
6
+ VercelApiConfigRequest,
7
+ TwitterIntegrationConfigRequest,
8
+ } from '../ipc-protocol.js';
9
+ import { log, defineHandlers, type HandlerContext } from './shared.js';
10
+
11
+ export function handleVercelApiConfig(
12
+ msg: VercelApiConfigRequest,
13
+ socket: net.Socket,
14
+ ctx: HandlerContext,
15
+ ): void {
16
+ try {
17
+ if (msg.action === 'get') {
18
+ const existing = getSecureKey('credential:vercel:api_token');
19
+ ctx.send(socket, {
20
+ type: 'vercel_api_config_response',
21
+ hasToken: !!existing,
22
+ success: true,
23
+ });
24
+ } else if (msg.action === 'set') {
25
+ if (!msg.apiToken) {
26
+ ctx.send(socket, {
27
+ type: 'vercel_api_config_response',
28
+ hasToken: false,
29
+ success: false,
30
+ error: 'apiToken is required for set action',
31
+ });
32
+ return;
33
+ }
34
+ const stored = setSecureKey('credential:vercel:api_token', msg.apiToken);
35
+ if (!stored) {
36
+ ctx.send(socket, {
37
+ type: 'vercel_api_config_response',
38
+ hasToken: false,
39
+ success: false,
40
+ error: 'Failed to store API token in secure storage',
41
+ });
42
+ return;
43
+ }
44
+ upsertCredentialMetadata('vercel', 'api_token', {
45
+ allowedTools: ['publish_page', 'unpublish_page'],
46
+ });
47
+ ctx.send(socket, {
48
+ type: 'vercel_api_config_response',
49
+ hasToken: true,
50
+ success: true,
51
+ });
52
+ } else {
53
+ deleteSecureKey('credential:vercel:api_token');
54
+ deleteCredentialMetadata('vercel', 'api_token');
55
+ ctx.send(socket, {
56
+ type: 'vercel_api_config_response',
57
+ hasToken: false,
58
+ success: true,
59
+ });
60
+ }
61
+ } catch (err) {
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ log.error({ err }, 'Failed to handle Vercel API config');
64
+ ctx.send(socket, {
65
+ type: 'vercel_api_config_response',
66
+ hasToken: false,
67
+ success: false,
68
+ error: message,
69
+ });
70
+ }
71
+ }
72
+
73
+ export function handleTwitterIntegrationConfig(
74
+ msg: TwitterIntegrationConfigRequest,
75
+ socket: net.Socket,
76
+ ctx: HandlerContext,
77
+ ): void {
78
+ try {
79
+ if (msg.action === 'get') {
80
+ const raw = loadRawConfig();
81
+ const mode = (raw.twitterIntegrationMode as 'local_byo' | 'managed' | undefined) ?? 'local_byo';
82
+ const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
83
+ const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
84
+ const localClientConfigured = !!getSecureKey('credential:integration:twitter:oauth_client_id');
85
+ const connected = !!getSecureKey('credential:integration:twitter:access_token');
86
+ const meta = getCredentialMetadata('integration:twitter', 'access_token');
87
+ ctx.send(socket, {
88
+ type: 'twitter_integration_config_response',
89
+ success: true,
90
+ mode,
91
+ managedAvailable: false,
92
+ localClientConfigured,
93
+ connected,
94
+ accountInfo: meta?.accountInfo ?? undefined,
95
+ strategy,
96
+ strategyConfigured,
97
+ });
98
+ } else if (msg.action === 'get_strategy') {
99
+ const raw = loadRawConfig();
100
+ const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
101
+ const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
102
+ ctx.send(socket, {
103
+ type: 'twitter_integration_config_response',
104
+ success: true,
105
+ managedAvailable: false,
106
+ localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
107
+ connected: !!getSecureKey('credential:integration:twitter:access_token'),
108
+ strategy,
109
+ strategyConfigured,
110
+ });
111
+ } else if (msg.action === 'set_strategy') {
112
+ const valid = ['oauth', 'browser', 'auto'];
113
+ const value = msg.strategy;
114
+ if (!value || !valid.includes(value)) {
115
+ ctx.send(socket, {
116
+ type: 'twitter_integration_config_response',
117
+ success: false,
118
+ managedAvailable: false,
119
+ localClientConfigured: false,
120
+ connected: false,
121
+ error: `Invalid strategy value: ${String(value)}. Must be one of: ${valid.join(', ')}`,
122
+ });
123
+ return;
124
+ }
125
+ const raw = loadRawConfig();
126
+ raw.twitterOperationStrategy = value;
127
+ saveRawConfig(raw);
128
+ ctx.send(socket, {
129
+ type: 'twitter_integration_config_response',
130
+ success: true,
131
+ managedAvailable: false,
132
+ localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
133
+ connected: !!getSecureKey('credential:integration:twitter:access_token'),
134
+ strategy: value as 'oauth' | 'browser' | 'auto',
135
+ strategyConfigured: true,
136
+ });
137
+ } else if (msg.action === 'set_mode') {
138
+ const raw = loadRawConfig();
139
+ raw.twitterIntegrationMode = msg.mode ?? 'local_byo';
140
+ saveRawConfig(raw);
141
+ ctx.send(socket, {
142
+ type: 'twitter_integration_config_response',
143
+ success: true,
144
+ mode: msg.mode ?? 'local_byo',
145
+ managedAvailable: false,
146
+ localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
147
+ connected: !!getSecureKey('credential:integration:twitter:access_token'),
148
+ });
149
+ } else if (msg.action === 'set_local_client') {
150
+ if (!msg.clientId) {
151
+ ctx.send(socket, {
152
+ type: 'twitter_integration_config_response',
153
+ success: false,
154
+ managedAvailable: false,
155
+ localClientConfigured: false,
156
+ connected: false,
157
+ error: 'clientId is required for set_local_client action',
158
+ });
159
+ return;
160
+ }
161
+ const previousClientId = getSecureKey('credential:integration:twitter:oauth_client_id');
162
+ const storedId = setSecureKey('credential:integration:twitter:oauth_client_id', msg.clientId);
163
+ if (!storedId) {
164
+ ctx.send(socket, {
165
+ type: 'twitter_integration_config_response',
166
+ success: false,
167
+ managedAvailable: false,
168
+ localClientConfigured: false,
169
+ connected: false,
170
+ error: 'Failed to store client ID in secure storage',
171
+ });
172
+ return;
173
+ }
174
+ if (msg.clientSecret) {
175
+ const storedSecret = setSecureKey('credential:integration:twitter:oauth_client_secret', msg.clientSecret);
176
+ if (!storedSecret) {
177
+ // Roll back the client ID to its previous value to avoid inconsistent OAuth state
178
+ if (previousClientId) {
179
+ setSecureKey('credential:integration:twitter:oauth_client_id', previousClientId);
180
+ } else {
181
+ deleteSecureKey('credential:integration:twitter:oauth_client_id');
182
+ }
183
+ ctx.send(socket, {
184
+ type: 'twitter_integration_config_response',
185
+ success: false,
186
+ managedAvailable: false,
187
+ localClientConfigured: !!previousClientId,
188
+ connected: false,
189
+ error: 'Failed to store client secret in secure storage',
190
+ });
191
+ return;
192
+ }
193
+ } else {
194
+ // Clear any stale secret when updating client without a secret (e.g. switching to PKCE)
195
+ deleteSecureKey('credential:integration:twitter:oauth_client_secret');
196
+ }
197
+ ctx.send(socket, {
198
+ type: 'twitter_integration_config_response',
199
+ success: true,
200
+ managedAvailable: false,
201
+ localClientConfigured: true,
202
+ connected: !!getSecureKey('credential:integration:twitter:access_token'),
203
+ });
204
+ } else if (msg.action === 'clear_local_client') {
205
+ // If connected, disconnect first
206
+ if (getSecureKey('credential:integration:twitter:access_token')) {
207
+ deleteSecureKey('credential:integration:twitter:access_token');
208
+ deleteSecureKey('credential:integration:twitter:refresh_token');
209
+ deleteCredentialMetadata('integration:twitter', 'access_token');
210
+ }
211
+ deleteSecureKey('credential:integration:twitter:oauth_client_id');
212
+ deleteSecureKey('credential:integration:twitter:oauth_client_secret');
213
+ ctx.send(socket, {
214
+ type: 'twitter_integration_config_response',
215
+ success: true,
216
+ managedAvailable: false,
217
+ localClientConfigured: false,
218
+ connected: false,
219
+ });
220
+ } else if (msg.action === 'disconnect') {
221
+ deleteSecureKey('credential:integration:twitter:access_token');
222
+ deleteSecureKey('credential:integration:twitter:refresh_token');
223
+ deleteCredentialMetadata('integration:twitter', 'access_token');
224
+ ctx.send(socket, {
225
+ type: 'twitter_integration_config_response',
226
+ success: true,
227
+ managedAvailable: false,
228
+ localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
229
+ connected: false,
230
+ });
231
+ } else {
232
+ ctx.send(socket, {
233
+ type: 'twitter_integration_config_response',
234
+ success: false,
235
+ managedAvailable: false,
236
+ localClientConfigured: false,
237
+ connected: false,
238
+ error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
239
+ });
240
+ }
241
+ } catch (err) {
242
+ const message = err instanceof Error ? err.message : String(err);
243
+ log.error({ err }, 'Failed to handle Twitter integration config');
244
+ ctx.send(socket, {
245
+ type: 'twitter_integration_config_response',
246
+ success: false,
247
+ managedAvailable: false,
248
+ localClientConfigured: false,
249
+ connected: false,
250
+ error: message,
251
+ });
252
+ }
253
+ }
254
+
255
+ export const integrationHandlers = defineHandlers({
256
+ vercel_api_config: handleVercelApiConfig,
257
+ twitter_integration_config: handleTwitterIntegrationConfig,
258
+ });
@@ -0,0 +1,143 @@
1
+ import * as net from 'node:net';
2
+ import { getConfig, loadRawConfig, saveRawConfig } from '../../config/loader.js';
3
+ import { initializeProviders } from '../../providers/registry.js';
4
+ import type {
5
+ ModelSetRequest,
6
+ ImageGenModelSetRequest,
7
+ } from '../ipc-protocol.js';
8
+ import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
9
+ import { MODEL_TO_PROVIDER } from '../session-slash.js';
10
+
11
+ export function handleModelGet(socket: net.Socket, ctx: HandlerContext): void {
12
+ const config = getConfig();
13
+ const configured = Object.keys(config.apiKeys).filter((k) => !!config.apiKeys[k]);
14
+ if (!configured.includes('ollama')) configured.push('ollama');
15
+ ctx.send(socket, {
16
+ type: 'model_info',
17
+ model: config.model,
18
+ provider: config.provider,
19
+ configuredProviders: configured,
20
+ });
21
+ }
22
+
23
+ export function handleModelSet(
24
+ msg: ModelSetRequest,
25
+ socket: net.Socket,
26
+ ctx: HandlerContext,
27
+ ): void {
28
+ try {
29
+ // If the requested model is already the current model AND the provider
30
+ // is already aligned with what MODEL_TO_PROVIDER expects, skip expensive
31
+ // reinitialization but still send model_info so the client confirms.
32
+ // If the provider has drifted (e.g. manual config edit), fall through
33
+ // so the full reinit path can repair it.
34
+ {
35
+ const current = getConfig();
36
+ const expectedProvider = MODEL_TO_PROVIDER[msg.model];
37
+ const providerAligned = !expectedProvider || current.provider === expectedProvider;
38
+ if (msg.model === current.model && providerAligned) {
39
+ const configured = Object.keys(current.apiKeys).filter((k) => !!current.apiKeys[k]);
40
+ if (!configured.includes('ollama')) configured.push('ollama');
41
+ ctx.send(socket, {
42
+ type: 'model_info',
43
+ model: current.model,
44
+ provider: current.provider,
45
+ configuredProviders: configured,
46
+ });
47
+ return;
48
+ }
49
+ }
50
+
51
+ // Validate API key before switching
52
+ const provider = MODEL_TO_PROVIDER[msg.model];
53
+ if (provider && provider !== 'ollama') {
54
+ const currentConfig = getConfig();
55
+ if (!currentConfig.apiKeys[provider]) {
56
+ // Send current model_info so the client resyncs its optimistic state
57
+ // (don't use generic 'error' type — it would interrupt in-flight chat)
58
+ const configured = Object.keys(currentConfig.apiKeys).filter((k) => !!currentConfig.apiKeys[k]);
59
+ if (!configured.includes('ollama')) configured.push('ollama');
60
+ ctx.send(socket, { type: 'model_info', model: currentConfig.model, provider: currentConfig.provider, configuredProviders: configured });
61
+ return;
62
+ }
63
+ }
64
+
65
+ // Use raw config to avoid persisting env-var API keys to disk
66
+ const raw = loadRawConfig();
67
+ raw.model = msg.model;
68
+ // Infer provider from model ID to keep provider and model in sync
69
+ raw.provider = provider ?? raw.provider;
70
+
71
+ // Suppress the file watcher callback — handleModelSet already does
72
+ // the full reload sequence; a redundant watcher-triggered reload
73
+ // would incorrectly evict sessions created after this method returns.
74
+ const wasSuppressed = ctx.suppressConfigReload;
75
+ ctx.setSuppressConfigReload(true);
76
+ try {
77
+ saveRawConfig(raw);
78
+ } catch (err) {
79
+ ctx.setSuppressConfigReload(wasSuppressed);
80
+ throw err;
81
+ }
82
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
83
+
84
+ // Re-initialize provider with the new model so LLM calls use it
85
+ const config = getConfig();
86
+ initializeProviders(config);
87
+
88
+ // Evict idle sessions immediately; mark busy ones as stale so they
89
+ // get recreated with the new provider once they finish processing.
90
+ for (const [id, session] of ctx.sessions) {
91
+ if (!session.isProcessing()) {
92
+ session.dispose();
93
+ ctx.sessions.delete(id);
94
+ } else {
95
+ session.markStale();
96
+ }
97
+ }
98
+
99
+ ctx.updateConfigFingerprint();
100
+
101
+ ctx.send(socket, {
102
+ type: 'model_info',
103
+ model: config.model,
104
+ provider: config.provider,
105
+ });
106
+ } catch (err) {
107
+ const message = err instanceof Error ? err.message : String(err);
108
+ ctx.send(socket, { type: 'error', message: `Failed to set model: ${message}` });
109
+ }
110
+ }
111
+
112
+ export function handleImageGenModelSet(
113
+ msg: ImageGenModelSetRequest,
114
+ _socket: net.Socket,
115
+ ctx: HandlerContext,
116
+ ): void {
117
+ try {
118
+ const raw = loadRawConfig();
119
+ raw.imageGenModel = msg.model;
120
+
121
+ const wasSuppressed = ctx.suppressConfigReload;
122
+ ctx.setSuppressConfigReload(true);
123
+ try {
124
+ saveRawConfig(raw);
125
+ } catch (err) {
126
+ ctx.setSuppressConfigReload(wasSuppressed);
127
+ throw err;
128
+ }
129
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
130
+
131
+ ctx.updateConfigFingerprint();
132
+ log.info({ model: msg.model }, 'Image generation model updated');
133
+ } catch (err) {
134
+ const message = err instanceof Error ? err.message : String(err);
135
+ log.error({ err }, `Failed to set image gen model: ${message}`);
136
+ }
137
+ }
138
+
139
+ export const modelHandlers = defineHandlers({
140
+ model_get: (_msg, socket, ctx) => handleModelGet(socket, ctx),
141
+ model_set: handleModelSet,
142
+ image_gen_model_set: handleImageGenModelSet,
143
+ });