@openclaw/discord 2026.3.13 → 2026.5.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +132 -0
  4. package/channel-config-api.ts +1 -0
  5. package/channel-plugin-api.ts +3 -0
  6. package/config-api.ts +4 -0
  7. package/configured-state.ts +6 -0
  8. package/contract-api.ts +21 -0
  9. package/directory-contract-api.ts +4 -0
  10. package/doctor-contract-api.ts +1 -0
  11. package/index.test.ts +13 -0
  12. package/index.ts +18 -13
  13. package/openclaw.plugin.json +3282 -1
  14. package/package.json +67 -2
  15. package/runtime-api.actions.ts +15 -0
  16. package/runtime-api.lookup.ts +22 -0
  17. package/runtime-api.monitor.ts +50 -0
  18. package/runtime-api.send.ts +79 -0
  19. package/runtime-api.threads.ts +30 -0
  20. package/runtime-api.ts +180 -0
  21. package/runtime-setter-api.ts +3 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-audit-contract-api.ts +1 -0
  24. package/security-contract-api.ts +4 -0
  25. package/session-key-api.ts +1 -0
  26. package/setup-entry.ts +9 -0
  27. package/setup-plugin-api.ts +3 -0
  28. package/src/account-inspect.test.ts +126 -0
  29. package/src/account-inspect.ts +132 -0
  30. package/src/accounts.test.ts +247 -0
  31. package/src/accounts.ts +196 -0
  32. package/src/actions/handle-action.guild-admin.ts +411 -0
  33. package/src/actions/handle-action.test.ts +185 -0
  34. package/src/actions/handle-action.ts +332 -0
  35. package/src/actions/runtime.guild.ts +446 -0
  36. package/src/actions/runtime.messaging.messages.ts +205 -0
  37. package/src/actions/runtime.messaging.reactions.ts +67 -0
  38. package/src/actions/runtime.messaging.runtime.ts +69 -0
  39. package/src/actions/runtime.messaging.send.ts +244 -0
  40. package/src/actions/runtime.messaging.shared.ts +92 -0
  41. package/src/actions/runtime.messaging.ts +37 -0
  42. package/src/actions/runtime.moderation-shared.ts +48 -0
  43. package/src/actions/runtime.moderation.authz.test.ts +151 -0
  44. package/src/actions/runtime.moderation.ts +116 -0
  45. package/src/actions/runtime.presence.test.ts +160 -0
  46. package/src/actions/runtime.presence.ts +117 -0
  47. package/src/actions/runtime.shared.ts +83 -0
  48. package/src/actions/runtime.test.ts +1056 -0
  49. package/src/actions/runtime.ts +81 -0
  50. package/src/api-barrel.test.ts +80 -0
  51. package/src/api.test.ts +130 -0
  52. package/src/api.ts +169 -0
  53. package/src/approval-handler.runtime.test.ts +41 -0
  54. package/src/approval-handler.runtime.ts +632 -0
  55. package/src/approval-native.test.ts +330 -0
  56. package/src/approval-native.ts +219 -0
  57. package/src/approval-runtime.ts +14 -0
  58. package/src/approval-shared.ts +53 -0
  59. package/src/audit-core.ts +141 -0
  60. package/src/audit.test.ts +145 -0
  61. package/src/audit.ts +32 -0
  62. package/src/channel-actions.contract.test.ts +45 -0
  63. package/src/channel-actions.runtime.ts +1 -0
  64. package/src/channel-actions.test.ts +236 -0
  65. package/src/channel-actions.ts +198 -0
  66. package/src/channel-api.ts +28 -0
  67. package/src/channel.conversation.ts +159 -0
  68. package/src/channel.loaders.ts +47 -0
  69. package/src/channel.runtime.ts +1 -0
  70. package/src/channel.setup.ts +12 -0
  71. package/src/channel.test.ts +539 -12
  72. package/src/channel.ts +596 -430
  73. package/src/chunk.test.ts +157 -0
  74. package/src/chunk.ts +321 -0
  75. package/src/client.proxy.test.ts +176 -0
  76. package/src/client.test.ts +76 -0
  77. package/src/client.ts +139 -0
  78. package/src/component-custom-id.ts +72 -0
  79. package/src/components-registry.ts +356 -0
  80. package/src/components.builders.ts +409 -0
  81. package/src/components.modal.ts +124 -0
  82. package/src/components.parse.ts +407 -0
  83. package/src/components.test.ts +312 -0
  84. package/src/components.ts +54 -0
  85. package/src/components.types.ts +187 -0
  86. package/src/config-schema.test.ts +325 -0
  87. package/src/config-schema.ts +6 -0
  88. package/src/config-ui-hints.ts +249 -0
  89. package/src/conversation-identity.ts +58 -0
  90. package/src/delivery-retry.ts +56 -0
  91. package/src/directory-cache.ts +116 -0
  92. package/src/directory-config.ts +58 -0
  93. package/src/directory-contract.test.ts +129 -0
  94. package/src/directory-live.test.ts +126 -0
  95. package/src/directory-live.ts +135 -0
  96. package/src/doctor-contract.ts +477 -0
  97. package/src/doctor-shared.ts +5 -0
  98. package/src/doctor.test.ts +405 -0
  99. package/src/doctor.ts +340 -0
  100. package/src/draft-chunking.test.ts +64 -0
  101. package/src/draft-chunking.ts +43 -0
  102. package/src/draft-stream.test.ts +159 -0
  103. package/src/draft-stream.ts +154 -0
  104. package/src/error-body.ts +38 -0
  105. package/src/exec-approvals.test.ts +88 -0
  106. package/src/exec-approvals.ts +110 -0
  107. package/src/gateway-logging.test.ts +98 -0
  108. package/src/gateway-logging.ts +67 -0
  109. package/src/group-policy.ts +113 -0
  110. package/src/guilds.ts +29 -0
  111. package/src/inbound-context.contract.test.ts +11 -0
  112. package/src/interactive-dispatch.ts +104 -0
  113. package/src/internal/api.commands.ts +51 -0
  114. package/src/internal/api.guild.ts +164 -0
  115. package/src/internal/api.interactions.ts +53 -0
  116. package/src/internal/api.messages.ts +113 -0
  117. package/src/internal/api.reactions.ts +38 -0
  118. package/src/internal/api.test.ts +262 -0
  119. package/src/internal/api.ts +61 -0
  120. package/src/internal/api.users.ts +19 -0
  121. package/src/internal/api.webhooks.ts +13 -0
  122. package/src/internal/client.test.ts +297 -0
  123. package/src/internal/client.ts +246 -0
  124. package/src/internal/command-deploy.ts +202 -0
  125. package/src/internal/commands.ts +188 -0
  126. package/src/internal/components.base.ts +65 -0
  127. package/src/internal/components.message.ts +279 -0
  128. package/src/internal/components.modal.ts +95 -0
  129. package/src/internal/components.ts +31 -0
  130. package/src/internal/discord.ts +11 -0
  131. package/src/internal/embeds.ts +35 -0
  132. package/src/internal/entity-cache.ts +98 -0
  133. package/src/internal/event-queue.ts +162 -0
  134. package/src/internal/gateway-close-codes.ts +25 -0
  135. package/src/internal/gateway-dispatch.ts +96 -0
  136. package/src/internal/gateway-identify-limiter.ts +26 -0
  137. package/src/internal/gateway-lifecycle.ts +61 -0
  138. package/src/internal/gateway-rate-limit.ts +104 -0
  139. package/src/internal/gateway.test.ts +475 -0
  140. package/src/internal/gateway.ts +437 -0
  141. package/src/internal/interaction-dispatch.test.ts +148 -0
  142. package/src/internal/interaction-dispatch.ts +130 -0
  143. package/src/internal/interaction-options.ts +98 -0
  144. package/src/internal/interaction-response.ts +53 -0
  145. package/src/internal/interactions.test.ts +253 -0
  146. package/src/internal/interactions.ts +337 -0
  147. package/src/internal/listeners.ts +85 -0
  148. package/src/internal/live-smoke.live.test.ts +26 -0
  149. package/src/internal/modal-fields.ts +95 -0
  150. package/src/internal/payload.ts +69 -0
  151. package/src/internal/rest-body.ts +115 -0
  152. package/src/internal/rest-errors.ts +88 -0
  153. package/src/internal/rest-routes.ts +50 -0
  154. package/src/internal/rest-scheduler.ts +412 -0
  155. package/src/internal/rest.test.ts +437 -0
  156. package/src/internal/rest.ts +213 -0
  157. package/src/internal/schemas.ts +36 -0
  158. package/src/internal/structures.ts +278 -0
  159. package/src/internal/test-builders.test-support.ts +163 -0
  160. package/src/internal/voice.ts +49 -0
  161. package/src/media-detection.ts +28 -0
  162. package/src/mentions.test.ts +111 -0
  163. package/src/mentions.ts +147 -0
  164. package/src/monitor/access-groups.ts +55 -0
  165. package/src/monitor/ack-reactions.ts +70 -0
  166. package/src/monitor/acp-bind-here.integration.test.ts +211 -0
  167. package/src/monitor/agent-components-auth.ts +7 -0
  168. package/src/monitor/agent-components-context.ts +144 -0
  169. package/src/monitor/agent-components-data.ts +224 -0
  170. package/src/monitor/agent-components-dm-auth.ts +221 -0
  171. package/src/monitor/agent-components-guild-auth.ts +322 -0
  172. package/src/monitor/agent-components-helpers.runtime.ts +5 -0
  173. package/src/monitor/agent-components-helpers.ts +34 -0
  174. package/src/monitor/agent-components-reply.ts +10 -0
  175. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  176. package/src/monitor/agent-components.dispatch.ts +366 -0
  177. package/src/monitor/agent-components.handlers.ts +303 -0
  178. package/src/monitor/agent-components.modal.ts +160 -0
  179. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  180. package/src/monitor/agent-components.runtime.ts +14 -0
  181. package/src/monitor/agent-components.system-controls.ts +211 -0
  182. package/src/monitor/agent-components.ts +70 -0
  183. package/src/monitor/agent-components.types.ts +57 -0
  184. package/src/monitor/agent-components.wildcard-controls.ts +168 -0
  185. package/src/monitor/agent-components.wildcard.test.ts +71 -0
  186. package/src/monitor/allow-list.ts +623 -0
  187. package/src/monitor/auto-presence.test.ts +156 -0
  188. package/src/monitor/auto-presence.ts +356 -0
  189. package/src/monitor/channel-access.ts +70 -0
  190. package/src/monitor/commands.test.ts +24 -0
  191. package/src/monitor/commands.ts +9 -0
  192. package/src/monitor/dm-command-auth.test.ts +197 -0
  193. package/src/monitor/dm-command-auth.ts +158 -0
  194. package/src/monitor/dm-command-decision.test.ts +113 -0
  195. package/src/monitor/dm-command-decision.ts +49 -0
  196. package/src/monitor/exec-approvals.test.ts +226 -0
  197. package/src/monitor/exec-approvals.ts +158 -0
  198. package/src/monitor/format.ts +45 -0
  199. package/src/monitor/gateway-handle.ts +34 -0
  200. package/src/monitor/gateway-metadata.test.ts +29 -0
  201. package/src/monitor/gateway-metadata.ts +298 -0
  202. package/src/monitor/gateway-plugin.test.ts +297 -0
  203. package/src/monitor/gateway-plugin.ts +294 -0
  204. package/src/monitor/gateway-registry.ts +37 -0
  205. package/src/monitor/gateway-supervisor.test.ts +150 -0
  206. package/src/monitor/gateway-supervisor.ts +206 -0
  207. package/src/monitor/inbound-context.test-helpers.ts +37 -0
  208. package/src/monitor/inbound-context.test.ts +106 -0
  209. package/src/monitor/inbound-context.ts +103 -0
  210. package/src/monitor/inbound-dedupe.ts +79 -0
  211. package/src/monitor/inbound-job.test.ts +203 -0
  212. package/src/monitor/inbound-job.ts +118 -0
  213. package/src/monitor/listeners.queue.ts +91 -0
  214. package/src/monitor/listeners.reactions.ts +610 -0
  215. package/src/monitor/listeners.test.ts +200 -0
  216. package/src/monitor/listeners.ts +150 -0
  217. package/src/monitor/message-channel-info.ts +96 -0
  218. package/src/monitor/message-forwarded.ts +107 -0
  219. package/src/monitor/message-handler.batch-gate.test.ts +22 -0
  220. package/src/monitor/message-handler.batch-gate.ts +19 -0
  221. package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
  222. package/src/monitor/message-handler.context.ts +393 -0
  223. package/src/monitor/message-handler.dm-preflight.ts +123 -0
  224. package/src/monitor/message-handler.draft-preview.ts +246 -0
  225. package/src/monitor/message-handler.hydration.test.ts +80 -0
  226. package/src/monitor/message-handler.hydration.ts +198 -0
  227. package/src/monitor/message-handler.inbound-context.test.ts +59 -0
  228. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  229. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  230. package/src/monitor/message-handler.preflight-channel-context.ts +55 -0
  231. package/src/monitor/message-handler.preflight-context.ts +54 -0
  232. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  233. package/src/monitor/message-handler.preflight-history.ts +23 -0
  234. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  235. package/src/monitor/message-handler.preflight-pluralkit.ts +27 -0
  236. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  237. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  238. package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
  239. package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
  240. package/src/monitor/message-handler.preflight.test.ts +1544 -0
  241. package/src/monitor/message-handler.preflight.ts +680 -0
  242. package/src/monitor/message-handler.preflight.types.ts +109 -0
  243. package/src/monitor/message-handler.process.test.ts +1301 -0
  244. package/src/monitor/message-handler.process.ts +684 -0
  245. package/src/monitor/message-handler.queue.test.ts +496 -0
  246. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  247. package/src/monitor/message-handler.test-harness.ts +99 -0
  248. package/src/monitor/message-handler.test-helpers.ts +75 -0
  249. package/src/monitor/message-handler.ts +274 -0
  250. package/src/monitor/message-media.ts +507 -0
  251. package/src/monitor/message-run-queue.ts +101 -0
  252. package/src/monitor/message-text.ts +171 -0
  253. package/src/monitor/message-utils.test.ts +1151 -0
  254. package/src/monitor/message-utils.ts +32 -0
  255. package/src/monitor/model-picker-preferences.test.ts +67 -0
  256. package/src/monitor/model-picker-preferences.ts +184 -0
  257. package/src/monitor/model-picker.state.ts +364 -0
  258. package/src/monitor/model-picker.test-utils.ts +26 -0
  259. package/src/monitor/model-picker.test.ts +794 -0
  260. package/src/monitor/model-picker.ts +38 -0
  261. package/src/monitor/model-picker.view.ts +695 -0
  262. package/src/monitor/monitor.agent-components.test.ts +375 -0
  263. package/src/monitor/monitor.test.ts +849 -0
  264. package/src/monitor/monitor.threading-utils.test.ts +598 -0
  265. package/src/monitor/native-command-agent-reply.ts +123 -0
  266. package/src/monitor/native-command-arg-ui.ts +233 -0
  267. package/src/monitor/native-command-auth.ts +308 -0
  268. package/src/monitor/native-command-bypass.ts +13 -0
  269. package/src/monitor/native-command-context.test.ts +98 -0
  270. package/src/monitor/native-command-context.ts +103 -0
  271. package/src/monitor/native-command-dispatch.ts +35 -0
  272. package/src/monitor/native-command-model-picker-apply.ts +177 -0
  273. package/src/monitor/native-command-model-picker-interaction.ts +461 -0
  274. package/src/monitor/native-command-model-picker-ui.ts +368 -0
  275. package/src/monitor/native-command-reply.test.ts +68 -0
  276. package/src/monitor/native-command-reply.ts +183 -0
  277. package/src/monitor/native-command-route.ts +91 -0
  278. package/src/monitor/native-command-status.ts +76 -0
  279. package/src/monitor/native-command-ui.ts +26 -0
  280. package/src/monitor/native-command-ui.types.ts +20 -0
  281. package/src/monitor/native-command.args.ts +45 -0
  282. package/src/monitor/native-command.command-arg.test.ts +99 -0
  283. package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
  284. package/src/monitor/native-command.model-picker.test.ts +767 -0
  285. package/src/monitor/native-command.options.test.ts +369 -0
  286. package/src/monitor/native-command.options.ts +153 -0
  287. package/src/monitor/native-command.plugin-dispatch.test.ts +879 -0
  288. package/src/monitor/native-command.runtime.ts +50 -0
  289. package/src/monitor/native-command.status-direct.test.ts +272 -0
  290. package/src/monitor/native-command.test-helpers.ts +64 -0
  291. package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
  292. package/src/monitor/native-command.ts +699 -0
  293. package/src/monitor/native-command.types.ts +9 -0
  294. package/src/monitor/native-interaction-channel-context.ts +50 -0
  295. package/src/monitor/preflight-audio.runtime.ts +9 -0
  296. package/src/monitor/preflight-audio.test.ts +157 -0
  297. package/src/monitor/preflight-audio.ts +130 -0
  298. package/src/monitor/presence-cache.ts +61 -0
  299. package/src/monitor/presence.test.ts +44 -0
  300. package/src/monitor/presence.ts +50 -0
  301. package/src/monitor/provider-session.runtime.ts +12 -0
  302. package/src/monitor/provider.acp.ts +89 -0
  303. package/src/monitor/provider.allowlist.test.ts +149 -0
  304. package/src/monitor/provider.allowlist.ts +394 -0
  305. package/src/monitor/provider.cleanup.ts +41 -0
  306. package/src/monitor/provider.commands.ts +129 -0
  307. package/src/monitor/provider.config-log.ts +45 -0
  308. package/src/monitor/provider.deploy-errors.ts +362 -0
  309. package/src/monitor/provider.deploy.ts +221 -0
  310. package/src/monitor/provider.interactions.ts +160 -0
  311. package/src/monitor/provider.lifecycle.test.ts +658 -0
  312. package/src/monitor/provider.lifecycle.ts +545 -0
  313. package/src/monitor/provider.proxy.test.ts +745 -0
  314. package/src/monitor/provider.rest-proxy.test.ts +121 -0
  315. package/src/monitor/provider.runtime.ts +1 -0
  316. package/src/monitor/provider.skill-dedupe.test.ts +42 -0
  317. package/src/monitor/provider.startup-log.ts +32 -0
  318. package/src/monitor/provider.startup.test.ts +426 -0
  319. package/src/monitor/provider.startup.ts +323 -0
  320. package/src/monitor/provider.test.ts +1111 -0
  321. package/src/monitor/provider.ts +713 -0
  322. package/src/monitor/reply-context.ts +64 -0
  323. package/src/monitor/reply-delivery.test.ts +244 -0
  324. package/src/monitor/reply-delivery.ts +203 -0
  325. package/src/monitor/rest-fetch.ts +43 -0
  326. package/src/monitor/route-resolution.test.ts +204 -0
  327. package/src/monitor/route-resolution.ts +140 -0
  328. package/src/monitor/sender-identity.ts +81 -0
  329. package/src/monitor/startup-status.test.ts +30 -0
  330. package/src/monitor/startup-status.ts +10 -0
  331. package/src/monitor/status.ts +22 -0
  332. package/src/monitor/system-events.ts +55 -0
  333. package/src/monitor/thread-bindings.config.ts +35 -0
  334. package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
  335. package/src/monitor/thread-bindings.discord-api.ts +318 -0
  336. package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
  337. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  338. package/src/monitor/thread-bindings.manager.ts +553 -0
  339. package/src/monitor/thread-bindings.messages.ts +6 -0
  340. package/src/monitor/thread-bindings.persona.test.ts +34 -0
  341. package/src/monitor/thread-bindings.persona.ts +25 -0
  342. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  343. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  344. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  345. package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
  346. package/src/monitor/thread-bindings.state.ts +540 -0
  347. package/src/monitor/thread-bindings.ts +48 -0
  348. package/src/monitor/thread-bindings.types.ts +83 -0
  349. package/src/monitor/thread-channel-context.ts +112 -0
  350. package/src/monitor/thread-session-close.test.ts +180 -0
  351. package/src/monitor/thread-session-close.ts +63 -0
  352. package/src/monitor/thread-title.generate.test.ts +197 -0
  353. package/src/monitor/thread-title.test.ts +31 -0
  354. package/src/monitor/thread-title.ts +181 -0
  355. package/src/monitor/threading.auto-thread.test.ts +327 -0
  356. package/src/monitor/threading.auto-thread.ts +287 -0
  357. package/src/monitor/threading.cache.ts +45 -0
  358. package/src/monitor/threading.parent-info.test.ts +156 -0
  359. package/src/monitor/threading.starter.test.ts +260 -0
  360. package/src/monitor/threading.starter.ts +287 -0
  361. package/src/monitor/threading.ts +20 -0
  362. package/src/monitor/threading.types.ts +102 -0
  363. package/src/monitor/timeouts.ts +84 -0
  364. package/src/monitor/typing.test.ts +42 -0
  365. package/src/monitor/typing.ts +17 -0
  366. package/src/monitor.gateway.test.ts +187 -0
  367. package/src/monitor.gateway.ts +75 -0
  368. package/src/monitor.test.ts +1397 -0
  369. package/src/monitor.ts +28 -0
  370. package/src/normalize.test.ts +56 -0
  371. package/src/normalize.ts +86 -0
  372. package/src/outbound-adapter.interactive-order.test.ts +64 -0
  373. package/src/outbound-adapter.test-harness.ts +207 -0
  374. package/src/outbound-adapter.test.ts +696 -0
  375. package/src/outbound-adapter.ts +291 -0
  376. package/src/outbound-approval.ts +29 -0
  377. package/src/outbound-components.ts +81 -0
  378. package/src/outbound-payload.contract.test.ts +38 -0
  379. package/src/outbound-payload.ts +134 -0
  380. package/src/outbound-send-context.ts +92 -0
  381. package/src/outbound-session-route.test.ts +34 -0
  382. package/src/outbound-session-route.ts +72 -0
  383. package/src/pluralkit.test.ts +67 -0
  384. package/src/pluralkit.ts +58 -0
  385. package/src/preview-streaming.ts +32 -0
  386. package/src/probe.intents.test.ts +94 -0
  387. package/src/probe.parse-token.test.ts +43 -0
  388. package/src/probe.runtime.ts +1 -0
  389. package/src/probe.ts +237 -0
  390. package/src/proxy-fetch.ts +92 -0
  391. package/src/proxy-request-client.test.ts +78 -0
  392. package/src/proxy-request-client.ts +54 -0
  393. package/src/recipient-resolution.ts +39 -0
  394. package/src/resolve-allowlist-common.test.ts +36 -0
  395. package/src/resolve-allowlist-common.ts +39 -0
  396. package/src/resolve-channels.test.ts +340 -0
  397. package/src/resolve-channels.ts +369 -0
  398. package/src/resolve-users.test.ts +222 -0
  399. package/src/resolve-users.ts +184 -0
  400. package/src/retry.test.ts +83 -0
  401. package/src/retry.ts +98 -0
  402. package/src/runtime-api.ts +64 -0
  403. package/src/runtime.ts +22 -5
  404. package/src/secret-config-contract.ts +140 -0
  405. package/src/security-audit.runtime.ts +1 -0
  406. package/src/security-audit.test.ts +246 -0
  407. package/src/security-audit.ts +208 -0
  408. package/src/security-contract.ts +47 -0
  409. package/src/security-doctor.test.ts +25 -0
  410. package/src/security-doctor.ts +20 -0
  411. package/src/security.ts +60 -0
  412. package/src/send-target-parsing.ts +14 -0
  413. package/src/send.channels.ts +139 -0
  414. package/src/send.components.test.ts +275 -0
  415. package/src/send.components.ts +383 -0
  416. package/src/send.creates-thread.test.ts +643 -0
  417. package/src/send.emojis-stickers.ts +57 -0
  418. package/src/send.guild.ts +170 -0
  419. package/src/send.message-request.ts +97 -0
  420. package/src/send.messages.test.ts +53 -0
  421. package/src/send.messages.ts +225 -0
  422. package/src/send.outbound.ts +414 -0
  423. package/src/send.permissions.authz.test.ts +188 -0
  424. package/src/send.permissions.ts +283 -0
  425. package/src/send.reactions.ts +155 -0
  426. package/src/send.sends-basic-channel-messages.test.ts +919 -0
  427. package/src/send.shared.ts +445 -0
  428. package/src/send.test-harness.ts +56 -0
  429. package/src/send.ts +82 -0
  430. package/src/send.types.ts +188 -0
  431. package/src/send.typing.test.ts +41 -0
  432. package/src/send.typing.ts +9 -0
  433. package/src/send.voice.ts +134 -0
  434. package/src/send.webhook-activity.test.ts +105 -0
  435. package/src/send.webhook.proxy.test.ts +191 -0
  436. package/src/send.webhook.ts +133 -0
  437. package/src/session-contract.ts +3 -0
  438. package/src/session-key-normalization.test.ts +44 -0
  439. package/src/session-key-normalization.ts +47 -0
  440. package/src/setup-account-state.test.ts +91 -0
  441. package/src/setup-account-state.ts +144 -0
  442. package/src/setup-adapter.ts +12 -0
  443. package/src/setup-core.ts +180 -0
  444. package/src/setup-runtime-helpers.ts +10 -0
  445. package/src/setup-surface.test.ts +96 -0
  446. package/src/setup-surface.ts +129 -0
  447. package/src/shared-interactive.test.ts +153 -0
  448. package/src/shared-interactive.ts +124 -0
  449. package/src/shared.test.ts +159 -0
  450. package/src/shared.ts +190 -0
  451. package/src/status-issues.test.ts +70 -0
  452. package/src/status-issues.ts +169 -0
  453. package/src/subagent-hooks.test.ts +40 -44
  454. package/src/subagent-hooks.ts +185 -122
  455. package/src/target-parsing.ts +53 -0
  456. package/src/target-resolver.ts +129 -0
  457. package/src/targets.test.ts +367 -0
  458. package/src/targets.ts +12 -0
  459. package/src/test-http-helpers.ts +10 -0
  460. package/src/test-support/component-runtime.ts +190 -0
  461. package/src/test-support/config.ts +7 -0
  462. package/src/test-support/configured-binding-runtime.ts +29 -0
  463. package/src/test-support/partial-channel.ts +26 -0
  464. package/src/test-support/provider.test-support.ts +545 -0
  465. package/src/token.test.ts +107 -0
  466. package/src/token.ts +60 -0
  467. package/src/ui-colors.ts +27 -0
  468. package/src/ui.ts +20 -0
  469. package/src/voice/access.test.ts +217 -0
  470. package/src/voice/access.ts +124 -0
  471. package/src/voice/audio.ts +173 -0
  472. package/src/voice/capture-state.test.ts +48 -0
  473. package/src/voice/capture-state.ts +120 -0
  474. package/src/voice/command.test.ts +164 -0
  475. package/src/voice/command.ts +283 -0
  476. package/src/voice/config.ts +8 -0
  477. package/src/voice/manager.e2e.test.ts +928 -0
  478. package/src/voice/manager.ready-listener.test.ts +37 -0
  479. package/src/voice/manager.runtime.ts +11 -0
  480. package/src/voice/manager.ts +691 -0
  481. package/src/voice/prompt.test.ts +16 -0
  482. package/src/voice/prompt.ts +17 -0
  483. package/src/voice/receive-recovery.test.ts +79 -0
  484. package/src/voice/receive-recovery.ts +159 -0
  485. package/src/voice/sanitize.test.ts +34 -0
  486. package/src/voice/sanitize.ts +32 -0
  487. package/src/voice/sdk-runtime.ts +14 -0
  488. package/src/voice/segment.ts +156 -0
  489. package/src/voice/session.ts +50 -0
  490. package/src/voice/speaker-context.ts +127 -0
  491. package/src/voice/tts.ts +125 -0
  492. package/src/voice-message.test.ts +234 -0
  493. package/src/voice-message.ts +444 -0
  494. package/subagent-hooks-api.ts +27 -0
  495. package/test-api.ts +4 -0
  496. package/thread-binding-api.ts +1 -0
  497. package/timeouts.ts +6 -0
  498. package/tsconfig.json +16 -0
@@ -0,0 +1,32 @@
1
+ export {
2
+ __resetDiscordChannelInfoCacheForTest,
3
+ resolveDiscordChannelInfo,
4
+ resolveDiscordMessageChannelId,
5
+ type DiscordChannelInfo,
6
+ type DiscordChannelInfoClient,
7
+ } from "./message-channel-info.js";
8
+ export {
9
+ hasDiscordMessageStickers,
10
+ normalizeDiscordMessageSnapshots,
11
+ normalizeDiscordStickerItems,
12
+ resolveDiscordMessageSnapshots,
13
+ resolveDiscordMessageStickers,
14
+ resolveDiscordReferencedForwardMessage,
15
+ resolveDiscordSnapshotStickers,
16
+ type DiscordMessageSnapshot,
17
+ type DiscordSnapshotAuthor,
18
+ type DiscordSnapshotMessage,
19
+ } from "./message-forwarded.js";
20
+ export {
21
+ buildDiscordMediaPayload,
22
+ buildDiscordMediaPlaceholder,
23
+ resolveForwardedMediaList,
24
+ resolveMediaList,
25
+ type DiscordMediaInfo,
26
+ type DiscordMediaResolveOptions,
27
+ } from "./message-media.js";
28
+ export {
29
+ resolveDiscordEmbedText,
30
+ resolveDiscordForwardedMessagesTextFromSnapshots,
31
+ resolveDiscordMessageText,
32
+ } from "./message-text.js";
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import {
6
+ readDiscordModelPickerRecentModels,
7
+ recordDiscordModelPickerRecentModel,
8
+ } from "./model-picker-preferences.js";
9
+
10
+ const tempDirs: string[] = [];
11
+
12
+ async function createStateEnv(): Promise<NodeJS.ProcessEnv> {
13
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-model-picker-"));
14
+ tempDirs.push(dir);
15
+ return { ...process.env, OPENCLAW_STATE_DIR: dir };
16
+ }
17
+
18
+ afterEach(async () => {
19
+ await Promise.all(
20
+ tempDirs.splice(0).map(async (dir) => {
21
+ await fs.rm(dir, { recursive: true, force: true });
22
+ }),
23
+ );
24
+ });
25
+
26
+ describe("discord model picker preferences", () => {
27
+ it("records recent models in recency order without duplicates", async () => {
28
+ const env = await createStateEnv();
29
+ const scope = { userId: "123" };
30
+
31
+ await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4o" });
32
+ await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4.1" });
33
+ await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4o" });
34
+
35
+ const recent = await readDiscordModelPickerRecentModels({ env, scope });
36
+ expect(recent).toEqual(["openai/gpt-4o", "openai/gpt-4.1"]);
37
+ });
38
+
39
+ it("filters recent models using an allowlist", async () => {
40
+ const env = await createStateEnv();
41
+ const scope = { userId: "456" };
42
+
43
+ await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4o" });
44
+ await recordDiscordModelPickerRecentModel({ env, scope, modelRef: "openai/gpt-4.1" });
45
+
46
+ const recent = await readDiscordModelPickerRecentModels({
47
+ env,
48
+ scope,
49
+ allowedModelRefs: new Set(["openai/gpt-4.1"]),
50
+ });
51
+ expect(recent).toEqual(["openai/gpt-4.1"]);
52
+ });
53
+
54
+ it("falls back to an empty store when the file is corrupt", async () => {
55
+ const env = await createStateEnv();
56
+ const stateDir = env.OPENCLAW_STATE_DIR as string;
57
+ const filePath = path.join(stateDir, "discord", "model-picker-preferences.json");
58
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
59
+ await fs.writeFile(filePath, "{not-json", "utf-8");
60
+
61
+ const recent = await readDiscordModelPickerRecentModels({
62
+ env,
63
+ scope: { userId: "789" },
64
+ });
65
+ expect(recent).toEqual([]);
66
+ });
67
+ });
@@ -0,0 +1,184 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { normalizeAccountId as normalizeSharedAccountId } from "openclaw/plugin-sdk/account-id";
4
+ import { withFileLock } from "openclaw/plugin-sdk/file-lock";
5
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
6
+ import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
7
+ import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
8
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
9
+
10
+ const MODEL_PICKER_PREFERENCES_LOCK_OPTIONS = {
11
+ retries: {
12
+ retries: 8,
13
+ factor: 2,
14
+ minTimeout: 50,
15
+ maxTimeout: 5_000,
16
+ randomize: true,
17
+ },
18
+ stale: 15_000,
19
+ } as const;
20
+
21
+ const DEFAULT_RECENT_LIMIT = 5;
22
+
23
+ type ModelPickerPreferencesEntry = {
24
+ recent: string[];
25
+ updatedAt: string;
26
+ };
27
+
28
+ type ModelPickerPreferencesStore = {
29
+ version: 1;
30
+ entries: Record<string, ModelPickerPreferencesEntry>;
31
+ };
32
+
33
+ function sanitizePreferenceEntries(entries: unknown): Record<string, ModelPickerPreferencesEntry> {
34
+ if (!entries || typeof entries !== "object") {
35
+ return {};
36
+ }
37
+ const normalizedEntries: Record<string, ModelPickerPreferencesEntry> = {};
38
+ for (const [key, value] of Object.entries(entries)) {
39
+ if (!value || typeof value !== "object") {
40
+ continue;
41
+ }
42
+ const typedValue = value as {
43
+ recent?: unknown;
44
+ updatedAt?: unknown;
45
+ };
46
+ const recent = Array.isArray(typedValue.recent)
47
+ ? typedValue.recent.filter((item: unknown): item is string => typeof item === "string")
48
+ : [];
49
+ const updatedAt = typeof typedValue.updatedAt === "string" ? typedValue.updatedAt : "";
50
+ normalizedEntries[key] = { recent, updatedAt };
51
+ }
52
+ return normalizedEntries;
53
+ }
54
+
55
+ export type DiscordModelPickerPreferenceScope = {
56
+ accountId?: string;
57
+ guildId?: string;
58
+ userId: string;
59
+ };
60
+
61
+ function resolvePreferencesStorePath(env: NodeJS.ProcessEnv = process.env): string {
62
+ const stateDir = resolveStateDir(env, os.homedir);
63
+ return path.join(stateDir, "discord", "model-picker-preferences.json");
64
+ }
65
+
66
+ function normalizeId(value?: string): string {
67
+ return normalizeOptionalString(value) ?? "";
68
+ }
69
+
70
+ export function buildDiscordModelPickerPreferenceKey(
71
+ scope: DiscordModelPickerPreferenceScope,
72
+ ): string | null {
73
+ const userId = normalizeId(scope.userId);
74
+ if (!userId) {
75
+ return null;
76
+ }
77
+ const accountId = normalizeSharedAccountId(scope.accountId);
78
+ const guildId = normalizeId(scope.guildId);
79
+ if (guildId) {
80
+ return `discord:${accountId}:guild:${guildId}:user:${userId}`;
81
+ }
82
+ return `discord:${accountId}:dm:user:${userId}`;
83
+ }
84
+
85
+ function normalizeModelRef(raw?: string): string | null {
86
+ const value = raw?.trim();
87
+ if (!value) {
88
+ return null;
89
+ }
90
+ const slashIndex = value.indexOf("/");
91
+ if (slashIndex <= 0 || slashIndex >= value.length - 1) {
92
+ return null;
93
+ }
94
+ const provider = normalizeProviderId(value.slice(0, slashIndex));
95
+ const model = value.slice(slashIndex + 1).trim();
96
+ if (!provider || !model) {
97
+ return null;
98
+ }
99
+ return `${provider}/${model}`;
100
+ }
101
+
102
+ function sanitizeRecentModels(models: string[] | undefined, limit: number): string[] {
103
+ const deduped: string[] = [];
104
+ const seen = new Set<string>();
105
+ for (const item of models ?? []) {
106
+ const normalized = normalizeModelRef(item);
107
+ if (!normalized || seen.has(normalized)) {
108
+ continue;
109
+ }
110
+ seen.add(normalized);
111
+ deduped.push(normalized);
112
+ if (deduped.length >= limit) {
113
+ break;
114
+ }
115
+ }
116
+ return deduped;
117
+ }
118
+
119
+ async function readPreferencesStore(filePath: string): Promise<ModelPickerPreferencesStore> {
120
+ const { value } = await readJsonFileWithFallback(filePath, {
121
+ version: 1,
122
+ entries: {} as Record<string, ModelPickerPreferencesEntry>,
123
+ });
124
+ if (!value || typeof value !== "object" || value.version !== 1) {
125
+ return { version: 1, entries: {} };
126
+ }
127
+ return {
128
+ version: 1,
129
+ entries: sanitizePreferenceEntries(value.entries),
130
+ };
131
+ }
132
+
133
+ export async function readDiscordModelPickerRecentModels(params: {
134
+ scope: DiscordModelPickerPreferenceScope;
135
+ limit?: number;
136
+ allowedModelRefs?: Set<string>;
137
+ env?: NodeJS.ProcessEnv;
138
+ }): Promise<string[]> {
139
+ const key = buildDiscordModelPickerPreferenceKey(params.scope);
140
+ if (!key) {
141
+ return [];
142
+ }
143
+ const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
144
+ const filePath = resolvePreferencesStorePath(params.env);
145
+ const store = await readPreferencesStore(filePath);
146
+ const entry = store.entries[key];
147
+ const recent = sanitizeRecentModels(entry?.recent, limit);
148
+ if (!params.allowedModelRefs || params.allowedModelRefs.size === 0) {
149
+ return recent;
150
+ }
151
+ return recent.filter((modelRef) => params.allowedModelRefs?.has(modelRef));
152
+ }
153
+
154
+ export async function recordDiscordModelPickerRecentModel(params: {
155
+ scope: DiscordModelPickerPreferenceScope;
156
+ modelRef: string;
157
+ limit?: number;
158
+ env?: NodeJS.ProcessEnv;
159
+ }): Promise<void> {
160
+ const key = buildDiscordModelPickerPreferenceKey(params.scope);
161
+ const normalizedModelRef = normalizeModelRef(params.modelRef);
162
+ if (!key || !normalizedModelRef) {
163
+ return;
164
+ }
165
+
166
+ const limit = Math.max(1, Math.min(params.limit ?? DEFAULT_RECENT_LIMIT, 10));
167
+ const filePath = resolvePreferencesStorePath(params.env);
168
+
169
+ await withFileLock(filePath, MODEL_PICKER_PREFERENCES_LOCK_OPTIONS, async () => {
170
+ const store = await readPreferencesStore(filePath);
171
+ const existing = sanitizeRecentModels(store.entries[key]?.recent, limit);
172
+ const next = [
173
+ normalizedModelRef,
174
+ ...existing.filter((entry) => entry !== normalizedModelRef),
175
+ ].slice(0, limit);
176
+
177
+ store.entries[key] = {
178
+ recent: next,
179
+ updatedAt: new Date().toISOString(),
180
+ };
181
+
182
+ await writeJsonFileAtomically(filePath, store);
183
+ });
184
+ }
@@ -0,0 +1,364 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
+ import type { ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
3
+ import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
4
+ import type { ComponentData } from "../internal/discord.js";
5
+
6
+ export const DISCORD_MODEL_PICKER_CUSTOM_ID_KEY = "mdlpk";
7
+ export const DISCORD_CUSTOM_ID_MAX_CHARS = 100;
8
+
9
+ export const DISCORD_COMPONENT_MAX_ROWS = 5;
10
+ export const DISCORD_COMPONENT_MAX_BUTTONS_PER_ROW = 5;
11
+ export const DISCORD_COMPONENT_MAX_SELECT_OPTIONS = 25;
12
+
13
+ export const DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE =
14
+ DISCORD_COMPONENT_MAX_BUTTONS_PER_ROW * (DISCORD_COMPONENT_MAX_ROWS - 1);
15
+ export const DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX =
16
+ DISCORD_COMPONENT_MAX_BUTTONS_PER_ROW * DISCORD_COMPONENT_MAX_ROWS;
17
+ export const DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE = DISCORD_COMPONENT_MAX_SELECT_OPTIONS;
18
+
19
+ const COMMAND_CONTEXTS = ["model", "models"] as const;
20
+ const PICKER_ACTIONS = [
21
+ "open",
22
+ "provider",
23
+ "model",
24
+ "runtime",
25
+ "submit",
26
+ "quick",
27
+ "back",
28
+ "reset",
29
+ "cancel",
30
+ "recents",
31
+ ] as const;
32
+ const PICKER_VIEWS = ["providers", "models", "recents"] as const;
33
+
34
+ export type DiscordModelPickerCommandContext = (typeof COMMAND_CONTEXTS)[number];
35
+ export type DiscordModelPickerAction = (typeof PICKER_ACTIONS)[number];
36
+ export type DiscordModelPickerView = (typeof PICKER_VIEWS)[number];
37
+ export type DiscordModelPickerLayout = "v2" | "classic";
38
+
39
+ export type DiscordModelPickerState = {
40
+ command: DiscordModelPickerCommandContext;
41
+ action: DiscordModelPickerAction;
42
+ view: DiscordModelPickerView;
43
+ userId: string;
44
+ provider?: string;
45
+ runtime?: string;
46
+ page: number;
47
+ providerPage?: number;
48
+ modelIndex?: number;
49
+ recentSlot?: number;
50
+ };
51
+
52
+ export type DiscordModelPickerProviderItem = {
53
+ id: string;
54
+ count: number;
55
+ };
56
+
57
+ export type DiscordModelPickerPage<T> = {
58
+ items: T[];
59
+ page: number;
60
+ pageSize: number;
61
+ totalPages: number;
62
+ totalItems: number;
63
+ hasPrev: boolean;
64
+ hasNext: boolean;
65
+ };
66
+
67
+ export type DiscordModelPickerModelPage = DiscordModelPickerPage<string> & {
68
+ provider: string;
69
+ };
70
+
71
+ let modelsProviderRuntimePromise:
72
+ | Promise<typeof import("openclaw/plugin-sdk/models-provider-runtime")>
73
+ | undefined;
74
+
75
+ async function loadModelsProviderRuntime() {
76
+ modelsProviderRuntimePromise ??= import("openclaw/plugin-sdk/models-provider-runtime");
77
+ return await modelsProviderRuntimePromise;
78
+ }
79
+
80
+ function encodeCustomIdValue(value: string): string {
81
+ return encodeURIComponent(value);
82
+ }
83
+
84
+ function decodeCustomIdValue(value: string): string {
85
+ try {
86
+ return decodeURIComponent(value);
87
+ } catch {
88
+ return value;
89
+ }
90
+ }
91
+
92
+ function isValidCommandContext(value: string): value is DiscordModelPickerCommandContext {
93
+ return (COMMAND_CONTEXTS as readonly string[]).includes(value);
94
+ }
95
+
96
+ function isValidPickerAction(value: string): value is DiscordModelPickerAction {
97
+ return (PICKER_ACTIONS as readonly string[]).includes(value);
98
+ }
99
+
100
+ function isValidPickerView(value: string): value is DiscordModelPickerView {
101
+ return (PICKER_VIEWS as readonly string[]).includes(value);
102
+ }
103
+
104
+ export function normalizeModelPickerPage(value: number | undefined): number {
105
+ const numeric = typeof value === "number" ? value : Number.NaN;
106
+ if (!Number.isFinite(numeric)) {
107
+ return 1;
108
+ }
109
+ return Math.max(1, Math.floor(numeric));
110
+ }
111
+
112
+ function parseRawPage(value: unknown): number {
113
+ if (typeof value === "number") {
114
+ return normalizeModelPickerPage(value);
115
+ }
116
+ if (typeof value === "string" && value.trim()) {
117
+ const parsed = Number.parseInt(value, 10);
118
+ if (Number.isFinite(parsed)) {
119
+ return normalizeModelPickerPage(parsed);
120
+ }
121
+ }
122
+ return 1;
123
+ }
124
+
125
+ function parseRawPositiveInt(value: unknown): number | undefined {
126
+ if (typeof value !== "string" && typeof value !== "number") {
127
+ return undefined;
128
+ }
129
+ const parsed = Number.parseInt(String(value), 10);
130
+ if (!Number.isFinite(parsed) || parsed < 1) {
131
+ return undefined;
132
+ }
133
+ return Math.floor(parsed);
134
+ }
135
+
136
+ function coerceString(value: unknown): string {
137
+ return typeof value === "string" || typeof value === "number" ? String(value) : "";
138
+ }
139
+
140
+ function clampPageSize(rawPageSize: number | undefined, max: number, fallback: number): number {
141
+ if (!Number.isFinite(rawPageSize)) {
142
+ return fallback;
143
+ }
144
+ return Math.min(max, Math.max(1, Math.floor(rawPageSize ?? fallback)));
145
+ }
146
+
147
+ function paginateItems<T>(params: {
148
+ items: T[];
149
+ page: number;
150
+ pageSize: number;
151
+ }): DiscordModelPickerPage<T> {
152
+ const totalItems = params.items.length;
153
+ const totalPages = Math.max(1, Math.ceil(totalItems / params.pageSize));
154
+ const page = Math.max(1, Math.min(params.page, totalPages));
155
+ const startIndex = (page - 1) * params.pageSize;
156
+ const endIndexExclusive = Math.min(totalItems, startIndex + params.pageSize);
157
+
158
+ return {
159
+ items: params.items.slice(startIndex, endIndexExclusive),
160
+ page,
161
+ pageSize: params.pageSize,
162
+ totalPages,
163
+ totalItems,
164
+ hasPrev: page > 1,
165
+ hasNext: page < totalPages,
166
+ };
167
+ }
168
+
169
+ export async function loadDiscordModelPickerData(
170
+ cfg: OpenClawConfig,
171
+ agentId?: string,
172
+ ): Promise<ModelsProviderData> {
173
+ const { buildModelsProviderData } = await loadModelsProviderRuntime();
174
+ return buildModelsProviderData(cfg, agentId);
175
+ }
176
+
177
+ export function buildDiscordModelPickerCustomId(params: {
178
+ command: DiscordModelPickerCommandContext;
179
+ action: DiscordModelPickerAction;
180
+ view: DiscordModelPickerView;
181
+ userId: string;
182
+ provider?: string;
183
+ runtime?: string;
184
+ page?: number;
185
+ providerPage?: number;
186
+ modelIndex?: number;
187
+ recentSlot?: number;
188
+ }): string {
189
+ const userId = params.userId.trim();
190
+ if (!userId) {
191
+ throw new Error("Discord model picker custom_id requires userId");
192
+ }
193
+
194
+ const page = normalizeModelPickerPage(params.page);
195
+ const providerPage =
196
+ typeof params.providerPage === "number" && Number.isFinite(params.providerPage)
197
+ ? Math.max(1, Math.floor(params.providerPage))
198
+ : undefined;
199
+ const normalizedProvider = params.provider ? normalizeProviderId(params.provider) : undefined;
200
+ const modelIndex =
201
+ typeof params.modelIndex === "number" && Number.isFinite(params.modelIndex)
202
+ ? Math.max(1, Math.floor(params.modelIndex))
203
+ : undefined;
204
+ const recentSlot =
205
+ typeof params.recentSlot === "number" && Number.isFinite(params.recentSlot)
206
+ ? Math.max(1, Math.floor(params.recentSlot))
207
+ : undefined;
208
+
209
+ const parts = [
210
+ `${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:c=${encodeCustomIdValue(params.command)}`,
211
+ `a=${encodeCustomIdValue(params.action)}`,
212
+ `v=${encodeCustomIdValue(params.view)}`,
213
+ `u=${encodeCustomIdValue(userId)}`,
214
+ `g=${String(page)}`,
215
+ ];
216
+ if (normalizedProvider) {
217
+ parts.push(`p=${encodeCustomIdValue(normalizedProvider)}`);
218
+ }
219
+ const runtime = params.runtime?.trim();
220
+ if (runtime) {
221
+ parts.push(`r=${encodeCustomIdValue(runtime)}`);
222
+ }
223
+ if (providerPage) {
224
+ parts.push(`pp=${String(providerPage)}`);
225
+ }
226
+ if (modelIndex) {
227
+ parts.push(`mi=${String(modelIndex)}`);
228
+ }
229
+ if (recentSlot) {
230
+ parts.push(`rs=${String(recentSlot)}`);
231
+ }
232
+
233
+ const customId = parts.join(";");
234
+ if (customId.length > DISCORD_CUSTOM_ID_MAX_CHARS) {
235
+ throw new Error(
236
+ `Discord model picker custom_id exceeds ${DISCORD_CUSTOM_ID_MAX_CHARS} chars (${customId.length})`,
237
+ );
238
+ }
239
+ return customId;
240
+ }
241
+
242
+ export function parseDiscordModelPickerCustomId(customId: string): DiscordModelPickerState | null {
243
+ const trimmed = customId.trim();
244
+ if (!trimmed.startsWith(`${DISCORD_MODEL_PICKER_CUSTOM_ID_KEY}:`)) {
245
+ return null;
246
+ }
247
+
248
+ const rawParts = trimmed.split(";");
249
+ const data: Record<string, string> = {};
250
+ for (const part of rawParts) {
251
+ const equalsIndex = part.indexOf("=");
252
+ if (equalsIndex <= 0) {
253
+ continue;
254
+ }
255
+ const rawKey = part.slice(0, equalsIndex);
256
+ const rawValue = part.slice(equalsIndex + 1);
257
+ const key = rawKey.includes(":") ? rawKey.split(":").slice(1).join(":") : rawKey;
258
+ if (!key) {
259
+ continue;
260
+ }
261
+ data[key] = rawValue;
262
+ }
263
+
264
+ return parseDiscordModelPickerData(data);
265
+ }
266
+
267
+ export function parseDiscordModelPickerData(data: ComponentData): DiscordModelPickerState | null {
268
+ if (!data || typeof data !== "object") {
269
+ return null;
270
+ }
271
+
272
+ const command = decodeCustomIdValue(coerceString(data.c ?? data.cmd));
273
+ const action = decodeCustomIdValue(coerceString(data.a ?? data.act));
274
+ const view = decodeCustomIdValue(coerceString(data.v ?? data.view));
275
+ const userId = decodeCustomIdValue(coerceString(data.u));
276
+ const providerRaw = decodeCustomIdValue(coerceString(data.p));
277
+ const runtimeRaw = decodeCustomIdValue(coerceString(data.r));
278
+ const page = parseRawPage(data.g ?? data.pg);
279
+ const providerPage = parseRawPositiveInt(data.pp);
280
+ const modelIndex = parseRawPositiveInt(data.mi);
281
+ const recentSlot = parseRawPositiveInt(data.rs);
282
+
283
+ if (!isValidCommandContext(command) || !isValidPickerAction(action) || !isValidPickerView(view)) {
284
+ return null;
285
+ }
286
+
287
+ const trimmedUserId = userId.trim();
288
+ if (!trimmedUserId) {
289
+ return null;
290
+ }
291
+
292
+ const provider = providerRaw ? normalizeProviderId(providerRaw) : undefined;
293
+ const runtime = runtimeRaw.trim() || undefined;
294
+
295
+ return {
296
+ command,
297
+ action,
298
+ view,
299
+ userId: trimmedUserId,
300
+ provider,
301
+ runtime,
302
+ page,
303
+ ...(typeof providerPage === "number" ? { providerPage } : {}),
304
+ ...(typeof modelIndex === "number" ? { modelIndex } : {}),
305
+ ...(typeof recentSlot === "number" ? { recentSlot } : {}),
306
+ };
307
+ }
308
+
309
+ export function buildDiscordModelPickerProviderItems(
310
+ data: ModelsProviderData,
311
+ ): DiscordModelPickerProviderItem[] {
312
+ return data.providers.map((provider) => ({
313
+ id: provider,
314
+ count: data.byProvider.get(provider)?.size ?? 0,
315
+ }));
316
+ }
317
+
318
+ export function getDiscordModelPickerProviderPage(params: {
319
+ data: ModelsProviderData;
320
+ page?: number;
321
+ pageSize?: number;
322
+ }): DiscordModelPickerPage<DiscordModelPickerProviderItem> {
323
+ const items = buildDiscordModelPickerProviderItems(params.data);
324
+ const canFitSinglePage = items.length <= DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX;
325
+ const maxPageSize = canFitSinglePage
326
+ ? DISCORD_MODEL_PICKER_PROVIDER_SINGLE_PAGE_MAX
327
+ : DISCORD_MODEL_PICKER_PROVIDER_PAGE_SIZE;
328
+ const pageSize = clampPageSize(params.pageSize, maxPageSize, maxPageSize);
329
+ return paginateItems({
330
+ items,
331
+ page: normalizeModelPickerPage(params.page),
332
+ pageSize,
333
+ });
334
+ }
335
+
336
+ export function getDiscordModelPickerModelPage(params: {
337
+ data: ModelsProviderData;
338
+ provider: string;
339
+ page?: number;
340
+ pageSize?: number;
341
+ }): DiscordModelPickerModelPage | null {
342
+ const provider = normalizeProviderId(params.provider);
343
+ const modelSet = params.data.byProvider.get(provider);
344
+ if (!modelSet) {
345
+ return null;
346
+ }
347
+
348
+ const pageSize = clampPageSize(
349
+ params.pageSize,
350
+ DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE,
351
+ DISCORD_MODEL_PICKER_MODEL_PAGE_SIZE,
352
+ );
353
+ const models = [...modelSet].toSorted();
354
+ const page = paginateItems({
355
+ items: models,
356
+ page: normalizeModelPickerPage(params.page),
357
+ pageSize,
358
+ });
359
+
360
+ return {
361
+ ...page,
362
+ provider,
363
+ };
364
+ }
@@ -0,0 +1,26 @@
1
+ import type { ModelsProviderData } from "openclaw/plugin-sdk/models-provider-runtime";
2
+
3
+ export function createModelsProviderData(
4
+ entries: Record<string, string[]>,
5
+ opts?: { defaultProviderOrder?: "insertion" | "sorted" },
6
+ ): ModelsProviderData {
7
+ const byProvider = new Map<string, Set<string>>();
8
+ for (const [provider, models] of Object.entries(entries)) {
9
+ byProvider.set(provider, new Set(models));
10
+ }
11
+ const providers = Object.keys(entries).toSorted();
12
+ const insertionProvider = Object.keys(entries)[0];
13
+ const defaultProvider =
14
+ opts?.defaultProviderOrder === "sorted"
15
+ ? (providers[0] ?? "openai")
16
+ : (insertionProvider ?? "openai");
17
+ return {
18
+ byProvider,
19
+ providers,
20
+ resolvedDefault: {
21
+ provider: defaultProvider,
22
+ model: entries[defaultProvider]?.[0] ?? "gpt-4o",
23
+ },
24
+ modelNames: new Map<string, string>(),
25
+ };
26
+ }