@ashx-j/lunr 0.2.25 → 0.2.27

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 (729) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/LICENSE +22 -0
  3. package/dist/builtin-extensions/ashxj-thinking.d.ts +1 -1
  4. package/dist/builtin-extensions/ashxj-thinking.d.ts.map +1 -1
  5. package/dist/builtin-extensions/ashxj-thinking.js +1 -1
  6. package/dist/builtin-extensions/ashxj-thinking.js.map +1 -1
  7. package/dist/builtin-extensions/ashxj-tui.d.ts +11 -1
  8. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  9. package/dist/builtin-extensions/ashxj-tui.js +6 -10
  10. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  11. package/dist/builtin-extensions/index.d.ts.map +1 -1
  12. package/dist/builtin-extensions/index.js +1 -0
  13. package/dist/builtin-extensions/index.js.map +1 -1
  14. package/dist/builtin-extensions/lunr-computer-use.d.ts +3 -0
  15. package/dist/builtin-extensions/lunr-computer-use.d.ts.map +1 -0
  16. package/dist/builtin-extensions/lunr-computer-use.js +179 -0
  17. package/dist/builtin-extensions/lunr-computer-use.js.map +1 -0
  18. package/dist/builtin-extensions/lunr-cron.d.ts +4 -5
  19. package/dist/builtin-extensions/lunr-cron.d.ts.map +1 -1
  20. package/dist/builtin-extensions/lunr-cron.js +94 -108
  21. package/dist/builtin-extensions/lunr-cron.js.map +1 -1
  22. package/dist/builtin-extensions/lunr-local-providers/local-servers.d.ts.map +1 -1
  23. package/dist/builtin-extensions/lunr-local-providers/local-servers.js +3 -1
  24. package/dist/builtin-extensions/lunr-local-providers/local-servers.js.map +1 -1
  25. package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.d.ts.map +1 -1
  26. package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.js +2 -2
  27. package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.js.map +1 -1
  28. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.d.ts.map +1 -1
  29. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js +1 -1
  30. package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js.map +1 -1
  31. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.d.ts.map +1 -1
  32. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.js +1 -1
  33. package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.js.map +1 -1
  34. package/dist/builtin-extensions/pi-intercom/index.d.ts.map +1 -1
  35. package/dist/builtin-extensions/pi-intercom/index.js +2 -2
  36. package/dist/builtin-extensions/pi-intercom/index.js.map +1 -1
  37. package/dist/builtin-extensions/pi-lsp-extension/bemol-extension/index.d.ts +1 -1
  38. package/dist/builtin-extensions/pi-lsp-extension/bemol-extension/index.d.ts.map +1 -1
  39. package/dist/builtin-extensions/pi-lsp-extension/bemol-extension/index.js +1 -1
  40. package/dist/builtin-extensions/pi-lsp-extension/bemol-extension/index.js.map +1 -1
  41. package/dist/builtin-extensions/pi-lsp-extension/src/index.d.ts.map +1 -1
  42. package/dist/builtin-extensions/pi-lsp-extension/src/index.js +1 -15
  43. package/dist/builtin-extensions/pi-lsp-extension/src/index.js.map +1 -1
  44. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-client.d.ts +6 -0
  45. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-client.d.ts.map +1 -1
  46. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-client.js +187 -132
  47. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-client.js.map +1 -1
  48. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-manager.d.ts +8 -0
  49. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-manager.d.ts.map +1 -1
  50. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-manager.js +142 -112
  51. package/dist/builtin-extensions/pi-lsp-extension/src/lsp-manager.js.map +1 -1
  52. package/dist/builtin-extensions/pi-lsp-extension/src/tools/code-rewrite.d.ts +12 -1
  53. package/dist/builtin-extensions/pi-lsp-extension/src/tools/code-rewrite.d.ts.map +1 -1
  54. package/dist/builtin-extensions/pi-lsp-extension/src/tools/code-rewrite.js +14 -7
  55. package/dist/builtin-extensions/pi-lsp-extension/src/tools/code-rewrite.js.map +1 -1
  56. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/rewrite-engine.d.ts +11 -3
  57. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/rewrite-engine.d.ts.map +1 -1
  58. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/rewrite-engine.js +67 -41
  59. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/rewrite-engine.js.map +1 -1
  60. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/search-engine.d.ts +4 -2
  61. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/search-engine.d.ts.map +1 -1
  62. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/search-engine.js +3 -0
  63. package/dist/builtin-extensions/pi-lsp-extension/src/tree-sitter/search-engine.js.map +1 -1
  64. package/dist/builtin-extensions/pi-mcp-adapter/commands.d.ts +2 -1
  65. package/dist/builtin-extensions/pi-mcp-adapter/commands.d.ts.map +1 -1
  66. package/dist/builtin-extensions/pi-mcp-adapter/commands.js +6 -6
  67. package/dist/builtin-extensions/pi-mcp-adapter/commands.js.map +1 -1
  68. package/dist/builtin-extensions/pi-mcp-adapter/config.d.ts +1 -1
  69. package/dist/builtin-extensions/pi-mcp-adapter/config.d.ts.map +1 -1
  70. package/dist/builtin-extensions/pi-mcp-adapter/config.js +11 -5
  71. package/dist/builtin-extensions/pi-mcp-adapter/config.js.map +1 -1
  72. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.d.ts.map +1 -1
  73. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.js +2 -2
  74. package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.js.map +1 -1
  75. package/dist/builtin-extensions/pi-mcp-adapter/host-html-template.d.ts.map +1 -1
  76. package/dist/builtin-extensions/pi-mcp-adapter/host-html-template.js +1 -1
  77. package/dist/builtin-extensions/pi-mcp-adapter/host-html-template.js.map +1 -1
  78. package/dist/builtin-extensions/pi-mcp-adapter/index.d.ts.map +1 -1
  79. package/dist/builtin-extensions/pi-mcp-adapter/index.js +48 -40
  80. package/dist/builtin-extensions/pi-mcp-adapter/index.js.map +1 -1
  81. package/dist/builtin-extensions/pi-mcp-adapter/init.d.ts.map +1 -1
  82. package/dist/builtin-extensions/pi-mcp-adapter/init.js +3 -1
  83. package/dist/builtin-extensions/pi-mcp-adapter/init.js.map +1 -1
  84. package/dist/builtin-extensions/pi-mcp-adapter/mcp-auth-flow.d.ts +12 -40
  85. package/dist/builtin-extensions/pi-mcp-adapter/mcp-auth-flow.d.ts.map +1 -1
  86. package/dist/builtin-extensions/pi-mcp-adapter/mcp-auth-flow.js +166 -235
  87. package/dist/builtin-extensions/pi-mcp-adapter/mcp-auth-flow.js.map +1 -1
  88. package/dist/builtin-extensions/pi-mcp-adapter/mcp-callback-server.d.ts +5 -0
  89. package/dist/builtin-extensions/pi-mcp-adapter/mcp-callback-server.d.ts.map +1 -1
  90. package/dist/builtin-extensions/pi-mcp-adapter/mcp-callback-server.js +43 -13
  91. package/dist/builtin-extensions/pi-mcp-adapter/mcp-callback-server.js.map +1 -1
  92. package/dist/builtin-extensions/pi-mcp-adapter/mcp-oauth-provider.d.ts +14 -3
  93. package/dist/builtin-extensions/pi-mcp-adapter/mcp-oauth-provider.d.ts.map +1 -1
  94. package/dist/builtin-extensions/pi-mcp-adapter/mcp-oauth-provider.js +73 -21
  95. package/dist/builtin-extensions/pi-mcp-adapter/mcp-oauth-provider.js.map +1 -1
  96. package/dist/builtin-extensions/pi-mcp-adapter/proxy-modes.d.ts.map +1 -1
  97. package/dist/builtin-extensions/pi-mcp-adapter/proxy-modes.js +4 -4
  98. package/dist/builtin-extensions/pi-mcp-adapter/proxy-modes.js.map +1 -1
  99. package/dist/builtin-extensions/pi-mcp-adapter/server-manager.d.ts.map +1 -1
  100. package/dist/builtin-extensions/pi-mcp-adapter/server-manager.js +2 -2
  101. package/dist/builtin-extensions/pi-mcp-adapter/server-manager.js.map +1 -1
  102. package/dist/builtin-extensions/pi-mcp-adapter/state.d.ts +2 -0
  103. package/dist/builtin-extensions/pi-mcp-adapter/state.d.ts.map +1 -1
  104. package/dist/builtin-extensions/pi-mcp-adapter/state.js.map +1 -1
  105. package/dist/builtin-extensions/pi-ollama-cloud/index.d.ts +1 -1
  106. package/dist/builtin-extensions/pi-ollama-cloud/index.d.ts.map +1 -1
  107. package/dist/builtin-extensions/pi-ollama-cloud/index.js +1 -1
  108. package/dist/builtin-extensions/pi-ollama-cloud/index.js.map +1 -1
  109. package/dist/builtin-extensions/pi-ollama-cloud/models.d.ts +11 -5
  110. package/dist/builtin-extensions/pi-ollama-cloud/models.d.ts.map +1 -1
  111. package/dist/builtin-extensions/pi-ollama-cloud/models.js +12 -32
  112. package/dist/builtin-extensions/pi-ollama-cloud/models.js.map +1 -1
  113. package/dist/builtin-extensions/pi-prompt-template-model/index.d.ts.map +1 -1
  114. package/dist/builtin-extensions/pi-prompt-template-model/index.js +5 -3
  115. package/dist/builtin-extensions/pi-prompt-template-model/index.js.map +1 -1
  116. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.d.ts +2 -1
  117. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.d.ts.map +1 -1
  118. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.js +21 -12
  119. package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.js.map +1 -1
  120. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.d.ts.map +1 -1
  121. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.js +7 -24
  122. package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.js.map +1 -1
  123. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  124. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +39 -77
  125. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  126. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
  127. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +1 -1
  128. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
  129. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +4 -4
  130. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  131. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +8 -5
  132. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  133. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.d.ts +1 -1
  134. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.d.ts.map +1 -1
  135. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.js +15 -31
  136. package/dist/builtin-extensions/pi-subagents/src/runs/background/notify.js.map +1 -1
  137. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.d.ts.map +1 -1
  138. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.js +7 -2
  139. package/dist/builtin-extensions/pi-subagents/src/runs/background/result-watcher.js.map +1 -1
  140. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts +1 -0
  141. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts.map +1 -1
  142. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js +21 -4
  143. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js.map +1 -1
  144. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.d.ts.map +1 -1
  145. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js +11 -4
  146. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js.map +1 -1
  147. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
  148. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +121 -25
  149. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
  150. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  151. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +25 -5
  152. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  153. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.d.ts +1 -0
  154. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.d.ts.map +1 -1
  155. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.js +5 -0
  156. package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.js.map +1 -1
  157. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.d.ts +1 -1
  158. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.d.ts.map +1 -1
  159. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js +2 -0
  160. package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js.map +1 -1
  161. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts +1 -1
  162. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts.map +1 -1
  163. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js +1 -1
  164. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js.map +1 -1
  165. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.d.ts +7 -8
  166. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.d.ts.map +1 -1
  167. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.js +60 -32
  168. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.js.map +1 -1
  169. package/dist/builtin-extensions/pi-subagents/src/shared/utils.d.ts +1 -1
  170. package/dist/builtin-extensions/pi-subagents/src/shared/utils.d.ts.map +1 -1
  171. package/dist/builtin-extensions/pi-subagents/src/shared/utils.js +15 -5
  172. package/dist/builtin-extensions/pi-subagents/src/shared/utils.js.map +1 -1
  173. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.d.ts.map +1 -1
  174. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js +4 -3
  175. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js.map +1 -1
  176. package/dist/builtin-extensions/pi-subagents/src/tui/communication.d.ts.map +1 -1
  177. package/dist/builtin-extensions/pi-subagents/src/tui/communication.js +1 -1
  178. package/dist/builtin-extensions/pi-subagents/src/tui/communication.js.map +1 -1
  179. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +2 -1
  180. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  181. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +22 -11
  182. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  183. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.d.ts.map +1 -1
  184. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.js +3 -3
  185. package/dist/builtin-extensions/pi-subagents/src/watchdog/review.js.map +1 -1
  186. package/dist/builtin-extensions/pi-tps.d.ts.map +1 -1
  187. package/dist/builtin-extensions/pi-tps.js +20 -17
  188. package/dist/builtin-extensions/pi-tps.js.map +1 -1
  189. package/dist/builtin-extensions/pi-web-access/activity.d.ts +1 -0
  190. package/dist/builtin-extensions/pi-web-access/activity.d.ts.map +1 -1
  191. package/dist/builtin-extensions/pi-web-access/activity.js +27 -5
  192. package/dist/builtin-extensions/pi-web-access/activity.js.map +1 -1
  193. package/dist/builtin-extensions/pi-web-access/bounded-body.d.ts +3 -0
  194. package/dist/builtin-extensions/pi-web-access/bounded-body.d.ts.map +1 -0
  195. package/dist/builtin-extensions/pi-web-access/bounded-body.js +43 -0
  196. package/dist/builtin-extensions/pi-web-access/bounded-body.js.map +1 -0
  197. package/dist/builtin-extensions/pi-web-access/content-page.d.ts +24 -0
  198. package/dist/builtin-extensions/pi-web-access/content-page.d.ts.map +1 -0
  199. package/dist/builtin-extensions/pi-web-access/content-page.js +57 -0
  200. package/dist/builtin-extensions/pi-web-access/content-page.js.map +1 -0
  201. package/dist/builtin-extensions/pi-web-access/extract.d.ts.map +1 -1
  202. package/dist/builtin-extensions/pi-web-access/extract.js +7 -2
  203. package/dist/builtin-extensions/pi-web-access/extract.js.map +1 -1
  204. package/dist/builtin-extensions/pi-web-access/github-extract.d.ts +0 -1
  205. package/dist/builtin-extensions/pi-web-access/github-extract.d.ts.map +1 -1
  206. package/dist/builtin-extensions/pi-web-access/github-extract.js +147 -36
  207. package/dist/builtin-extensions/pi-web-access/github-extract.js.map +1 -1
  208. package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
  209. package/dist/builtin-extensions/pi-web-access/index.js +48 -73
  210. package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
  211. package/dist/builtin-extensions/pi-web-access/session-cleanup.d.ts +12 -4
  212. package/dist/builtin-extensions/pi-web-access/session-cleanup.d.ts.map +1 -1
  213. package/dist/builtin-extensions/pi-web-access/session-cleanup.js +35 -12
  214. package/dist/builtin-extensions/pi-web-access/session-cleanup.js.map +1 -1
  215. package/dist/builtin-extensions/pi-web-access/storage.d.ts +9 -6
  216. package/dist/builtin-extensions/pi-web-access/storage.d.ts.map +1 -1
  217. package/dist/builtin-extensions/pi-web-access/storage.js +36 -24
  218. package/dist/builtin-extensions/pi-web-access/storage.js.map +1 -1
  219. package/dist/builtin-extensions/simple-pi-memory.d.ts +1 -1
  220. package/dist/builtin-extensions/simple-pi-memory.d.ts.map +1 -1
  221. package/dist/builtin-extensions/simple-pi-memory.js +1 -1
  222. package/dist/builtin-extensions/simple-pi-memory.js.map +1 -1
  223. package/dist/cli/install-cli.d.ts.map +1 -1
  224. package/dist/cli/install-cli.js +38 -30
  225. package/dist/cli/install-cli.js.map +1 -1
  226. package/dist/core/agent-session-runtime.d.ts +13 -0
  227. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  228. package/dist/core/agent-session-runtime.js +75 -11
  229. package/dist/core/agent-session-runtime.js.map +1 -1
  230. package/dist/core/agent-session-services.d.ts +1 -0
  231. package/dist/core/agent-session-services.d.ts.map +1 -1
  232. package/dist/core/agent-session-services.js +1 -0
  233. package/dist/core/agent-session-services.js.map +1 -1
  234. package/dist/core/agent-session.d.ts +40 -6
  235. package/dist/core/agent-session.d.ts.map +1 -1
  236. package/dist/core/agent-session.js +309 -76
  237. package/dist/core/agent-session.js.map +1 -1
  238. package/dist/core/browser/runtime.d.ts +2 -0
  239. package/dist/core/browser/runtime.d.ts.map +1 -1
  240. package/dist/core/browser/runtime.js +80 -45
  241. package/dist/core/browser/runtime.js.map +1 -1
  242. package/dist/core/browser/setup.d.ts.map +1 -1
  243. package/dist/core/browser/setup.js +12 -1
  244. package/dist/core/browser/setup.js.map +1 -1
  245. package/dist/core/computer-use/adapter.d.ts +28 -0
  246. package/dist/core/computer-use/adapter.d.ts.map +1 -0
  247. package/dist/core/computer-use/adapter.js +138 -0
  248. package/dist/core/computer-use/adapter.js.map +1 -0
  249. package/dist/core/computer-use/image.d.ts +27 -0
  250. package/dist/core/computer-use/image.d.ts.map +1 -0
  251. package/dist/core/computer-use/image.js +84 -0
  252. package/dist/core/computer-use/image.js.map +1 -0
  253. package/dist/core/computer-use/lease.d.ts +18 -0
  254. package/dist/core/computer-use/lease.d.ts.map +1 -0
  255. package/dist/core/computer-use/lease.js +129 -0
  256. package/dist/core/computer-use/lease.js.map +1 -0
  257. package/dist/core/computer-use/macos-runtime.d.ts +14 -0
  258. package/dist/core/computer-use/macos-runtime.d.ts.map +1 -0
  259. package/dist/core/computer-use/macos-runtime.js +148 -0
  260. package/dist/core/computer-use/macos-runtime.js.map +1 -0
  261. package/dist/core/computer-use/policy.d.ts +19 -0
  262. package/dist/core/computer-use/policy.d.ts.map +1 -0
  263. package/dist/core/computer-use/policy.js +67 -0
  264. package/dist/core/computer-use/policy.js.map +1 -0
  265. package/dist/core/computer-use/release.generated.d.ts +31 -0
  266. package/dist/core/computer-use/release.generated.d.ts.map +1 -0
  267. package/dist/core/computer-use/release.generated.js +36 -0
  268. package/dist/core/computer-use/release.generated.js.map +1 -0
  269. package/dist/core/computer-use/runtime.d.ts +13 -0
  270. package/dist/core/computer-use/runtime.d.ts.map +1 -0
  271. package/dist/core/computer-use/runtime.js +158 -0
  272. package/dist/core/computer-use/runtime.js.map +1 -0
  273. package/dist/core/computer-use/schemas.d.ts +100 -0
  274. package/dist/core/computer-use/schemas.d.ts.map +1 -0
  275. package/dist/core/computer-use/schemas.js +96 -0
  276. package/dist/core/computer-use/schemas.js.map +1 -0
  277. package/dist/core/computer-use/windows-desktop.d.ts +3 -0
  278. package/dist/core/computer-use/windows-desktop.d.ts.map +1 -0
  279. package/dist/core/computer-use/windows-desktop.js +48 -0
  280. package/dist/core/computer-use/windows-desktop.js.map +1 -0
  281. package/dist/core/computer-use/workflow.d.ts +38 -0
  282. package/dist/core/computer-use/workflow.d.ts.map +1 -0
  283. package/dist/core/computer-use/workflow.js +900 -0
  284. package/dist/core/computer-use/workflow.js.map +1 -0
  285. package/dist/core/cron/jobs.d.ts +26 -8
  286. package/dist/core/cron/jobs.d.ts.map +1 -1
  287. package/dist/core/cron/jobs.js +357 -232
  288. package/dist/core/cron/jobs.js.map +1 -1
  289. package/dist/core/cron/origin-context.d.ts +1 -0
  290. package/dist/core/cron/origin-context.d.ts.map +1 -1
  291. package/dist/core/cron/origin-context.js.map +1 -1
  292. package/dist/core/cron/scheduler.d.ts +22 -17
  293. package/dist/core/cron/scheduler.d.ts.map +1 -1
  294. package/dist/core/cron/scheduler.js +153 -73
  295. package/dist/core/cron/scheduler.js.map +1 -1
  296. package/dist/core/extensions/index.d.ts +1 -1
  297. package/dist/core/extensions/index.d.ts.map +1 -1
  298. package/dist/core/extensions/index.js.map +1 -1
  299. package/dist/core/extensions/loader.d.ts.map +1 -1
  300. package/dist/core/extensions/loader.js +4 -0
  301. package/dist/core/extensions/loader.js.map +1 -1
  302. package/dist/core/extensions/runner.d.ts +1 -0
  303. package/dist/core/extensions/runner.d.ts.map +1 -1
  304. package/dist/core/extensions/runner.js +8 -0
  305. package/dist/core/extensions/runner.js.map +1 -1
  306. package/dist/core/extensions/types.d.ts +8 -0
  307. package/dist/core/extensions/types.d.ts.map +1 -1
  308. package/dist/core/extensions/types.js.map +1 -1
  309. package/dist/core/footer-data-provider.d.ts +4 -0
  310. package/dist/core/footer-data-provider.d.ts.map +1 -1
  311. package/dist/core/footer-data-provider.js +22 -1
  312. package/dist/core/footer-data-provider.js.map +1 -1
  313. package/dist/core/large-subagent-launch.d.ts +1 -1
  314. package/dist/core/large-subagent-launch.d.ts.map +1 -1
  315. package/dist/core/large-subagent-launch.js +7 -7
  316. package/dist/core/large-subagent-launch.js.map +1 -1
  317. package/dist/core/model-registry.d.ts +2 -0
  318. package/dist/core/model-registry.d.ts.map +1 -1
  319. package/dist/core/model-registry.js +5 -0
  320. package/dist/core/model-registry.js.map +1 -1
  321. package/dist/core/model-runtime.d.ts +4 -1
  322. package/dist/core/model-runtime.d.ts.map +1 -1
  323. package/dist/core/model-runtime.js +40 -4
  324. package/dist/core/model-runtime.js.map +1 -1
  325. package/dist/core/package-manager.d.ts.map +1 -1
  326. package/dist/core/package-manager.js.map +1 -1
  327. package/dist/core/permissions.d.ts +4 -4
  328. package/dist/core/permissions.d.ts.map +1 -1
  329. package/dist/core/permissions.js +25 -38
  330. package/dist/core/permissions.js.map +1 -1
  331. package/dist/core/plan-mode.d.ts +5 -13
  332. package/dist/core/plan-mode.d.ts.map +1 -1
  333. package/dist/core/plan-mode.js +135 -116
  334. package/dist/core/plan-mode.js.map +1 -1
  335. package/dist/core/project-trust.d.ts.map +1 -1
  336. package/dist/core/project-trust.js +1 -1
  337. package/dist/core/project-trust.js.map +1 -1
  338. package/dist/core/prompt-resource-bridge.d.ts +12 -0
  339. package/dist/core/prompt-resource-bridge.d.ts.map +1 -0
  340. package/dist/core/prompt-resource-bridge.js +9 -0
  341. package/dist/core/prompt-resource-bridge.js.map +1 -0
  342. package/dist/core/protected-targets.d.ts +6 -0
  343. package/dist/core/protected-targets.d.ts.map +1 -0
  344. package/dist/core/protected-targets.js +45 -0
  345. package/dist/core/protected-targets.js.map +1 -0
  346. package/dist/core/resource-loader.d.ts.map +1 -1
  347. package/dist/core/resource-loader.js +7 -0
  348. package/dist/core/resource-loader.js.map +1 -1
  349. package/dist/core/rollback.d.ts +19 -3
  350. package/dist/core/rollback.d.ts.map +1 -1
  351. package/dist/core/rollback.js +172 -56
  352. package/dist/core/rollback.js.map +1 -1
  353. package/dist/core/runtime-credentials.d.ts +3 -0
  354. package/dist/core/runtime-credentials.d.ts.map +1 -1
  355. package/dist/core/runtime-credentials.js +14 -1
  356. package/dist/core/runtime-credentials.js.map +1 -1
  357. package/dist/core/sdk.d.ts +3 -0
  358. package/dist/core/sdk.d.ts.map +1 -1
  359. package/dist/core/sdk.js +1 -0
  360. package/dist/core/sdk.js.map +1 -1
  361. package/dist/core/session-manager.d.ts +6 -0
  362. package/dist/core/session-manager.d.ts.map +1 -1
  363. package/dist/core/session-manager.js +95 -32
  364. package/dist/core/session-manager.js.map +1 -1
  365. package/dist/core/session-ownership.d.ts.map +1 -1
  366. package/dist/core/session-ownership.js +8 -3
  367. package/dist/core/session-ownership.js.map +1 -1
  368. package/dist/core/session-retention.d.ts.map +1 -1
  369. package/dist/core/session-retention.js +18 -7
  370. package/dist/core/session-retention.js.map +1 -1
  371. package/dist/core/session-startup-settings.d.ts +10 -0
  372. package/dist/core/session-startup-settings.d.ts.map +1 -0
  373. package/dist/core/session-startup-settings.js +70 -0
  374. package/dist/core/session-startup-settings.js.map +1 -0
  375. package/dist/core/settings-manager.d.ts +6 -0
  376. package/dist/core/settings-manager.d.ts.map +1 -1
  377. package/dist/core/settings-manager.js +19 -0
  378. package/dist/core/settings-manager.js.map +1 -1
  379. package/dist/core/subagent-event-transport.d.ts +3 -0
  380. package/dist/core/subagent-event-transport.d.ts.map +1 -0
  381. package/dist/core/subagent-event-transport.js +61 -0
  382. package/dist/core/subagent-event-transport.js.map +1 -0
  383. package/dist/core/subscriptions.d.ts +16 -7
  384. package/dist/core/subscriptions.d.ts.map +1 -1
  385. package/dist/core/subscriptions.js +96 -55
  386. package/dist/core/subscriptions.js.map +1 -1
  387. package/dist/core/trust-manager.d.ts +2 -1
  388. package/dist/core/trust-manager.d.ts.map +1 -1
  389. package/dist/core/trust-manager.js +6 -1
  390. package/dist/core/trust-manager.js.map +1 -1
  391. package/dist/core/update-check.d.ts.map +1 -1
  392. package/dist/core/update-check.js.map +1 -1
  393. package/dist/core/usage-adapters/kimi-coding.d.ts +2 -2
  394. package/dist/core/usage-adapters/kimi-coding.d.ts.map +1 -1
  395. package/dist/core/usage-adapters/kimi-coding.js +2 -2
  396. package/dist/core/usage-adapters/kimi-coding.js.map +1 -1
  397. package/dist/core/usage-adapters/openai-codex.d.ts +3 -2
  398. package/dist/core/usage-adapters/openai-codex.d.ts.map +1 -1
  399. package/dist/core/usage-adapters/openai-codex.js +3 -3
  400. package/dist/core/usage-adapters/openai-codex.js.map +1 -1
  401. package/dist/core/usage-adapters/xai.d.ts +2 -2
  402. package/dist/core/usage-adapters/xai.d.ts.map +1 -1
  403. package/dist/core/usage-adapters/xai.js +3 -3
  404. package/dist/core/usage-adapters/xai.js.map +1 -1
  405. package/dist/core/usage-adapters/zai.d.ts +2 -2
  406. package/dist/core/usage-adapters/zai.d.ts.map +1 -1
  407. package/dist/core/usage-adapters/zai.js +2 -2
  408. package/dist/core/usage-adapters/zai.js.map +1 -1
  409. package/dist/core/usage-service.d.ts +9 -2
  410. package/dist/core/usage-service.d.ts.map +1 -1
  411. package/dist/core/usage-service.js +95 -50
  412. package/dist/core/usage-service.js.map +1 -1
  413. package/dist/gateway/adapters/discord.d.ts.map +1 -1
  414. package/dist/gateway/adapters/discord.js +2 -2
  415. package/dist/gateway/adapters/discord.js.map +1 -1
  416. package/dist/gateway/agent-bridge.d.ts +9 -1
  417. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  418. package/dist/gateway/agent-bridge.js +91 -37
  419. package/dist/gateway/agent-bridge.js.map +1 -1
  420. package/dist/gateway/approval.d.ts.map +1 -1
  421. package/dist/gateway/approval.js +3 -3
  422. package/dist/gateway/approval.js.map +1 -1
  423. package/dist/gateway/authz.d.ts +4 -4
  424. package/dist/gateway/authz.d.ts.map +1 -1
  425. package/dist/gateway/authz.js +8 -12
  426. package/dist/gateway/authz.js.map +1 -1
  427. package/dist/gateway/buttons.d.ts +8 -1
  428. package/dist/gateway/buttons.d.ts.map +1 -1
  429. package/dist/gateway/buttons.js +76 -18
  430. package/dist/gateway/buttons.js.map +1 -1
  431. package/dist/gateway/commands.d.ts +1 -0
  432. package/dist/gateway/commands.d.ts.map +1 -1
  433. package/dist/gateway/commands.js +12 -10
  434. package/dist/gateway/commands.js.map +1 -1
  435. package/dist/gateway/cron.d.ts +12 -11
  436. package/dist/gateway/cron.d.ts.map +1 -1
  437. package/dist/gateway/cron.js +85 -62
  438. package/dist/gateway/cron.js.map +1 -1
  439. package/dist/gateway/index.d.ts.map +1 -1
  440. package/dist/gateway/index.js +5 -4
  441. package/dist/gateway/index.js.map +1 -1
  442. package/dist/gateway/mobile-commands.d.ts +1 -0
  443. package/dist/gateway/mobile-commands.d.ts.map +1 -1
  444. package/dist/gateway/mobile-commands.js +64 -22
  445. package/dist/gateway/mobile-commands.js.map +1 -1
  446. package/dist/gateway/presenter.d.ts +12 -2
  447. package/dist/gateway/presenter.d.ts.map +1 -1
  448. package/dist/gateway/presenter.js +74 -39
  449. package/dist/gateway/presenter.js.map +1 -1
  450. package/dist/gateway/router.d.ts +2 -2
  451. package/dist/gateway/router.d.ts.map +1 -1
  452. package/dist/gateway/router.js +22 -9
  453. package/dist/gateway/router.js.map +1 -1
  454. package/dist/gateway/service.d.ts +2 -0
  455. package/dist/gateway/service.d.ts.map +1 -1
  456. package/dist/gateway/service.js +153 -0
  457. package/dist/gateway/service.js.map +1 -1
  458. package/dist/gateway/setup.d.ts.map +1 -1
  459. package/dist/gateway/setup.js +6 -4
  460. package/dist/gateway/setup.js.map +1 -1
  461. package/dist/main.d.ts.map +1 -1
  462. package/dist/main.js +86 -33
  463. package/dist/main.js.map +1 -1
  464. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  465. package/dist/modes/interactive/components/assistant-message.js +6 -4
  466. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  467. package/dist/modes/interactive/components/copyable-text.d.ts.map +1 -1
  468. package/dist/modes/interactive/components/copyable-text.js +3 -2
  469. package/dist/modes/interactive/components/copyable-text.js.map +1 -1
  470. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  471. package/dist/modes/interactive/components/extension-editor.js +1 -1
  472. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  473. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  474. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  475. package/dist/modes/interactive/components/settings-selector.js +20 -0
  476. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  477. package/dist/modes/interactive/interactive-mode.d.ts +2 -2
  478. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  479. package/dist/modes/interactive/interactive-mode.js +104 -109
  480. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  481. package/dist/modes/interactive/smooth-streaming.d.ts.map +1 -1
  482. package/dist/modes/interactive/smooth-streaming.js.map +1 -1
  483. package/dist/modes/print-mode.d.ts.map +1 -1
  484. package/dist/modes/print-mode.js +9 -1
  485. package/dist/modes/print-mode.js.map +1 -1
  486. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  487. package/dist/modes/rpc/rpc-mode.js +2 -1
  488. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  489. package/dist/node-runtime/adapter-247RTYLS.js +16 -0
  490. package/dist/node-runtime/agent-session-services-KDMSPQDY.js +53 -0
  491. package/dist/node-runtime/{brave-UWDA3I22.js → brave-2AFFAXNN.js} +1 -1
  492. package/dist/node-runtime/{builtin-extensions-ZLAX7TUH.js → builtin-extensions-OX6KELNX.js} +9 -7
  493. package/dist/node-runtime/{chunk-QH6YL7VK.js → chunk-252ZVPPF.js} +3 -3
  494. package/dist/node-runtime/{chunk-PCEW76PM.js → chunk-2OIPYKLZ.js} +6 -6
  495. package/dist/node-runtime/{chunk-XBWPBZBN.js → chunk-2SWW7KMW.js} +1 -1
  496. package/dist/node-runtime/{chunk-UQUCFAJ3.js → chunk-2VIW3IMF.js} +40 -7
  497. package/dist/node-runtime/{chunk-UOEDI7RP.js → chunk-4KNKQGHM.js} +2 -2
  498. package/dist/node-runtime/{chunk-FQMM2EZ2.js → chunk-64XTQNLA.js} +29 -7
  499. package/dist/node-runtime/{chunk-NRKGMJYU.js → chunk-6O3IW3IA.js} +2 -2
  500. package/dist/node-runtime/{chunk-5ZQXRPMC.js → chunk-6ZZHGUSA.js} +2 -2
  501. package/dist/node-runtime/chunk-A2WO6WIN.js +1197 -0
  502. package/dist/node-runtime/{chunk-ZXSGTGJ6.js → chunk-APVM5NEU.js} +169 -148
  503. package/dist/node-runtime/{chunk-RC7DMV45.js → chunk-AYHH7YSD.js} +205 -144
  504. package/dist/node-runtime/{chunk-LWYIVUGV.js → chunk-B2IBAXNX.js} +48 -31
  505. package/dist/node-runtime/{chunk-M3LZLQW2.js → chunk-BDRGN3L2.js} +1 -0
  506. package/dist/node-runtime/{chunk-GJ2RDBRL.js → chunk-BYC6RKCN.js} +4 -2
  507. package/dist/node-runtime/{chunk-KWIYUUZK.js → chunk-C3P6Q3B2.js} +469 -283
  508. package/dist/node-runtime/{chunk-YCUZEGJG.js → chunk-DQSCFU7R.js} +17 -8
  509. package/dist/node-runtime/chunk-EBLAZVSA.js +4170 -0
  510. package/dist/node-runtime/{chunk-2KIPT4HJ.js → chunk-EPR2F3GU.js} +68 -20
  511. package/dist/node-runtime/{chunk-CSFI6YXL.js → chunk-ES6PBSIE.js} +1 -1
  512. package/dist/node-runtime/{chunk-IF24A3FH.js → chunk-EVM653DD.js} +4 -4
  513. package/dist/node-runtime/{chunk-HTFRRDSF.js → chunk-F2SMQ6ES.js} +3 -2
  514. package/dist/node-runtime/chunk-F2V2F4ZG.js +114 -0
  515. package/dist/node-runtime/chunk-FE3WZI7F.js +290 -0
  516. package/dist/node-runtime/{chunk-GYIXXQAO.js → chunk-FIRA54AC.js} +2 -5
  517. package/dist/node-runtime/{chunk-6VYZ5UFP.js → chunk-FLGEEH27.js} +1 -1
  518. package/dist/node-runtime/{chunk-5EFIU3OM.js → chunk-FTKEZL27.js} +1 -1
  519. package/dist/node-runtime/chunk-FY4A63RK.js +73 -0
  520. package/dist/node-runtime/chunk-FYIFPFXE.js +98 -0
  521. package/dist/node-runtime/{chunk-HMBGROQK.js → chunk-GLDXBBL6.js} +3 -3
  522. package/dist/node-runtime/{chunk-KWA4OC2J.js → chunk-H6YBCWKG.js} +1 -1
  523. package/dist/node-runtime/{chunk-2MI5OPMN.js → chunk-HNNHHJAB.js} +68 -50
  524. package/dist/node-runtime/{chunk-6XL67VVY.js → chunk-HUD2RFJW.js} +17 -2
  525. package/dist/node-runtime/{chunk-I54LGHWM.js → chunk-I2H5QMWW.js} +3 -3
  526. package/dist/node-runtime/chunk-IAZSK7KK.js +108 -0
  527. package/dist/node-runtime/{chunk-2HHLJWGD.js → chunk-IHHTWSXN.js} +9 -9
  528. package/dist/node-runtime/{chunk-DAPBPCBP.js → chunk-IPP5BIV5.js} +25 -3
  529. package/dist/node-runtime/{chunk-QFWG2RVL.js → chunk-IXG6F4NT.js} +11 -0
  530. package/dist/node-runtime/{chunk-RDDAZOMU.js → chunk-JL4HYQXK.js} +1 -1
  531. package/dist/node-runtime/{chunk-CDVHZOOK.js → chunk-JQYNHAKX.js} +1 -1
  532. package/dist/node-runtime/{chunk-WF7PGWRZ.js → chunk-JWEVT363.js} +37 -28
  533. package/dist/node-runtime/{chunk-2HMIRK7W.js → chunk-LHV3V67B.js} +5 -5
  534. package/dist/node-runtime/{chunk-UAEOTO52.js → chunk-MNQXDFPV.js} +6 -2
  535. package/dist/node-runtime/{chunk-IJIFHRTB.js → chunk-MQPKUHRN.js} +165 -85
  536. package/dist/node-runtime/{chunk-MPKSOJBT.js → chunk-NHATCSOI.js} +4 -7
  537. package/dist/node-runtime/{chunk-RMQZU2YU.js → chunk-NNNR3QIY.js} +1 -1
  538. package/dist/node-runtime/{chunk-RWTKOC3B.js → chunk-QBTJG3UM.js} +1 -1
  539. package/dist/node-runtime/{chunk-E4D3IGP2.js → chunk-QNVMKNNN.js} +20 -17
  540. package/dist/node-runtime/{chunk-GS42T6MX.js → chunk-QW3AAHWN.js} +1 -1
  541. package/dist/node-runtime/{chunk-7S3T6YEA.js → chunk-R4WSB2MQ.js} +5 -5
  542. package/dist/node-runtime/{chunk-B4HP6ZWM.js → chunk-RHCGYWO7.js} +1 -1
  543. package/dist/node-runtime/{chunk-3XPLBUEM.js → chunk-RLGY2QIL.js} +49 -59
  544. package/dist/node-runtime/{chunk-KKUTQQV7.js → chunk-RYRGL7P4.js} +1 -1
  545. package/dist/node-runtime/{chunk-AD6VXAO7.js → chunk-TCT6PCUR.js} +47 -115
  546. package/dist/node-runtime/chunk-U5F4IC7R.js +53 -0
  547. package/dist/node-runtime/{chunk-OCVIXVTZ.js → chunk-UBWW2PID.js} +13 -2
  548. package/dist/node-runtime/{chunk-HFVZUPFJ.js → chunk-UNUZICC4.js} +3 -3
  549. package/dist/node-runtime/{chunk-LQP3RB67.js → chunk-UYRKUKFE.js} +1 -1
  550. package/dist/node-runtime/{chunk-DSIKTUPW.js → chunk-VSUEAI7O.js} +2993 -3820
  551. package/dist/node-runtime/{chunk-NAY6UUQZ.js → chunk-WDIVLMOH.js} +1 -1
  552. package/dist/node-runtime/{chunk-E24EO34J.js → chunk-WPXUR3HE.js} +1 -1
  553. package/dist/node-runtime/{chunk-SLVOAQOY.js → chunk-WXYNALHD.js} +113 -1
  554. package/dist/node-runtime/{chunk-KJ6DWS3K.js → chunk-XEOCUEUJ.js} +121 -60
  555. package/dist/node-runtime/chunk-YD3IZAZ7.js +47 -0
  556. package/dist/node-runtime/chunk-YSB6URMT.js +41 -0
  557. package/dist/node-runtime/{chunk-VQFSUJJG.js → chunk-ZDJUR45D.js} +430 -129
  558. package/dist/node-runtime/{chunk-YDO2F2IA.js → chunk-ZEVFBFNF.js} +95 -142
  559. package/dist/node-runtime/chunk-ZIQUS5QO.js +180 -0
  560. package/dist/node-runtime/cli-runtime.js +4 -4
  561. package/dist/node-runtime/{command-6WDOYKS6.js → command-3HMSY6W2.js} +5 -5
  562. package/dist/node-runtime/{commands-NZLSOHPE.js → commands-D6A6H4TS.js} +11 -16
  563. package/dist/node-runtime/commands-OCSSEEWJ.js +37 -0
  564. package/dist/node-runtime/{config-QDD5PUE2.js → config-CU7M7BTK.js} +1 -1
  565. package/dist/node-runtime/{config-D4QUPAHX.js → config-DS7XLMEM.js} +2 -2
  566. package/dist/node-runtime/{direct-tool-executor-77GW4F5P.js → direct-tool-executor-2QJXVQD2.js} +8 -10
  567. package/dist/node-runtime/{exa-LL64QZR2.js → exa-6VBZNPGE.js} +1 -1
  568. package/dist/node-runtime/{export-html-VM4J2MNR.js → export-html-BRWF5AHI.js} +4 -4
  569. package/dist/node-runtime/{extension-input-E7BJZ35H.js → extension-input-VBBMX7YV.js} +5 -5
  570. package/dist/node-runtime/{extension-selector-WU42XDTU.js → extension-selector-T5AOG5XB.js} +5 -5
  571. package/dist/node-runtime/{extract-EQJE7FZZ.js → extract-BFX2N35A.js} +3 -3
  572. package/dist/node-runtime/{gateway-INAYBF4Q.js → gateway-6623C2U3.js} +61 -53
  573. package/dist/node-runtime/{gemini-search-35OCRFLN.js → gemini-search-C66QHDT6.js} +2 -2
  574. package/dist/node-runtime/{gemini-url-context-UQY3PHHW.js → gemini-url-context-T7DEK73R.js} +3 -3
  575. package/dist/node-runtime/{github-extract-F64RZOEA.js → github-extract-HOGE3D6F.js} +107 -38
  576. package/dist/node-runtime/{goal-NJEE5CTU.js → goal-AIHEDUYA.js} +5 -5
  577. package/dist/node-runtime/index.js +54 -48
  578. package/dist/node-runtime/{init-HS6NQXSK.js → init-JBQJIPYI.js} +3 -4
  579. package/dist/node-runtime/{install-cli-KL4UDDAD.js → install-cli-INGHIVBY.js} +30 -25
  580. package/dist/node-runtime/interactive-mode-FM5DNUSW.js +74 -0
  581. package/dist/node-runtime/{interactive-view-NGFAMHPW.js → interactive-view-K33PM4OS.js} +8 -8
  582. package/dist/node-runtime/{lsp-manager-3ASUSO4T.js → lsp-manager-S554V5HR.js} +240 -155
  583. package/dist/node-runtime/{lunr-browser-IR7TL3J7.js → lunr-browser-H4FDOR3C.js} +7 -7
  584. package/dist/node-runtime/lunr-computer-use-VDP2QNRQ.js +158 -0
  585. package/dist/node-runtime/{lunr-cron-LWIZJM2N.js → lunr-cron-LKVA5CZZ.js} +94 -79
  586. package/dist/node-runtime/main.js +42 -39
  587. package/dist/node-runtime/{mobile-commands-J7OJEKFO.js → mobile-commands-XT3NQXLD.js} +13 -12
  588. package/dist/node-runtime/{model-runtime-OO5HFLWE.js → model-runtime-MZ2JMDV4.js} +4 -4
  589. package/dist/node-runtime/{openai-search-B3MSNCQA.js → openai-search-K72LJDYN.js} +1 -1
  590. package/dist/node-runtime/{package-manager-cli-VYZM5WHN.js → package-manager-cli-CYM262SI.js} +20 -19
  591. package/dist/node-runtime/{parallel-AI2MNSCU.js → parallel-X724NCNU.js} +1 -1
  592. package/dist/node-runtime/{perplexity-QVJW3I35.js → perplexity-RDHQHVMK.js} +2 -2
  593. package/dist/node-runtime/{pi-intercom-QM53RLTS.js → pi-intercom-NHHEPBMI.js} +2 -2
  594. package/dist/node-runtime/{pi-mcp-adapter-5UKBZP44.js → pi-mcp-adapter-PP3GQJQ2.js} +98 -94
  595. package/dist/node-runtime/{pi-ollama-cloud-2F3Y2QNM.js → pi-ollama-cloud-4MNWIMFN.js} +11 -24
  596. package/dist/node-runtime/{pi-prompt-template-model-QWTCGLJU.js → pi-prompt-template-model-FTGELLS5.js} +32 -36
  597. package/dist/node-runtime/{pi-subagents-DNY7EAIG.js → pi-subagents-LT3CTO4E.js} +114 -116
  598. package/dist/node-runtime/{pi-web-access-WTGMNZ32.js → pi-web-access-Z3LP246C.js} +144 -108
  599. package/dist/node-runtime/{print-mode-XSFM26EL.js → print-mode-2XHN5SUA.js} +4 -3
  600. package/dist/node-runtime/{process-registry-4FAKZQOL.js → process-registry-Q4QWIU33.js} +3 -3
  601. package/dist/node-runtime/{proxy-modes-LGEUYD7R.js → proxy-modes-VEGC6QG5.js} +9 -13
  602. package/dist/node-runtime/rpc-entry.js +42 -39
  603. package/dist/node-runtime/{rpc-mode-O3XV4SID.js → rpc-mode-32DHJPEJ.js} +4 -4
  604. package/dist/node-runtime/{runtime-4VA6JKNF.js → runtime-EDUUGAQD.js} +56 -46
  605. package/dist/node-runtime/runtime-UGXM4NPQ.js +21 -0
  606. package/dist/node-runtime/{runtime-bridges-VRDDR73P.js → runtime-bridges-DNKMNJCF.js} +8 -7
  607. package/dist/node-runtime/{service-MDQIPQ26.js → service-IHLGFLBT.js} +6 -4
  608. package/dist/node-runtime/{session-handoff-GWARVEWT.js → session-handoff-LNSPDTKY.js} +4 -4
  609. package/dist/node-runtime/{session-manager-JB5J6XBN.js → session-manager-YIFIAVGC.js} +3 -3
  610. package/dist/node-runtime/{session-selector-GIM4DHQ2.js → session-selector-I73U3EIO.js} +7 -7
  611. package/dist/node-runtime/{settings-manager-J4SPQPDJ.js → settings-manager-P2TKGYTR.js} +6 -5
  612. package/dist/node-runtime/{setup-JBKOLJ6F.js → setup-H7CCELIL.js} +15 -1
  613. package/dist/node-runtime/{setup-CQ2NLVK5.js → setup-OLQ4GXWS.js} +22 -21
  614. package/dist/node-runtime/{src-IVPNZ2BX.js → src-6RV4PXKJ.js} +64 -36
  615. package/dist/node-runtime/{subagent-executor-KZZQGAKV.js → subagent-executor-Q5F7DBVM.js} +141 -62
  616. package/dist/node-runtime/{tavily-WEZFVFP7.js → tavily-KZBFYQB6.js} +1 -1
  617. package/dist/node-runtime/{theme-UOO5RVFN.js → theme-YLVZKKZD.js} +2 -2
  618. package/dist/node-runtime/{trust-manager-MMISNCYO.js → trust-manager-ZCU76D23.js} +2 -2
  619. package/dist/node-runtime/{update-cli-GTFJC7XK.js → update-cli-F3XXJANQ.js} +7 -6
  620. package/dist/node-runtime/{usage-service-LFB2SWYV.js → usage-service-CPAGBED5.js} +1 -1
  621. package/dist/node-runtime/{video-extract-OOZHR7SU.js → video-extract-UPKYIBW3.js} +3 -3
  622. package/dist/node-runtime/workflow-ICDI4N3J.js +767 -0
  623. package/dist/node-runtime/{youtube-extract-FNPAUNHU.js → youtube-extract-77NT7FWH.js} +4 -4
  624. package/dist/utils/clipboard-image.d.ts.map +1 -1
  625. package/dist/utils/clipboard-image.js.map +1 -1
  626. package/dist/utils/exif-orientation.d.ts.map +1 -1
  627. package/dist/utils/exif-orientation.js +25 -10
  628. package/dist/utils/exif-orientation.js.map +1 -1
  629. package/dist/utils/image-resize-core.d.ts +2 -0
  630. package/dist/utils/image-resize-core.d.ts.map +1 -1
  631. package/dist/utils/image-resize-core.js +12 -0
  632. package/dist/utils/image-resize-core.js.map +1 -1
  633. package/dist/utils/paths.d.ts +4 -0
  634. package/dist/utils/paths.d.ts.map +1 -1
  635. package/dist/utils/paths.js +33 -2
  636. package/dist/utils/paths.js.map +1 -1
  637. package/dist/utils/sanitize-terminal-text.d.ts +7 -0
  638. package/dist/utils/sanitize-terminal-text.d.ts.map +1 -0
  639. package/dist/utils/sanitize-terminal-text.js +67 -0
  640. package/dist/utils/sanitize-terminal-text.js.map +1 -0
  641. package/docs/browser.md +1 -1
  642. package/docs/computer-use.md +362 -0
  643. package/docs/custom-provider.md +3 -3
  644. package/docs/extensions.md +8 -4
  645. package/docs/features.md +22 -12
  646. package/docs/keybindings.md +1 -1
  647. package/docs/models.md +5 -5
  648. package/docs/packages.md +1 -1
  649. package/docs/providers.md +2 -2
  650. package/docs/rpc.md +7 -7
  651. package/docs/security.md +10 -5
  652. package/docs/sessions.md +2 -2
  653. package/docs/skills.md +1 -1
  654. package/docs/terminal-setup.md +7 -7
  655. package/docs/themes.md +1 -1
  656. package/docs/usage.md +1 -1
  657. package/examples/extensions/auto-commit-on-exit.ts +1 -1
  658. package/examples/extensions/bash-spawn-hook.ts +1 -1
  659. package/examples/extensions/built-in-tool-renderer.ts +1 -1
  660. package/examples/extensions/claude-rules.ts +1 -1
  661. package/examples/extensions/commands.ts +1 -1
  662. package/examples/extensions/custom-compaction.ts +1 -1
  663. package/examples/extensions/custom-provider-anthropic/index.ts +3 -3
  664. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  665. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  666. package/examples/extensions/custom-provider-gitlab-duo/index.ts +1 -1
  667. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  668. package/examples/extensions/doom-overlay/index.ts +1 -1
  669. package/examples/extensions/git-merge-and-resolve.ts +2 -2
  670. package/examples/extensions/gondolin/index.ts +1 -1
  671. package/examples/extensions/gondolin/package-lock.json +2 -2
  672. package/examples/extensions/gondolin/package.json +1 -1
  673. package/examples/extensions/hidden-thinking-label.ts +1 -1
  674. package/examples/extensions/inline-bash.ts +2 -2
  675. package/examples/extensions/input-transform-streaming.ts +2 -2
  676. package/examples/extensions/input-transform.ts +2 -2
  677. package/examples/extensions/interactive-shell.ts +1 -1
  678. package/examples/extensions/kimi-deferred-tools.ts +1 -1
  679. package/examples/extensions/mac-system-theme.ts +1 -1
  680. package/examples/extensions/minimal-mode.ts +1 -1
  681. package/examples/extensions/modal-editor.ts +1 -1
  682. package/examples/extensions/model-status.ts +1 -1
  683. package/examples/extensions/notify.ts +3 -3
  684. package/examples/extensions/overlay-qa-tests.ts +1 -1
  685. package/examples/extensions/overlay-test.ts +1 -1
  686. package/examples/extensions/pirate.ts +1 -1
  687. package/examples/extensions/preset.ts +3 -3
  688. package/examples/extensions/project-trust.ts +4 -4
  689. package/examples/extensions/prompt-customizer.ts +1 -1
  690. package/examples/extensions/rainbow-editor.ts +1 -1
  691. package/examples/extensions/rpc-demo.ts +1 -1
  692. package/examples/extensions/sandbox/index.ts +7 -7
  693. package/examples/extensions/sandbox/package-lock.json +2 -2
  694. package/examples/extensions/sandbox/package.json +1 -1
  695. package/examples/extensions/shutdown-command.ts +5 -5
  696. package/examples/extensions/ssh.ts +2 -2
  697. package/examples/extensions/subagent/README.md +3 -3
  698. package/examples/extensions/subagent/index.ts +2 -2
  699. package/examples/extensions/titlebar-spinner.ts +1 -1
  700. package/examples/extensions/tool-override.ts +1 -1
  701. package/examples/extensions/tools.ts +1 -1
  702. package/examples/extensions/with-deps/package-lock.json +2 -2
  703. package/examples/extensions/with-deps/package.json +1 -1
  704. package/examples/extensions/working-indicator.ts +1 -1
  705. package/examples/extensions/working-message-test.ts +1 -1
  706. package/examples/sdk/01-minimal.ts +1 -1
  707. package/examples/sdk/03-custom-prompt.ts +1 -1
  708. package/examples/sdk/04-skills.ts +1 -1
  709. package/examples/sdk/06-extensions.ts +2 -2
  710. package/examples/sdk/08-prompt-templates.ts +1 -1
  711. package/install-lock/package-lock.json +3841 -0
  712. package/install-lock/package.json +18 -0
  713. package/native/computer-use/LICENSE.md +21 -0
  714. package/npm-shrinkwrap.json +3858 -0
  715. package/package.json +15 -9
  716. package/dist/modes/interactive/components/exit-card.d.ts +0 -22
  717. package/dist/modes/interactive/components/exit-card.d.ts.map +0 -1
  718. package/dist/modes/interactive/components/exit-card.js +0 -69
  719. package/dist/modes/interactive/components/exit-card.js.map +0 -1
  720. package/dist/node-runtime/agent-session-services-YXXFCP5J.js +0 -49
  721. package/dist/node-runtime/chunk-4HXHDDCF.js +0 -16
  722. package/dist/node-runtime/chunk-4TLO4PO2.js +0 -3427
  723. package/dist/node-runtime/chunk-AUXVGBO4.js +0 -19
  724. package/dist/node-runtime/chunk-OVX6X377.js +0 -50
  725. package/dist/node-runtime/chunk-QTU6AKNJ.js +0 -773
  726. package/dist/node-runtime/chunk-TORR5XBV.js +0 -356
  727. package/dist/node-runtime/commands-QC6VUZJQ.js +0 -36
  728. package/dist/node-runtime/interactive-mode-Z5M7WYYZ.js +0 -72
  729. package/dist/node-runtime/mcp-auth-flow-QIXHW6QN.js +0 -32
@@ -1 +1 @@
1
- {"version":3,"file":"usage-service.d.ts","sourceRoot":"","sources":["../../src/core/usage-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plan/subscription usage service. Per-provider adapters query provider quota\n * endpoints (60-second cache each) so /usage and the footer can show subscription windows\n * alongside session/context totals.\n *\n * Deliberately absent adapters:\n * - anthropic: Anthropic policy prohibits subscription usage tracking in\n * third-party coding agents (2026-07-18 decision). OAuth Anthropic sessions\n * fall back to hiding the cost meter.\n * - ollama-cloud: no usage API exists.\n *\n * Adapter/network errors resolve to `undefined` (callers omit the plan UI).\n * Auth failures are different: they return `error` and are not cached as empty.\n */\n\nimport type { ModelRuntime } from \"./model-runtime.ts\";\nimport { runtimeScope } from \"./runtime-scope.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { fetchKimiPlanUsage } from \"./usage-adapters/kimi-coding.ts\";\nimport { fetchCodexPlanUsage } from \"./usage-adapters/openai-codex.ts\";\nimport { fetchXaiPlanUsage } from \"./usage-adapters/xai.ts\";\nimport { fetchZaiPlanUsage } from \"./usage-adapters/zai.ts\";\n\nexport interface PlanUsageWindow {\n\tlabel: string;\n\tusedPercent: number;\n\t/** Epoch ms when the window resets. */\n\tresetsAt?: number;\n}\n\nexport interface PlanUsage {\n\tprovider: string;\n\tplanLabel?: string;\n\twindows: PlanUsageWindow[];\n}\n\nexport interface PlanUsageResult {\n\t/** First plan, kept for footer and compatibility callers. */\n\tusage?: PlanUsage;\n\t/** Every plan returned by the provider. */\n\tusages?: PlanUsage[];\n\t/** Actionable auth failure. Not cached as an empty success. */\n\terror?: string;\n}\n\nexport interface AllPlanUsageResult {\n\tusages: PlanUsage[];\n\terrors: string[];\n}\n\nconst XAI_RELOGIN = \"xAI login expired. Run /login xai.\";\n\nfunction errorChain(error: unknown): string {\n\tconst parts: string[] = [];\n\tlet current: unknown = error;\n\tconst seen = new Set<unknown>();\n\twhile (current && typeof current === \"object\" && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tif (current instanceof Error) {\n\t\t\tparts.push(current.message);\n\t\t\tcurrent = current.cause;\n\t\t\tcontinue;\n\t\t}\n\t\tbreak;\n\t}\n\treturn parts.join(\" \");\n}\n\n/** Classify adapter/getAuth failures that mean the SuperGrok session is dead. */\nexport function planUsageAuthError(error: unknown): string | undefined {\n\tconst chain = errorChain(error);\n\tif (/timed out|aborted|cancelled|ECONN|ENOTFOUND|network/i.test(chain)) return undefined;\n\t// 403 on billing is often entitlement, not a dead refresh token.\n\tif (/xai billing rejected the session \\(HTTP 401\\)/i.test(chain)) return XAI_RELOGIN;\n\tif (/invalid_grant|refresh token revoked/i.test(chain)) return XAI_RELOGIN;\n\treturn undefined;\n}\n\ntype UsageAdapter = (runtime: ModelRuntime) => Promise<PlanUsage | PlanUsage[] | undefined>;\n\nconst ADAPTERS: Readonly<Record<string, UsageAdapter>> = {\n\t\"openai-codex\": fetchCodexPlanUsage,\n\t\"kimi-coding\": fetchKimiPlanUsage,\n\tzai: fetchZaiPlanUsage,\n\txai: fetchXaiPlanUsage,\n};\n\nexport const PLAN_USAGE_CACHE_TTL_MS = 60 * 1000;\n\nexport type PlanUsageWindowPreference = \"5h\" | \"weekly\";\n\nexport function isWeeklyPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /week/i.test(window.label);\n}\n\nexport function isFiveHourPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /5h/i.test(window.label) || window.label === \"5h\";\n}\n\n/** Preferred window, falling back to the other, skipping Extra/on-demand. */\nexport function pickPlanWindow(\n\tusage: PlanUsage | undefined,\n\tpreferred: PlanUsageWindowPreference,\n): PlanUsageWindow | undefined {\n\tif (!usage?.windows.length) return undefined;\n\tconst usable = usage.windows.filter((w) => !/extra/i.test(w.label));\n\tconst weekly = usable.find(isWeeklyPlanWindow);\n\tconst fiveH = usable.find(isFiveHourPlanWindow);\n\tif (preferred === \"5h\") return fiveH ?? weekly ?? usable[0] ?? usage.windows[0];\n\treturn weekly ?? fiveH ?? usable[0] ?? usage.windows[0];\n}\n\nexport function footerPlanLabel(window: PlanUsageWindow): string {\n\tif (isFiveHourPlanWindow(window)) return \"5h\";\n\tif (isWeeklyPlanWindow(window)) return \"wk\";\n\treturn window.label.length <= 4 ? window.label : window.label.slice(0, 4);\n}\n\nconst cache = new Map<string, { expiresAt: number; value: PlanUsage[] }>();\nconst inflight = new Map<string, Promise<PlanUsageResult>>();\n\n/** Whether a plan-usage adapter exists for this provider. */\nexport function hasPlanUsageAdapter(providerId: string): boolean {\n\treturn providerId in ADAPTERS;\n}\n\n/** Plan usage for a provider, or undefined when unsupported/unavailable. Never throws. */\nexport async function getPlanUsage(providerId: string, runtime: ModelRuntime): Promise<PlanUsage | undefined> {\n\treturn (await getPlanUsageResult(providerId, runtime)).usage;\n}\n\n/** Like getPlanUsage, but surfaces auth failures instead of hiding them. */\nexport async function getPlanUsageResult(providerId: string, runtime: ModelRuntime): Promise<PlanUsageResult> {\n\tconst adapter = ADAPTERS[providerId];\n\tif (!adapter) return {};\n\tconst now = Date.now();\n\tconst cached = cache.get(providerId);\n\tif (cached && cached.expiresAt > now) return { usage: cached.value[0], usages: cached.value };\n\tconst pending = inflight.get(providerId);\n\tif (pending) return pending;\n\tconst task = (async (): Promise<PlanUsageResult> => {\n\t\ttry {\n\t\t\tconst value = await adapter(runtime);\n\t\t\tconst usages = value === undefined ? [] : Array.isArray(value) ? value : [value];\n\t\t\tcache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: usages });\n\t\t\treturn { usage: usages[0], usages };\n\t\t} catch (error) {\n\t\t\tconst authError = planUsageAuthError(error);\n\t\t\tif (authError) return { error: authError };\n\t\t\tcache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: [] });\n\t\t\treturn {};\n\t\t} finally {\n\t\t\tinflight.delete(providerId);\n\t\t}\n\t})();\n\tinflight.set(providerId, task);\n\treturn task;\n}\n\n/** Fetch every stored-credential plan plus the current provider, in parallel. */\nexport async function getAllPlanUsageResults(\n\tcurrentProvider: string | undefined,\n\truntime: ModelRuntime,\n): Promise<AllPlanUsageResult> {\n\tconst stored = new Set((await runtime.listCredentials()).map((credential) => credential.providerId));\n\tconst providers = Object.keys(ADAPTERS).filter(\n\t\t(providerId) => stored.has(providerId) || providerId === currentProvider,\n\t);\n\tconst results = await Promise.all(providers.map((providerId) => getPlanUsageResult(providerId, runtime)));\n\treturn {\n\t\tusages: results.flatMap((result) => result.usages ?? []),\n\t\terrors: results.flatMap((result) => (result.error ? [`${result.error}`] : [])),\n\t};\n}\n\n/** Last fetched usage, including expired cache (footer can show stale while refetching). */\nexport function peekPlanUsage(providerId: string): PlanUsage | undefined {\n\treturn cache.get(providerId)?.value[0];\n}\n\n/** Clear the adapter cache (tests, /login changes). */\nexport function clearPlanUsageCache(): void {\n\tcache.clear();\n\tinflight.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Extension bridge\n// ---------------------------------------------------------------------------\n\nexport const USAGE_SERVICE_BRIDGE_SYMBOL = Symbol.for(\"@lunr/usage-service\");\n\nexport interface FooterPlanSegment {\n\tlabel: string;\n\tusedPercent: number;\n}\n\nexport interface UsageServiceBridge {\n\tpeek(providerId: string): PlanUsage | undefined;\n\tprefetch(providerId: string): void;\n\tgetPreferredWindow(): PlanUsageWindowPreference;\n\tpickForFooter(providerId: string): FooterPlanSegment | undefined;\n\tsetOnUpdate(fn: (() => void) | undefined): void;\n}\n\nlet activeRuntime: ModelRuntime | undefined;\nlet activeUsageSettings: SettingsManager | undefined;\nlet onUsageUpdate: (() => void) | undefined;\n\nfunction preferredWindow(): PlanUsageWindowPreference {\n\treturn (runtimeScope.getStore()?.settingsManager ?? activeUsageSettings)?.getPlanUsageWindow() ?? \"weekly\";\n}\n\nconst usageBridge: UsageServiceBridge = {\n\tpeek(providerId: string): PlanUsage | undefined {\n\t\treturn peekPlanUsage(providerId);\n\t},\n\tprefetch(providerId: string): void {\n\t\tconst runtime = runtimeScope.getStore()?.modelRuntime ?? activeRuntime;\n\t\tif (!runtime || !hasPlanUsageAdapter(providerId)) return;\n\t\tvoid getPlanUsage(providerId, runtime).then(() => onUsageUpdate?.());\n\t},\n\tsetOnUpdate(fn: (() => void) | undefined): void {\n\t\tonUsageUpdate = fn;\n\t},\n\tgetPreferredWindow(): PlanUsageWindowPreference {\n\t\treturn preferredWindow();\n\t},\n\tpickForFooter(providerId: string): FooterPlanSegment | undefined {\n\t\tconst window = pickPlanWindow(peekPlanUsage(providerId), preferredWindow());\n\t\tif (!window) return undefined;\n\t\treturn { label: footerPlanLabel(window), usedPercent: window.usedPercent };\n\t},\n};\n\nexport function registerUsageServiceBridge(runtime: ModelRuntime, settingsManager: SettingsManager): void {\n\tactiveRuntime = runtime;\n\tactiveUsageSettings = settingsManager;\n\t(globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE_SYMBOL] = usageBridge;\n}\n\nexport function getUsageServiceBridge(): UsageServiceBridge | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE_SYMBOL] as UsageServiceBridge | undefined;\n}\n"]}
1
+ {"version":3,"file":"usage-service.d.ts","sourceRoot":"","sources":["../../src/core/usage-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plan/subscription usage service. Per-provider adapters query provider quota\n * endpoints (60-second cache each) so /usage and the footer can show subscription windows\n * alongside session/context totals.\n *\n * Deliberately absent adapters:\n * - anthropic: Anthropic policy prohibits subscription usage tracking in\n * third-party coding agents (2026-07-18 decision). OAuth Anthropic sessions\n * fall back to hiding the cost meter.\n * - ollama-cloud: no usage API exists.\n *\n * Adapter/network errors resolve to `undefined` (callers omit the plan UI).\n * Auth failures are different: they return `error` and are not cached as empty.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { AuthResult } from \"@ashx-j/lunr-ai\";\nimport type { ModelRuntime } from \"./model-runtime.ts\";\nimport { runtimeScope } from \"./runtime-scope.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { fetchKimiPlanUsage } from \"./usage-adapters/kimi-coding.ts\";\nimport { fetchCodexPlanUsage, resolveCodexHeaders } from \"./usage-adapters/openai-codex.ts\";\nimport { fetchXaiPlanUsage } from \"./usage-adapters/xai.ts\";\nimport { fetchZaiPlanUsage } from \"./usage-adapters/zai.ts\";\n\nexport interface PlanUsageWindow {\n\tlabel: string;\n\tusedPercent: number;\n\t/** Epoch ms when the window resets. */\n\tresetsAt?: number;\n}\n\nexport interface PlanUsage {\n\tprovider: string;\n\tplanLabel?: string;\n\twindows: PlanUsageWindow[];\n}\n\nexport interface PlanUsageResult {\n\t/** First plan, kept for footer and compatibility callers. */\n\tusage?: PlanUsage;\n\t/** Every plan returned by the provider. */\n\tusages?: PlanUsage[];\n\t/** Actionable auth failure. Not cached as an empty success. */\n\terror?: string;\n}\n\nexport interface AllPlanUsageResult {\n\tusages: PlanUsage[];\n\terrors: string[];\n}\n\nconst XAI_RELOGIN = \"xAI login expired. Run /login xai.\";\n\nfunction errorChain(error: unknown): string {\n\tconst parts: string[] = [];\n\tlet current: unknown = error;\n\tconst seen = new Set<unknown>();\n\twhile (current && typeof current === \"object\" && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tif (current instanceof Error) {\n\t\t\tparts.push(current.message);\n\t\t\tcurrent = current.cause;\n\t\t\tcontinue;\n\t\t}\n\t\tbreak;\n\t}\n\treturn parts.join(\" \");\n}\n\n/** Classify adapter/getAuth failures that mean the SuperGrok session is dead. */\nexport function planUsageAuthError(error: unknown): string | undefined {\n\tconst chain = errorChain(error);\n\tif (/timed out|aborted|cancelled|ECONN|ENOTFOUND|network/i.test(chain)) return undefined;\n\t// 403 on billing is often entitlement, not a dead refresh token.\n\tif (/xai billing rejected the session \\(HTTP 401\\)/i.test(chain)) return XAI_RELOGIN;\n\tif (/invalid_grant|refresh token revoked/i.test(chain)) return XAI_RELOGIN;\n\treturn undefined;\n}\n\nexport interface UsageAuthSnapshot {\n\tresolution?: AuthResult;\n\tcodexHeaders?: Record<string, string>;\n\toauth: boolean;\n}\n\ntype UsageAdapter = (runtime: ModelRuntime, auth: UsageAuthSnapshot) => Promise<PlanUsage | PlanUsage[] | undefined>;\n\nconst ADAPTERS: Readonly<Record<string, UsageAdapter>> = {\n\t\"openai-codex\": fetchCodexPlanUsage,\n\t\"kimi-coding\": fetchKimiPlanUsage,\n\tzai: fetchZaiPlanUsage,\n\txai: fetchXaiPlanUsage,\n};\n\nexport const PLAN_USAGE_CACHE_TTL_MS = 60 * 1000;\n\nexport type PlanUsageWindowPreference = \"5h\" | \"weekly\";\n\nexport function isWeeklyPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /week/i.test(window.label);\n}\n\nexport function isFiveHourPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /5h/i.test(window.label) || window.label === \"5h\";\n}\n\n/** Preferred window, falling back to the other, skipping Extra/on-demand. */\nexport function pickPlanWindow(\n\tusage: PlanUsage | undefined,\n\tpreferred: PlanUsageWindowPreference,\n): PlanUsageWindow | undefined {\n\tif (!usage?.windows.length) return undefined;\n\tconst usable = usage.windows.filter((w) => !/extra/i.test(w.label));\n\tconst weekly = usable.find(isWeeklyPlanWindow);\n\tconst fiveH = usable.find(isFiveHourPlanWindow);\n\tif (preferred === \"5h\") return fiveH ?? weekly ?? usable[0] ?? usage.windows[0];\n\treturn weekly ?? fiveH ?? usable[0] ?? usage.windows[0];\n}\n\nexport function footerPlanLabel(window: PlanUsageWindow): string {\n\tif (isFiveHourPlanWindow(window)) return \"5h\";\n\tif (isWeeklyPlanWindow(window)) return \"wk\";\n\treturn window.label.length <= 4 ? window.label : window.label.slice(0, 4);\n}\n\ninterface UsageState {\n\taccounts: Map<string, string>;\n\tauthReads: Map<string, { started: number; settled: number }>;\n\tcache: Map<string, { expiresAt: number; value: PlanUsage[] }>;\n\tinflight: Map<string, Promise<PlanUsageResult>>;\n}\nlet generation = 0;\nlet states = new WeakMap<ModelRuntime, UsageState>();\n\nfunction stateFor(runtime: ModelRuntime): UsageState {\n\tlet state = states.get(runtime);\n\tif (!state) {\n\t\tstate = { accounts: new Map(), authReads: new Map(), cache: new Map(), inflight: new Map() };\n\t\tstates.set(runtime, state);\n\t}\n\treturn state;\n}\n\n/** Whether a plan-usage adapter exists for this provider. */\nexport function hasPlanUsageAdapter(providerId: string): boolean {\n\treturn providerId in ADAPTERS;\n}\n\n/** Plan usage for a provider, or undefined when unsupported/unavailable. Never throws. */\nexport async function getPlanUsage(providerId: string, runtime: ModelRuntime): Promise<PlanUsage | undefined> {\n\treturn (await getPlanUsageResult(providerId, runtime)).usage;\n}\n\n/** Like getPlanUsage, but surfaces auth failures instead of hiding them. */\nexport async function getPlanUsageResult(providerId: string, runtime: ModelRuntime): Promise<PlanUsageResult> {\n\tconst adapter = ADAPTERS[providerId];\n\tif (!adapter) return {};\n\tconst state = stateFor(runtime);\n\tconst requestGeneration = generation;\n\tconst reads = state.authReads.get(providerId) ?? { started: 0, settled: 0 };\n\tstate.authReads.set(providerId, reads);\n\tconst authRead = ++reads.started;\n\tlet auth: UsageAuthSnapshot;\n\ttry {\n\t\tauth =\n\t\t\tproviderId === \"openai-codex\"\n\t\t\t\t? { codexHeaders: await resolveCodexHeaders(runtime), oauth: runtime.isUsingOAuth(providerId) }\n\t\t\t\t: { resolution: await runtime.getAuth(providerId), oauth: runtime.isUsingOAuth(providerId) };\n\t} catch (error) {\n\t\tif (reads.settled <= authRead) {\n\t\t\treads.settled = authRead;\n\t\t\tstate.accounts.delete(providerId);\n\t\t\tstate.cache.delete(providerId);\n\t\t\tstate.inflight.delete(providerId);\n\t\t}\n\t\treturn { error: planUsageAuthError(error) };\n\t}\n\tif (generation !== requestGeneration || states.get(runtime) !== state) return {};\n\t// Only a digest is retained. Neither credentials nor account identifiers are persisted.\n\tconst account = createHash(\"sha256\").update(JSON.stringify(auth)).digest(\"hex\");\n\tconst previous = state.accounts.get(providerId);\n\tif (reads.settled > authRead && previous !== account) return {};\n\treads.settled = Math.max(reads.settled, authRead);\n\tif (previous !== account) {\n\t\tstate.cache.delete(providerId);\n\t\tstate.inflight.delete(providerId);\n\t\tstate.accounts.set(providerId, account);\n\t}\n\tconst cached = state.cache.get(providerId);\n\tif (cached && cached.expiresAt > Date.now()) return { usage: cached.value[0], usages: cached.value };\n\tconst pending = state.inflight.get(providerId);\n\tif (pending) return pending;\n\tconst isCurrent = () =>\n\t\tgeneration === requestGeneration &&\n\t\tstates.get(runtime) === state &&\n\t\tstate.accounts.get(providerId) === account &&\n\t\tstate.inflight.get(providerId) === task;\n\tconst task = (async (): Promise<PlanUsageResult> => {\n\t\ttry {\n\t\t\tconst value = await adapter(runtime, auth);\n\t\t\tconst usages = value === undefined ? [] : Array.isArray(value) ? value : [value];\n\t\t\tif (isCurrent())\n\t\t\t\tstate.cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: usages });\n\t\t\treturn { usage: usages[0], usages };\n\t\t} catch (error) {\n\t\t\tconst authError = planUsageAuthError(error);\n\t\t\tif (authError) return { error: authError };\n\t\t\tif (isCurrent()) state.cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: [] });\n\t\t\treturn {};\n\t\t} finally {\n\t\t\tif (isCurrent()) state.inflight.delete(providerId);\n\t\t}\n\t})();\n\tstate.inflight.set(providerId, task);\n\treturn task;\n}\n\n/** Fetch every stored-credential plan plus the current provider, in parallel. */\nexport async function getAllPlanUsageResults(\n\tcurrentProvider: string | undefined,\n\truntime: ModelRuntime,\n): Promise<AllPlanUsageResult> {\n\tconst stored = new Set((await runtime.listCredentials()).map((credential) => credential.providerId));\n\tconst providers = Object.keys(ADAPTERS).filter(\n\t\t(providerId) => stored.has(providerId) || providerId === currentProvider,\n\t);\n\tconst results = await Promise.all(providers.map((providerId) => getPlanUsageResult(providerId, runtime)));\n\treturn {\n\t\tusages: results.flatMap((result) => result.usages ?? []),\n\t\terrors: results.flatMap((result) => (result.error ? [`${result.error}`] : [])),\n\t};\n}\n\n/** Last fetched usage, including expired cache (footer can show stale while refetching). */\nexport function peekPlanUsage(providerId: string, runtime: ModelRuntime): PlanUsage | undefined {\n\treturn states.get(runtime)?.cache.get(providerId)?.value[0];\n}\n\n/** Clear the adapter cache (tests, /login changes). */\nexport function clearPlanUsageCache(): void {\n\tgeneration++;\n\tstates = new WeakMap();\n}\n\n// ---------------------------------------------------------------------------\n// Extension bridge\n// ---------------------------------------------------------------------------\n\nexport const USAGE_SERVICE_BRIDGE_SYMBOL = Symbol.for(\"@lunr/usage-service\");\n\nexport interface FooterPlanSegment {\n\tlabel: string;\n\tusedPercent: number;\n}\n\nexport interface UsageServiceBridge {\n\tpeek(providerId: string): PlanUsage | undefined;\n\tprefetch(providerId: string): void;\n\tgetPreferredWindow(): PlanUsageWindowPreference;\n\tpickForFooter(providerId: string): FooterPlanSegment | undefined;\n\tsetOnUpdate(fn: (() => void) | undefined): void;\n}\n\nconst bridges = new WeakMap<ModelRuntime, UsageServiceBridge>();\n\n/** Register a bridge bound to its runtime; footer callbacks can retain it outside async scope. */\nexport function registerUsageServiceBridge(runtime: ModelRuntime, settingsManager: SettingsManager): void {\n\tlet onUpdate: (() => void) | undefined;\n\tconst bridge: UsageServiceBridge = {\n\t\tpeek: (providerId) => peekPlanUsage(providerId, runtime),\n\t\tprefetch(providerId) {\n\t\t\tif (!hasPlanUsageAdapter(providerId)) return;\n\t\t\tvoid getPlanUsage(providerId, runtime).then(() => onUpdate?.());\n\t\t},\n\t\tsetOnUpdate(fn) {\n\t\t\tonUpdate = fn;\n\t\t},\n\t\tgetPreferredWindow: () => settingsManager.getPlanUsageWindow(),\n\t\tpickForFooter(providerId) {\n\t\t\tconst window = pickPlanWindow(peekPlanUsage(providerId, runtime), settingsManager.getPlanUsageWindow());\n\t\t\treturn window ? { label: footerPlanLabel(window), usedPercent: window.usedPercent } : undefined;\n\t\t},\n\t};\n\tbridges.set(runtime, bridge);\n\t// Compatibility bridge requires an explicit async owner; it never falls back to another session.\n\t(globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE_SYMBOL] = {\n\t\tpeek: (providerId: string) => getUsageServiceBridge()?.peek(providerId),\n\t\tprefetch: (providerId: string) => getUsageServiceBridge()?.prefetch(providerId),\n\t\tpickForFooter: (providerId: string) => getUsageServiceBridge()?.pickForFooter(providerId),\n\t\tgetPreferredWindow: () => getUsageServiceBridge()?.getPreferredWindow() ?? \"weekly\",\n\t\tsetOnUpdate: (fn: (() => void) | undefined) => getUsageServiceBridge()?.setOnUpdate(fn),\n\t} satisfies UsageServiceBridge;\n}\n\nexport function getUsageServiceBridge(runtime = runtimeScope.getStore()?.modelRuntime): UsageServiceBridge | undefined {\n\treturn runtime ? bridges.get(runtime) : undefined;\n}\n"]}
@@ -12,9 +12,10 @@
12
12
  * Adapter/network errors resolve to `undefined` (callers omit the plan UI).
13
13
  * Auth failures are different: they return `error` and are not cached as empty.
14
14
  */
15
+ import { createHash } from "node:crypto";
15
16
  import { runtimeScope } from "./runtime-scope.js";
16
17
  import { fetchKimiPlanUsage } from "./usage-adapters/kimi-coding.js";
17
- import { fetchCodexPlanUsage } from "./usage-adapters/openai-codex.js";
18
+ import { fetchCodexPlanUsage, resolveCodexHeaders } from "./usage-adapters/openai-codex.js";
18
19
  import { fetchXaiPlanUsage } from "./usage-adapters/xai.js";
19
20
  import { fetchZaiPlanUsage } from "./usage-adapters/zai.js";
20
21
  const XAI_RELOGIN = "xAI login expired. Run /login xai.";
@@ -76,8 +77,16 @@ export function footerPlanLabel(window) {
76
77
  return "wk";
77
78
  return window.label.length <= 4 ? window.label : window.label.slice(0, 4);
78
79
  }
79
- const cache = new Map();
80
- const inflight = new Map();
80
+ let generation = 0;
81
+ let states = new WeakMap();
82
+ function stateFor(runtime) {
83
+ let state = states.get(runtime);
84
+ if (!state) {
85
+ state = { accounts: new Map(), authReads: new Map(), cache: new Map(), inflight: new Map() };
86
+ states.set(runtime, state);
87
+ }
88
+ return state;
89
+ }
81
90
  /** Whether a plan-usage adapter exists for this provider. */
82
91
  export function hasPlanUsageAdapter(providerId) {
83
92
  return providerId in ADAPTERS;
@@ -91,32 +100,72 @@ export async function getPlanUsageResult(providerId, runtime) {
91
100
  const adapter = ADAPTERS[providerId];
92
101
  if (!adapter)
93
102
  return {};
94
- const now = Date.now();
95
- const cached = cache.get(providerId);
96
- if (cached && cached.expiresAt > now)
103
+ const state = stateFor(runtime);
104
+ const requestGeneration = generation;
105
+ const reads = state.authReads.get(providerId) ?? { started: 0, settled: 0 };
106
+ state.authReads.set(providerId, reads);
107
+ const authRead = ++reads.started;
108
+ let auth;
109
+ try {
110
+ auth =
111
+ providerId === "openai-codex"
112
+ ? { codexHeaders: await resolveCodexHeaders(runtime), oauth: runtime.isUsingOAuth(providerId) }
113
+ : { resolution: await runtime.getAuth(providerId), oauth: runtime.isUsingOAuth(providerId) };
114
+ }
115
+ catch (error) {
116
+ if (reads.settled <= authRead) {
117
+ reads.settled = authRead;
118
+ state.accounts.delete(providerId);
119
+ state.cache.delete(providerId);
120
+ state.inflight.delete(providerId);
121
+ }
122
+ return { error: planUsageAuthError(error) };
123
+ }
124
+ if (generation !== requestGeneration || states.get(runtime) !== state)
125
+ return {};
126
+ // Only a digest is retained. Neither credentials nor account identifiers are persisted.
127
+ const account = createHash("sha256").update(JSON.stringify(auth)).digest("hex");
128
+ const previous = state.accounts.get(providerId);
129
+ if (reads.settled > authRead && previous !== account)
130
+ return {};
131
+ reads.settled = Math.max(reads.settled, authRead);
132
+ if (previous !== account) {
133
+ state.cache.delete(providerId);
134
+ state.inflight.delete(providerId);
135
+ state.accounts.set(providerId, account);
136
+ }
137
+ const cached = state.cache.get(providerId);
138
+ if (cached && cached.expiresAt > Date.now())
97
139
  return { usage: cached.value[0], usages: cached.value };
98
- const pending = inflight.get(providerId);
140
+ const pending = state.inflight.get(providerId);
99
141
  if (pending)
100
142
  return pending;
143
+ const isCurrent = () => generation === requestGeneration &&
144
+ states.get(runtime) === state &&
145
+ state.accounts.get(providerId) === account &&
146
+ state.inflight.get(providerId) === task;
101
147
  const task = (async () => {
102
148
  try {
103
- const value = await adapter(runtime);
149
+ const value = await adapter(runtime, auth);
104
150
  const usages = value === undefined ? [] : Array.isArray(value) ? value : [value];
105
- cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: usages });
151
+ if (isCurrent())
152
+ state.cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: usages });
106
153
  return { usage: usages[0], usages };
107
154
  }
108
155
  catch (error) {
109
156
  const authError = planUsageAuthError(error);
110
157
  if (authError)
111
158
  return { error: authError };
112
- cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: [] });
159
+ if (isCurrent())
160
+ state.cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: [] });
113
161
  return {};
114
162
  }
115
163
  finally {
116
- inflight.delete(providerId);
164
+ if (isCurrent())
165
+ state.inflight.delete(providerId);
117
166
  }
118
167
  })();
119
- inflight.set(providerId, task);
168
+ state.inflight.set(providerId, task);
120
169
  return task;
121
170
  }
122
171
  /** Fetch every stored-credential plan plus the current provider, in parallel. */
@@ -130,53 +179,49 @@ export async function getAllPlanUsageResults(currentProvider, runtime) {
130
179
  };
131
180
  }
132
181
  /** Last fetched usage, including expired cache (footer can show stale while refetching). */
133
- export function peekPlanUsage(providerId) {
134
- return cache.get(providerId)?.value[0];
182
+ export function peekPlanUsage(providerId, runtime) {
183
+ return states.get(runtime)?.cache.get(providerId)?.value[0];
135
184
  }
136
185
  /** Clear the adapter cache (tests, /login changes). */
137
186
  export function clearPlanUsageCache() {
138
- cache.clear();
139
- inflight.clear();
187
+ generation++;
188
+ states = new WeakMap();
140
189
  }
141
190
  // ---------------------------------------------------------------------------
142
191
  // Extension bridge
143
192
  // ---------------------------------------------------------------------------
144
193
  export const USAGE_SERVICE_BRIDGE_SYMBOL = Symbol.for("@lunr/usage-service");
145
- let activeRuntime;
146
- let activeUsageSettings;
147
- let onUsageUpdate;
148
- function preferredWindow() {
149
- return (runtimeScope.getStore()?.settingsManager ?? activeUsageSettings)?.getPlanUsageWindow() ?? "weekly";
150
- }
151
- const usageBridge = {
152
- peek(providerId) {
153
- return peekPlanUsage(providerId);
154
- },
155
- prefetch(providerId) {
156
- const runtime = runtimeScope.getStore()?.modelRuntime ?? activeRuntime;
157
- if (!runtime || !hasPlanUsageAdapter(providerId))
158
- return;
159
- void getPlanUsage(providerId, runtime).then(() => onUsageUpdate?.());
160
- },
161
- setOnUpdate(fn) {
162
- onUsageUpdate = fn;
163
- },
164
- getPreferredWindow() {
165
- return preferredWindow();
166
- },
167
- pickForFooter(providerId) {
168
- const window = pickPlanWindow(peekPlanUsage(providerId), preferredWindow());
169
- if (!window)
170
- return undefined;
171
- return { label: footerPlanLabel(window), usedPercent: window.usedPercent };
172
- },
173
- };
194
+ const bridges = new WeakMap();
195
+ /** Register a bridge bound to its runtime; footer callbacks can retain it outside async scope. */
174
196
  export function registerUsageServiceBridge(runtime, settingsManager) {
175
- activeRuntime = runtime;
176
- activeUsageSettings = settingsManager;
177
- globalThis[USAGE_SERVICE_BRIDGE_SYMBOL] = usageBridge;
197
+ let onUpdate;
198
+ const bridge = {
199
+ peek: (providerId) => peekPlanUsage(providerId, runtime),
200
+ prefetch(providerId) {
201
+ if (!hasPlanUsageAdapter(providerId))
202
+ return;
203
+ void getPlanUsage(providerId, runtime).then(() => onUpdate?.());
204
+ },
205
+ setOnUpdate(fn) {
206
+ onUpdate = fn;
207
+ },
208
+ getPreferredWindow: () => settingsManager.getPlanUsageWindow(),
209
+ pickForFooter(providerId) {
210
+ const window = pickPlanWindow(peekPlanUsage(providerId, runtime), settingsManager.getPlanUsageWindow());
211
+ return window ? { label: footerPlanLabel(window), usedPercent: window.usedPercent } : undefined;
212
+ },
213
+ };
214
+ bridges.set(runtime, bridge);
215
+ // Compatibility bridge requires an explicit async owner; it never falls back to another session.
216
+ globalThis[USAGE_SERVICE_BRIDGE_SYMBOL] = {
217
+ peek: (providerId) => getUsageServiceBridge()?.peek(providerId),
218
+ prefetch: (providerId) => getUsageServiceBridge()?.prefetch(providerId),
219
+ pickForFooter: (providerId) => getUsageServiceBridge()?.pickForFooter(providerId),
220
+ getPreferredWindow: () => getUsageServiceBridge()?.getPreferredWindow() ?? "weekly",
221
+ setOnUpdate: (fn) => getUsageServiceBridge()?.setOnUpdate(fn),
222
+ };
178
223
  }
179
- export function getUsageServiceBridge() {
180
- return globalThis[USAGE_SERVICE_BRIDGE_SYMBOL];
224
+ export function getUsageServiceBridge(runtime = runtimeScope.getStore()?.modelRuntime) {
225
+ return runtime ? bridges.get(runtime) : undefined;
181
226
  }
182
227
  //# sourceMappingURL=usage-service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usage-service.js","sourceRoot":"","sources":["../../src/core/usage-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plan/subscription usage service. Per-provider adapters query provider quota\n * endpoints (60-second cache each) so /usage and the footer can show subscription windows\n * alongside session/context totals.\n *\n * Deliberately absent adapters:\n * - anthropic: Anthropic policy prohibits subscription usage tracking in\n * third-party coding agents (2026-07-18 decision). OAuth Anthropic sessions\n * fall back to hiding the cost meter.\n * - ollama-cloud: no usage API exists.\n *\n * Adapter/network errors resolve to `undefined` (callers omit the plan UI).\n * Auth failures are different: they return `error` and are not cached as empty.\n */\n\nimport type { ModelRuntime } from \"./model-runtime.ts\";\nimport { runtimeScope } from \"./runtime-scope.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { fetchKimiPlanUsage } from \"./usage-adapters/kimi-coding.ts\";\nimport { fetchCodexPlanUsage } from \"./usage-adapters/openai-codex.ts\";\nimport { fetchXaiPlanUsage } from \"./usage-adapters/xai.ts\";\nimport { fetchZaiPlanUsage } from \"./usage-adapters/zai.ts\";\n\nexport interface PlanUsageWindow {\n\tlabel: string;\n\tusedPercent: number;\n\t/** Epoch ms when the window resets. */\n\tresetsAt?: number;\n}\n\nexport interface PlanUsage {\n\tprovider: string;\n\tplanLabel?: string;\n\twindows: PlanUsageWindow[];\n}\n\nexport interface PlanUsageResult {\n\t/** First plan, kept for footer and compatibility callers. */\n\tusage?: PlanUsage;\n\t/** Every plan returned by the provider. */\n\tusages?: PlanUsage[];\n\t/** Actionable auth failure. Not cached as an empty success. */\n\terror?: string;\n}\n\nexport interface AllPlanUsageResult {\n\tusages: PlanUsage[];\n\terrors: string[];\n}\n\nconst XAI_RELOGIN = \"xAI login expired. Run /login xai.\";\n\nfunction errorChain(error: unknown): string {\n\tconst parts: string[] = [];\n\tlet current: unknown = error;\n\tconst seen = new Set<unknown>();\n\twhile (current && typeof current === \"object\" && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tif (current instanceof Error) {\n\t\t\tparts.push(current.message);\n\t\t\tcurrent = current.cause;\n\t\t\tcontinue;\n\t\t}\n\t\tbreak;\n\t}\n\treturn parts.join(\" \");\n}\n\n/** Classify adapter/getAuth failures that mean the SuperGrok session is dead. */\nexport function planUsageAuthError(error: unknown): string | undefined {\n\tconst chain = errorChain(error);\n\tif (/timed out|aborted|cancelled|ECONN|ENOTFOUND|network/i.test(chain)) return undefined;\n\t// 403 on billing is often entitlement, not a dead refresh token.\n\tif (/xai billing rejected the session \\(HTTP 401\\)/i.test(chain)) return XAI_RELOGIN;\n\tif (/invalid_grant|refresh token revoked/i.test(chain)) return XAI_RELOGIN;\n\treturn undefined;\n}\n\ntype UsageAdapter = (runtime: ModelRuntime) => Promise<PlanUsage | PlanUsage[] | undefined>;\n\nconst ADAPTERS: Readonly<Record<string, UsageAdapter>> = {\n\t\"openai-codex\": fetchCodexPlanUsage,\n\t\"kimi-coding\": fetchKimiPlanUsage,\n\tzai: fetchZaiPlanUsage,\n\txai: fetchXaiPlanUsage,\n};\n\nexport const PLAN_USAGE_CACHE_TTL_MS = 60 * 1000;\n\nexport type PlanUsageWindowPreference = \"5h\" | \"weekly\";\n\nexport function isWeeklyPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /week/i.test(window.label);\n}\n\nexport function isFiveHourPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /5h/i.test(window.label) || window.label === \"5h\";\n}\n\n/** Preferred window, falling back to the other, skipping Extra/on-demand. */\nexport function pickPlanWindow(\n\tusage: PlanUsage | undefined,\n\tpreferred: PlanUsageWindowPreference,\n): PlanUsageWindow | undefined {\n\tif (!usage?.windows.length) return undefined;\n\tconst usable = usage.windows.filter((w) => !/extra/i.test(w.label));\n\tconst weekly = usable.find(isWeeklyPlanWindow);\n\tconst fiveH = usable.find(isFiveHourPlanWindow);\n\tif (preferred === \"5h\") return fiveH ?? weekly ?? usable[0] ?? usage.windows[0];\n\treturn weekly ?? fiveH ?? usable[0] ?? usage.windows[0];\n}\n\nexport function footerPlanLabel(window: PlanUsageWindow): string {\n\tif (isFiveHourPlanWindow(window)) return \"5h\";\n\tif (isWeeklyPlanWindow(window)) return \"wk\";\n\treturn window.label.length <= 4 ? window.label : window.label.slice(0, 4);\n}\n\nconst cache = new Map<string, { expiresAt: number; value: PlanUsage[] }>();\nconst inflight = new Map<string, Promise<PlanUsageResult>>();\n\n/** Whether a plan-usage adapter exists for this provider. */\nexport function hasPlanUsageAdapter(providerId: string): boolean {\n\treturn providerId in ADAPTERS;\n}\n\n/** Plan usage for a provider, or undefined when unsupported/unavailable. Never throws. */\nexport async function getPlanUsage(providerId: string, runtime: ModelRuntime): Promise<PlanUsage | undefined> {\n\treturn (await getPlanUsageResult(providerId, runtime)).usage;\n}\n\n/** Like getPlanUsage, but surfaces auth failures instead of hiding them. */\nexport async function getPlanUsageResult(providerId: string, runtime: ModelRuntime): Promise<PlanUsageResult> {\n\tconst adapter = ADAPTERS[providerId];\n\tif (!adapter) return {};\n\tconst now = Date.now();\n\tconst cached = cache.get(providerId);\n\tif (cached && cached.expiresAt > now) return { usage: cached.value[0], usages: cached.value };\n\tconst pending = inflight.get(providerId);\n\tif (pending) return pending;\n\tconst task = (async (): Promise<PlanUsageResult> => {\n\t\ttry {\n\t\t\tconst value = await adapter(runtime);\n\t\t\tconst usages = value === undefined ? [] : Array.isArray(value) ? value : [value];\n\t\t\tcache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: usages });\n\t\t\treturn { usage: usages[0], usages };\n\t\t} catch (error) {\n\t\t\tconst authError = planUsageAuthError(error);\n\t\t\tif (authError) return { error: authError };\n\t\t\tcache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: [] });\n\t\t\treturn {};\n\t\t} finally {\n\t\t\tinflight.delete(providerId);\n\t\t}\n\t})();\n\tinflight.set(providerId, task);\n\treturn task;\n}\n\n/** Fetch every stored-credential plan plus the current provider, in parallel. */\nexport async function getAllPlanUsageResults(\n\tcurrentProvider: string | undefined,\n\truntime: ModelRuntime,\n): Promise<AllPlanUsageResult> {\n\tconst stored = new Set((await runtime.listCredentials()).map((credential) => credential.providerId));\n\tconst providers = Object.keys(ADAPTERS).filter(\n\t\t(providerId) => stored.has(providerId) || providerId === currentProvider,\n\t);\n\tconst results = await Promise.all(providers.map((providerId) => getPlanUsageResult(providerId, runtime)));\n\treturn {\n\t\tusages: results.flatMap((result) => result.usages ?? []),\n\t\terrors: results.flatMap((result) => (result.error ? [`${result.error}`] : [])),\n\t};\n}\n\n/** Last fetched usage, including expired cache (footer can show stale while refetching). */\nexport function peekPlanUsage(providerId: string): PlanUsage | undefined {\n\treturn cache.get(providerId)?.value[0];\n}\n\n/** Clear the adapter cache (tests, /login changes). */\nexport function clearPlanUsageCache(): void {\n\tcache.clear();\n\tinflight.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Extension bridge\n// ---------------------------------------------------------------------------\n\nexport const USAGE_SERVICE_BRIDGE_SYMBOL = Symbol.for(\"@lunr/usage-service\");\n\nexport interface FooterPlanSegment {\n\tlabel: string;\n\tusedPercent: number;\n}\n\nexport interface UsageServiceBridge {\n\tpeek(providerId: string): PlanUsage | undefined;\n\tprefetch(providerId: string): void;\n\tgetPreferredWindow(): PlanUsageWindowPreference;\n\tpickForFooter(providerId: string): FooterPlanSegment | undefined;\n\tsetOnUpdate(fn: (() => void) | undefined): void;\n}\n\nlet activeRuntime: ModelRuntime | undefined;\nlet activeUsageSettings: SettingsManager | undefined;\nlet onUsageUpdate: (() => void) | undefined;\n\nfunction preferredWindow(): PlanUsageWindowPreference {\n\treturn (runtimeScope.getStore()?.settingsManager ?? activeUsageSettings)?.getPlanUsageWindow() ?? \"weekly\";\n}\n\nconst usageBridge: UsageServiceBridge = {\n\tpeek(providerId: string): PlanUsage | undefined {\n\t\treturn peekPlanUsage(providerId);\n\t},\n\tprefetch(providerId: string): void {\n\t\tconst runtime = runtimeScope.getStore()?.modelRuntime ?? activeRuntime;\n\t\tif (!runtime || !hasPlanUsageAdapter(providerId)) return;\n\t\tvoid getPlanUsage(providerId, runtime).then(() => onUsageUpdate?.());\n\t},\n\tsetOnUpdate(fn: (() => void) | undefined): void {\n\t\tonUsageUpdate = fn;\n\t},\n\tgetPreferredWindow(): PlanUsageWindowPreference {\n\t\treturn preferredWindow();\n\t},\n\tpickForFooter(providerId: string): FooterPlanSegment | undefined {\n\t\tconst window = pickPlanWindow(peekPlanUsage(providerId), preferredWindow());\n\t\tif (!window) return undefined;\n\t\treturn { label: footerPlanLabel(window), usedPercent: window.usedPercent };\n\t},\n};\n\nexport function registerUsageServiceBridge(runtime: ModelRuntime, settingsManager: SettingsManager): void {\n\tactiveRuntime = runtime;\n\tactiveUsageSettings = settingsManager;\n\t(globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE_SYMBOL] = usageBridge;\n}\n\nexport function getUsageServiceBridge(): UsageServiceBridge | undefined {\n\treturn (globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE_SYMBOL] as UsageServiceBridge | undefined;\n}\n"]}
1
+ {"version":3,"file":"usage-service.js","sourceRoot":"","sources":["../../src/core/usage-service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plan/subscription usage service. Per-provider adapters query provider quota\n * endpoints (60-second cache each) so /usage and the footer can show subscription windows\n * alongside session/context totals.\n *\n * Deliberately absent adapters:\n * - anthropic: Anthropic policy prohibits subscription usage tracking in\n * third-party coding agents (2026-07-18 decision). OAuth Anthropic sessions\n * fall back to hiding the cost meter.\n * - ollama-cloud: no usage API exists.\n *\n * Adapter/network errors resolve to `undefined` (callers omit the plan UI).\n * Auth failures are different: they return `error` and are not cached as empty.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { AuthResult } from \"@ashx-j/lunr-ai\";\nimport type { ModelRuntime } from \"./model-runtime.ts\";\nimport { runtimeScope } from \"./runtime-scope.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport { fetchKimiPlanUsage } from \"./usage-adapters/kimi-coding.ts\";\nimport { fetchCodexPlanUsage, resolveCodexHeaders } from \"./usage-adapters/openai-codex.ts\";\nimport { fetchXaiPlanUsage } from \"./usage-adapters/xai.ts\";\nimport { fetchZaiPlanUsage } from \"./usage-adapters/zai.ts\";\n\nexport interface PlanUsageWindow {\n\tlabel: string;\n\tusedPercent: number;\n\t/** Epoch ms when the window resets. */\n\tresetsAt?: number;\n}\n\nexport interface PlanUsage {\n\tprovider: string;\n\tplanLabel?: string;\n\twindows: PlanUsageWindow[];\n}\n\nexport interface PlanUsageResult {\n\t/** First plan, kept for footer and compatibility callers. */\n\tusage?: PlanUsage;\n\t/** Every plan returned by the provider. */\n\tusages?: PlanUsage[];\n\t/** Actionable auth failure. Not cached as an empty success. */\n\terror?: string;\n}\n\nexport interface AllPlanUsageResult {\n\tusages: PlanUsage[];\n\terrors: string[];\n}\n\nconst XAI_RELOGIN = \"xAI login expired. Run /login xai.\";\n\nfunction errorChain(error: unknown): string {\n\tconst parts: string[] = [];\n\tlet current: unknown = error;\n\tconst seen = new Set<unknown>();\n\twhile (current && typeof current === \"object\" && !seen.has(current)) {\n\t\tseen.add(current);\n\t\tif (current instanceof Error) {\n\t\t\tparts.push(current.message);\n\t\t\tcurrent = current.cause;\n\t\t\tcontinue;\n\t\t}\n\t\tbreak;\n\t}\n\treturn parts.join(\" \");\n}\n\n/** Classify adapter/getAuth failures that mean the SuperGrok session is dead. */\nexport function planUsageAuthError(error: unknown): string | undefined {\n\tconst chain = errorChain(error);\n\tif (/timed out|aborted|cancelled|ECONN|ENOTFOUND|network/i.test(chain)) return undefined;\n\t// 403 on billing is often entitlement, not a dead refresh token.\n\tif (/xai billing rejected the session \\(HTTP 401\\)/i.test(chain)) return XAI_RELOGIN;\n\tif (/invalid_grant|refresh token revoked/i.test(chain)) return XAI_RELOGIN;\n\treturn undefined;\n}\n\nexport interface UsageAuthSnapshot {\n\tresolution?: AuthResult;\n\tcodexHeaders?: Record<string, string>;\n\toauth: boolean;\n}\n\ntype UsageAdapter = (runtime: ModelRuntime, auth: UsageAuthSnapshot) => Promise<PlanUsage | PlanUsage[] | undefined>;\n\nconst ADAPTERS: Readonly<Record<string, UsageAdapter>> = {\n\t\"openai-codex\": fetchCodexPlanUsage,\n\t\"kimi-coding\": fetchKimiPlanUsage,\n\tzai: fetchZaiPlanUsage,\n\txai: fetchXaiPlanUsage,\n};\n\nexport const PLAN_USAGE_CACHE_TTL_MS = 60 * 1000;\n\nexport type PlanUsageWindowPreference = \"5h\" | \"weekly\";\n\nexport function isWeeklyPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /week/i.test(window.label);\n}\n\nexport function isFiveHourPlanWindow(window: PlanUsageWindow): boolean {\n\treturn /5h/i.test(window.label) || window.label === \"5h\";\n}\n\n/** Preferred window, falling back to the other, skipping Extra/on-demand. */\nexport function pickPlanWindow(\n\tusage: PlanUsage | undefined,\n\tpreferred: PlanUsageWindowPreference,\n): PlanUsageWindow | undefined {\n\tif (!usage?.windows.length) return undefined;\n\tconst usable = usage.windows.filter((w) => !/extra/i.test(w.label));\n\tconst weekly = usable.find(isWeeklyPlanWindow);\n\tconst fiveH = usable.find(isFiveHourPlanWindow);\n\tif (preferred === \"5h\") return fiveH ?? weekly ?? usable[0] ?? usage.windows[0];\n\treturn weekly ?? fiveH ?? usable[0] ?? usage.windows[0];\n}\n\nexport function footerPlanLabel(window: PlanUsageWindow): string {\n\tif (isFiveHourPlanWindow(window)) return \"5h\";\n\tif (isWeeklyPlanWindow(window)) return \"wk\";\n\treturn window.label.length <= 4 ? window.label : window.label.slice(0, 4);\n}\n\ninterface UsageState {\n\taccounts: Map<string, string>;\n\tauthReads: Map<string, { started: number; settled: number }>;\n\tcache: Map<string, { expiresAt: number; value: PlanUsage[] }>;\n\tinflight: Map<string, Promise<PlanUsageResult>>;\n}\nlet generation = 0;\nlet states = new WeakMap<ModelRuntime, UsageState>();\n\nfunction stateFor(runtime: ModelRuntime): UsageState {\n\tlet state = states.get(runtime);\n\tif (!state) {\n\t\tstate = { accounts: new Map(), authReads: new Map(), cache: new Map(), inflight: new Map() };\n\t\tstates.set(runtime, state);\n\t}\n\treturn state;\n}\n\n/** Whether a plan-usage adapter exists for this provider. */\nexport function hasPlanUsageAdapter(providerId: string): boolean {\n\treturn providerId in ADAPTERS;\n}\n\n/** Plan usage for a provider, or undefined when unsupported/unavailable. Never throws. */\nexport async function getPlanUsage(providerId: string, runtime: ModelRuntime): Promise<PlanUsage | undefined> {\n\treturn (await getPlanUsageResult(providerId, runtime)).usage;\n}\n\n/** Like getPlanUsage, but surfaces auth failures instead of hiding them. */\nexport async function getPlanUsageResult(providerId: string, runtime: ModelRuntime): Promise<PlanUsageResult> {\n\tconst adapter = ADAPTERS[providerId];\n\tif (!adapter) return {};\n\tconst state = stateFor(runtime);\n\tconst requestGeneration = generation;\n\tconst reads = state.authReads.get(providerId) ?? { started: 0, settled: 0 };\n\tstate.authReads.set(providerId, reads);\n\tconst authRead = ++reads.started;\n\tlet auth: UsageAuthSnapshot;\n\ttry {\n\t\tauth =\n\t\t\tproviderId === \"openai-codex\"\n\t\t\t\t? { codexHeaders: await resolveCodexHeaders(runtime), oauth: runtime.isUsingOAuth(providerId) }\n\t\t\t\t: { resolution: await runtime.getAuth(providerId), oauth: runtime.isUsingOAuth(providerId) };\n\t} catch (error) {\n\t\tif (reads.settled <= authRead) {\n\t\t\treads.settled = authRead;\n\t\t\tstate.accounts.delete(providerId);\n\t\t\tstate.cache.delete(providerId);\n\t\t\tstate.inflight.delete(providerId);\n\t\t}\n\t\treturn { error: planUsageAuthError(error) };\n\t}\n\tif (generation !== requestGeneration || states.get(runtime) !== state) return {};\n\t// Only a digest is retained. Neither credentials nor account identifiers are persisted.\n\tconst account = createHash(\"sha256\").update(JSON.stringify(auth)).digest(\"hex\");\n\tconst previous = state.accounts.get(providerId);\n\tif (reads.settled > authRead && previous !== account) return {};\n\treads.settled = Math.max(reads.settled, authRead);\n\tif (previous !== account) {\n\t\tstate.cache.delete(providerId);\n\t\tstate.inflight.delete(providerId);\n\t\tstate.accounts.set(providerId, account);\n\t}\n\tconst cached = state.cache.get(providerId);\n\tif (cached && cached.expiresAt > Date.now()) return { usage: cached.value[0], usages: cached.value };\n\tconst pending = state.inflight.get(providerId);\n\tif (pending) return pending;\n\tconst isCurrent = () =>\n\t\tgeneration === requestGeneration &&\n\t\tstates.get(runtime) === state &&\n\t\tstate.accounts.get(providerId) === account &&\n\t\tstate.inflight.get(providerId) === task;\n\tconst task = (async (): Promise<PlanUsageResult> => {\n\t\ttry {\n\t\t\tconst value = await adapter(runtime, auth);\n\t\t\tconst usages = value === undefined ? [] : Array.isArray(value) ? value : [value];\n\t\t\tif (isCurrent())\n\t\t\t\tstate.cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: usages });\n\t\t\treturn { usage: usages[0], usages };\n\t\t} catch (error) {\n\t\t\tconst authError = planUsageAuthError(error);\n\t\t\tif (authError) return { error: authError };\n\t\t\tif (isCurrent()) state.cache.set(providerId, { expiresAt: Date.now() + PLAN_USAGE_CACHE_TTL_MS, value: [] });\n\t\t\treturn {};\n\t\t} finally {\n\t\t\tif (isCurrent()) state.inflight.delete(providerId);\n\t\t}\n\t})();\n\tstate.inflight.set(providerId, task);\n\treturn task;\n}\n\n/** Fetch every stored-credential plan plus the current provider, in parallel. */\nexport async function getAllPlanUsageResults(\n\tcurrentProvider: string | undefined,\n\truntime: ModelRuntime,\n): Promise<AllPlanUsageResult> {\n\tconst stored = new Set((await runtime.listCredentials()).map((credential) => credential.providerId));\n\tconst providers = Object.keys(ADAPTERS).filter(\n\t\t(providerId) => stored.has(providerId) || providerId === currentProvider,\n\t);\n\tconst results = await Promise.all(providers.map((providerId) => getPlanUsageResult(providerId, runtime)));\n\treturn {\n\t\tusages: results.flatMap((result) => result.usages ?? []),\n\t\terrors: results.flatMap((result) => (result.error ? [`${result.error}`] : [])),\n\t};\n}\n\n/** Last fetched usage, including expired cache (footer can show stale while refetching). */\nexport function peekPlanUsage(providerId: string, runtime: ModelRuntime): PlanUsage | undefined {\n\treturn states.get(runtime)?.cache.get(providerId)?.value[0];\n}\n\n/** Clear the adapter cache (tests, /login changes). */\nexport function clearPlanUsageCache(): void {\n\tgeneration++;\n\tstates = new WeakMap();\n}\n\n// ---------------------------------------------------------------------------\n// Extension bridge\n// ---------------------------------------------------------------------------\n\nexport const USAGE_SERVICE_BRIDGE_SYMBOL = Symbol.for(\"@lunr/usage-service\");\n\nexport interface FooterPlanSegment {\n\tlabel: string;\n\tusedPercent: number;\n}\n\nexport interface UsageServiceBridge {\n\tpeek(providerId: string): PlanUsage | undefined;\n\tprefetch(providerId: string): void;\n\tgetPreferredWindow(): PlanUsageWindowPreference;\n\tpickForFooter(providerId: string): FooterPlanSegment | undefined;\n\tsetOnUpdate(fn: (() => void) | undefined): void;\n}\n\nconst bridges = new WeakMap<ModelRuntime, UsageServiceBridge>();\n\n/** Register a bridge bound to its runtime; footer callbacks can retain it outside async scope. */\nexport function registerUsageServiceBridge(runtime: ModelRuntime, settingsManager: SettingsManager): void {\n\tlet onUpdate: (() => void) | undefined;\n\tconst bridge: UsageServiceBridge = {\n\t\tpeek: (providerId) => peekPlanUsage(providerId, runtime),\n\t\tprefetch(providerId) {\n\t\t\tif (!hasPlanUsageAdapter(providerId)) return;\n\t\t\tvoid getPlanUsage(providerId, runtime).then(() => onUpdate?.());\n\t\t},\n\t\tsetOnUpdate(fn) {\n\t\t\tonUpdate = fn;\n\t\t},\n\t\tgetPreferredWindow: () => settingsManager.getPlanUsageWindow(),\n\t\tpickForFooter(providerId) {\n\t\t\tconst window = pickPlanWindow(peekPlanUsage(providerId, runtime), settingsManager.getPlanUsageWindow());\n\t\t\treturn window ? { label: footerPlanLabel(window), usedPercent: window.usedPercent } : undefined;\n\t\t},\n\t};\n\tbridges.set(runtime, bridge);\n\t// Compatibility bridge requires an explicit async owner; it never falls back to another session.\n\t(globalThis as Record<symbol, unknown>)[USAGE_SERVICE_BRIDGE_SYMBOL] = {\n\t\tpeek: (providerId: string) => getUsageServiceBridge()?.peek(providerId),\n\t\tprefetch: (providerId: string) => getUsageServiceBridge()?.prefetch(providerId),\n\t\tpickForFooter: (providerId: string) => getUsageServiceBridge()?.pickForFooter(providerId),\n\t\tgetPreferredWindow: () => getUsageServiceBridge()?.getPreferredWindow() ?? \"weekly\",\n\t\tsetOnUpdate: (fn: (() => void) | undefined) => getUsageServiceBridge()?.setOnUpdate(fn),\n\t} satisfies UsageServiceBridge;\n}\n\nexport function getUsageServiceBridge(runtime = runtimeScope.getStore()?.modelRuntime): UsageServiceBridge | undefined {\n\treturn runtime ? bridges.get(runtime) : undefined;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../../src/gateway/adapters/discord.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway Discord adapter (Phase 3).\n *\n * Built on discord.js v14 (gateway websocket; the library owns reconnects and\n * REST rate-limit queuing internally, so neither is handled here). No voice,\n * no native slash-command registration — commands arrive as plain message\n * text, same as Telegram.\n *\n * Intents: Guilds, GuildMessages, DirectMessages, MessageContent. GuildMembers\n * is deliberately NOT requested: it is a privileged intent, and asking for it\n * without the dev-portal opt-in makes Discord refuse the identify — the bot\n * connects and immediately goes offline. v1 has no role-based config\n * (SessionSource.roleAuthorized stays undefined), so there is nothing to gain.\n *\n * Inbound: messageToEvent() is the pure message → MessageEvent mapping\n * (dm/group/thread chat types, <@id>/<@!id> mention detection with the mention\n * stripped, thread-participation counts as a mention, own + all-bot + system +\n * attachment-only messages dropped, other-mention-without-us dropped outside\n * DMs — hermes DISCORD_IGNORE_NO_MENTION). A module-level seen-id Set\n * (cap 500, FIFO evict) drops gateway-resume replays. cfg.ignoredChannels is\n * enforced adapter-side (channel id or parent id; blacklist wins over\n * mentions). Consecutive texts from the same chat/thread/user are debounced\n * 600ms and merged (\"\\n\"-joined, latest messageId kept); texts starting with\n * \"/\" flush immediately so commands never merge.\n * Button interactions are routed via interactionCreate → buttonInteractionToEvent().\n *\n * chatId/threadId mapping (pairs with session-keys.ts buildSessionKey):\n * dm → chatId = channel.id → agent:main:discord:dm:<channelId>\n * thread → chatId = PARENT channel id, threadId = → agent:main:discord:thread:<parentId>:<threadId>\n * thread channel id (shared session, no userId)\n * guild text/announcement → chatId = channel.id → agent:main:discord:group:<channelId>[:<userId>]\n * (chatType \"group\"; userId appended when groupSessionsPerUser is on)\n *\n * autoThread (cfg.discord.autoThread, default true): a bot-mentioning message\n * in a guild TEXT channel gets a thread spun off it (startThread, 60min\n * archive); the event is re-targeted to the thread (chatType \"thread\", chatId\n * parent id, threadId thread id). On failure the event stays in-channel.\n *\n * Thread participation: an in-memory Set of thread ids the bot has sent to\n * (recorded on every successful send() into a thread and on autoThread\n * creation); messages in those threads count as mentionedBot so follow-ups\n * don't need a re-mention. NOT persisted in v1 (hermes persists it).\n *\n * Outbound: channel.send({ content, reply: { messageReference } }) with a\n * one-shot plain-send fallback when the reply reference fails. DiscordAPIError\n * 50035 (invalid form body, e.g. overlong content) is reported non-retryable;\n * 429s are queued internally by discord.js; everything else is retryable.\n * editMessage resolves the channel via a messageId → channel map recorded at\n * send() time (router passes only chatId, but thread messages live in their\n * own channel); Unknown Message (10008) is a terminal { success: false }.\n * Message splitting is NOT done here — the router owns that (text.ts).\n *\n * Buttons: ActionRowBuilder rows of ButtonBuilder components (max 5×5).\n * answerCallback acknowledges the interaction; a text argument produces an\n * ephemeral reply (used for unauthorized/expired taps), otherwise deferUpdate().\n *\n * Typing: sendTyping() is a self-contained refresher — Discord typing bubbles\n * last ~10s, so the action is re-sent on an unref'd 8s interval per target.\n * The interval stops when send() completes for that target or after 2 minutes\n * without a refresh (safety net). All errors swallowed.\n *\n * Testability: the discord.js Client goes through an injectable clientFactory\n * (tests inject a minimal fake), timers through an injectable Scheduler, and\n * messageToEvent() operates on the structural DiscordMessageLike — plain mock\n * objects, no real Client construction.\n */\n\nimport {\n\tActionRowBuilder,\n\tApplicationCommandOptionType,\n\tButtonBuilder,\n\tButtonStyle,\n\tChannelType,\n\tClient,\n\tEvents,\n\tGatewayIntentBits,\n\ttype Interaction,\n} from \"discord.js\";\nimport { isAuthorized, isGatewayOwner } from \"../authz.ts\";\nimport { type DiscordConfig, loadGatewayConfig } from \"../config.ts\";\nimport { downloadAttachment } from \"../download.ts\";\nimport { createPairingStore } from \"../pairing.ts\";\nimport type {\n\tButtonSpec,\n\tCallbackEvent,\n\tChatType,\n\tInboundAttachment,\n\tMessageEvent,\n\tPlatformAdapter,\n\tSendOptions,\n\tSendResult,\n} from \"../types.ts\";\nimport type { Scheduler } from \"./telegram.ts\";\n\nconst DEFAULT_DEBOUNCE_MS = 600;\nconst TYPING_INTERVAL_MS = 8000; // Discord typing lasts ~10s\nconst TYPING_SAFETY_MS = 120_000; // auto-stop after 2min without a refresh\nconst REPLY_TO_TEXT_MAX = 500;\nconst THREAD_NAME_MAX = 40;\nconst SEEN_CAP = 500;\nconst SENT_CHANNEL_CAP = 500;\nconst CALLBACK_INTERACTION_CAP = 500;\nconst CHANNEL_CACHE_CAP = 500;\nconst PARTICIPATING_THREAD_CAP = 500;\nconst ERR_INVALID_FORM_BODY = 50035; // e.g. content over 2000 chars — non-retryable\nconst ERR_UNKNOWN_MESSAGE = 10008; // deleted/unknown message — terminal\n\nconst THREAD_TYPES: readonly number[] = [\n\tChannelType.PublicThread,\n\tChannelType.PrivateThread,\n\tChannelType.AnnouncementThread,\n];\n\n// ---------------------------------------------------------------------------\n// Structural discord.js shapes (only the fields the adapter reads)\n// ---------------------------------------------------------------------------\n\nexport interface DiscordSentMessageLike {\n\tid: string;\n}\n\nexport interface DiscordFetchedMessageLike {\n\tcontent?: string;\n\tedit?(options: string | { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordChannelLike {\n\tid: string;\n\ttype: number;\n\tparentId?: string | null;\n\tsend?(payload: {\n\t\tcontent: string;\n\t\tfiles?: string[];\n\t\treply?: { messageReference: string };\n\t\tcomponents?: unknown[];\n\t}): Promise<DiscordSentMessageLike>;\n\tsendTyping?(): Promise<unknown>;\n\tmessages?: {\n\t\tcache?: { get(id: string): { content?: string } | undefined };\n\t\tfetch(id: string): Promise<DiscordFetchedMessageLike | null>;\n\t};\n}\n\nexport interface DiscordButtonInteractionLike {\n\tid: string;\n\tcustomId: string;\n\tuser: { id: string; username?: string };\n\tmessage: { id: string; channel: DiscordChannelLike };\n\tisButton(): boolean;\n\tdeferUpdate(): Promise<unknown>;\n\treply?(options: { content: string; ephemeral: boolean }): Promise<unknown>;\n\tupdate?(options: { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordAttachmentLike {\n\tid: string;\n\t/** Direct CDN URL for the attachment content. */\n\turl: string;\n\tcontentType?: string;\n\tname?: string;\n\tsize?: number;\n}\n\nexport interface DiscordMessageLike {\n\tid: string;\n\tcontent?: string;\n\tsystem?: boolean;\n\tauthor: { id: string; bot?: boolean; username?: string };\n\tchannel: DiscordChannelLike;\n\treference?: { messageId?: string | null } | null;\n\tmentions?: {\n\t\thas(user: { id: string }): boolean;\n\t\trepliedUser?: { id: string } | null;\n\t};\n\tstartThread?(opts: { name: string; autoArchiveDuration: number }): Promise<DiscordChannelLike>;\n\tattachments?: DiscordAttachmentLike[];\n}\n\n/** Minimal Client surface; the real discord.js Client satisfies this. */\nexport interface DiscordClientLike {\n\tapplication?: Pick<NonNullable<Client[\"application\"]>, \"commands\"> | null;\n\tuser: { id: string } | null;\n\tlogin(token: string): Promise<unknown>;\n\ton(event: string, listener: (arg: unknown) => void): unknown;\n\tonce(event: string, listener: () => void): unknown;\n\tdestroy(): void;\n\tchannels: { fetch(id: string): Promise<DiscordChannelLike | null> };\n}\n\nexport type DiscordClientFactory = () => DiscordClientLike;\n\n// ---------------------------------------------------------------------------\n// Pure mapping + small helpers\n// ---------------------------------------------------------------------------\n\nfunction errMessage(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/** lunr: download an inbound Discord attachment from its CDN URL. */\n/** Discord REST error code (DiscordAPIError.code) when present. */\nfunction discordErrorCode(err: unknown): number | undefined {\n\tconst code = (err as { code?: unknown } | null)?.code;\n\treturn typeof code === \"number\" ? code : undefined;\n}\n\nexport interface MessageToEventOptions {\n\t/** Channel ids (or parent ids for threads) to drop outright — blacklist wins over mentions. */\n\tignoredChannels?: readonly string[];\n\t/** Threads the bot has spoken in; messages there count as mentionedBot. */\n\tparticipatingThreads?: ReadonlySet<string>;\n}\n\nfunction isImageContentType(contentType: string | undefined): boolean {\n\treturn typeof contentType === \"string\" && contentType.split(\";\")[0]?.trim().toLowerCase().startsWith(\"image/\");\n}\n\n/** lunr: collect inbound image attachments (contentType image/*) from a Discord message. */\nexport function collectDiscordImageAttachments(message: DiscordMessageLike): DiscordAttachmentLike[] {\n\treturn Array.from(message.attachments?.values() ?? []).filter((attachment) =>\n\t\tisImageContentType(attachment.contentType),\n\t);\n}\n\n/**\n * Map a Discord message to a MessageEvent; null = drop (own message, any bot,\n * system message, ignored channel, unsupported channel type, or a non-DM\n * message mentioning others but not us). Messages with no text but image\n * attachments are kept (v1: images inbound).\n */\nexport function messageToEvent(\n\tmessage: DiscordMessageLike,\n\tbotUser: { id: string },\n\toptions: MessageToEventOptions = {},\n): MessageEvent | null {\n\tconst author = message.author;\n\tif (!author) return null;\n\tif (author.id === botUser.id) return null; // own messages\n\tif (author.bot) return null; // all bots (same simplification as telegram)\n\tif (message.system) return null;\n\tconst content = (message.content ?? \"\").trim();\n\tconst imageAttachments = collectDiscordImageAttachments(message);\n\tif (content === \"\" && imageAttachments.length === 0 && !Array.from(message.attachments?.values() ?? []).length)\n\t\treturn null;\n\n\tconst channel = message.channel;\n\tconst ignored = options.ignoredChannels ?? [];\n\tif (ignored.includes(channel.id) || (channel.parentId != null && ignored.includes(channel.parentId))) {\n\t\treturn null; // blacklist wins over mentions\n\t}\n\n\t// chatId/threadId mapping — see the file header for the session-key pairing.\n\tlet chatType: ChatType;\n\tlet chatId: string;\n\tlet threadId: string | undefined;\n\tif (channel.type === ChannelType.DM) {\n\t\tchatType = \"dm\";\n\t\tchatId = channel.id;\n\t} else if (THREAD_TYPES.includes(channel.type)) {\n\t\tchatType = \"thread\";\n\t\tchatId = channel.parentId ?? channel.id;\n\t\tthreadId = channel.id;\n\t} else if (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\tchatType = \"group\";\n\t\tchatId = channel.id;\n\t} else {\n\t\treturn null; // voice/category/forum containers etc. — unsupported in v1\n\t}\n\n\tconst explicitMention = message.mentions?.has(botUser) === true;\n\tconst participating = threadId !== undefined && options.participatingThreads?.has(threadId) === true;\n\tconst mentionedBot = explicitMention || participating;\n\n\t// Multi-agent filter (hermes DISCORD_IGNORE_NO_MENTION): outside DMs, a\n\t// message that mentions other users/bots but not us is not for us.\n\tif (!mentionedBot && chatType !== \"dm\") {\n\t\tconst mentionedIds = [...content.matchAll(/<@!?(\\d+)>/g)].map((m) => m[1]);\n\t\tif (mentionedIds.some((id) => id !== botUser.id)) return null;\n\t}\n\n\tlet text = content;\n\tif (explicitMention) {\n\t\ttext = text.replace(new RegExp(`<@!?${botUser.id}>`, \"g\"), \"\").trim();\n\t}\n\n\tconst refId = message.reference?.messageId;\n\tconst cachedReply = refId != null ? channel.messages?.cache?.get(refId)?.content : undefined;\n\n\treturn {\n\t\ttext,\n\t\tmessageId: message.id,\n\t\treplyToText: cachedReply !== undefined ? cachedReply.slice(0, REPLY_TO_TEXT_MAX) : undefined,\n\t\tsource: {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId,\n\t\t\tchatType,\n\t\t\tuserId: author.id,\n\t\t\tuserName: author.username,\n\t\t\tthreadId,\n\t\t},\n\t\tmetadata: mentionedBot ? { mentionedBot: true } : undefined,\n\t};\n}\n\nfunction mapDiscordChannel(\n\tchannel: DiscordChannelLike,\n): { chatType: ChatType; chatId: string; threadId?: string } | null {\n\tif (channel.type === ChannelType.DM) {\n\t\treturn { chatType: \"dm\", chatId: channel.id };\n\t}\n\tif (THREAD_TYPES.includes(channel.type)) {\n\t\treturn { chatType: \"thread\", chatId: channel.parentId ?? channel.id, threadId: channel.id };\n\t}\n\tif (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\treturn { chatType: \"group\", chatId: channel.id };\n\t}\n\treturn null;\n}\n\nexport function buttonInteractionToEvent(interaction: DiscordButtonInteractionLike): CallbackEvent | null {\n\tconst channel = interaction.message.channel;\n\tconst mapped = mapDiscordChannel(channel);\n\tif (!mapped) return null;\n\treturn {\n\t\tid: interaction.id,\n\t\tchatId: mapped.chatId,\n\t\tmessageId: interaction.message.id,\n\t\tuserId: interaction.user.id,\n\t\tuserName: interaction.user.username,\n\t\tdata: interaction.customId,\n\t\tthreadId: mapped.threadId,\n\t};\n}\n\nfunction buildDiscordComponents(buttons: ButtonSpec[][]): unknown[] {\n\tconst rows: ActionRowBuilder<ButtonBuilder>[] = [];\n\tfor (const row of buttons) {\n\t\tconst actionRow = new ActionRowBuilder<ButtonBuilder>();\n\t\tfor (const button of row) {\n\t\t\tconst style = button.data === \"\" ? ButtonStyle.Secondary : ButtonStyle.Secondary;\n\t\t\tconst builder = new ButtonBuilder().setCustomId(button.data).setLabel(button.label).setStyle(style);\n\t\t\tif (button.data === \"\") builder.setDisabled(true);\n\t\t\tactionRow.addComponents(builder);\n\t\t}\n\t\trows.push(actionRow);\n\t}\n\treturn rows;\n}\n\n// ---------------------------------------------------------------------------\n// Gateway-resume replay dedup (module-level: one process = one bot connection)\n// ---------------------------------------------------------------------------\n\nconst seenMessageIds = new Set<string>();\n\n/** Returns true when this id was already seen (drop the replay). */\nfunction markMessageSeen(id: string): boolean {\n\tif (seenMessageIds.has(id)) return true;\n\tseenMessageIds.add(id);\n\tif (seenMessageIds.size > SEEN_CAP) {\n\t\tconst oldest = seenMessageIds.values().next().value; // Sets iterate in insertion order\n\t\tif (oldest !== undefined) seenMessageIds.delete(oldest);\n\t}\n\treturn false;\n}\n\n/** Test hook: reset RESUME dedup state between tests. */\nexport function resetSeenMessageIds(): void {\n\tseenMessageIds.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\nconst defaultScheduler: Scheduler = {\n\tsetTimeout: (fn, ms) => {\n\t\tconst handle = setTimeout(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearTimeout: (handle) => clearTimeout(handle),\n\tsetInterval: (fn, ms) => {\n\t\tconst handle = setInterval(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearInterval: (handle) => clearInterval(handle),\n};\n\nfunction defaultClientFactory(): DiscordClientLike {\n\t// No GatewayIntentBits.GuildMembers — see the file header for the\n\t// privileged-intent trap (bot stays offline without the portal opt-in).\n\treturn new Client({\n\t\tintents: [\n\t\t\tGatewayIntentBits.Guilds,\n\t\t\tGatewayIntentBits.GuildMessages,\n\t\t\tGatewayIntentBits.DirectMessages,\n\t\t\tGatewayIntentBits.MessageContent,\n\t\t],\n\t}) as unknown as DiscordClientLike;\n}\n\nexport interface DiscordAdapterOptions {\n\t/** Injected Client factory (tests). When omitted, a real discord.js Client is created. */\n\tclientFactory?: DiscordClientFactory;\n\t/** lunr: injected image downloader for inbound attachments (tests). Defaults to fetch(url). */\n\tdownloadAttachment?: (url: string) => Promise<Uint8Array>;\n\t/** lunr: skip inbound media larger than this many bytes. Defaults to 10MB. */\n\tmaxMediaBytes?: number;\n\tscheduler?: Scheduler;\n\tdebounceMs?: number;\n\tnow?: () => number;\n}\n\ninterface PendingEntry {\n\tevent: MessageEvent;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\ninterface TypingEntry {\n\tinterval: ReturnType<typeof setInterval>;\n\tlastRefresh: number;\n}\n\nfunction typingKey(chatId: string, threadId?: string): string {\n\treturn `${chatId}:${threadId ?? \"\"}`;\n}\n\nexport class DiscordAdapter implements PlatformAdapter {\n\treadonly platform = \"discord\";\n\treadonly maxMessageLength = 2000;\n\n\tprivate readonly cfg: DiscordConfig;\n\tprivate readonly clientFactory: DiscordClientFactory;\n\tprivate readonly downloadAttachment: (url: string) => Promise<Uint8Array>;\n\tprivate readonly maxMediaBytes: number;\n\tprivate readonly scheduler: Scheduler;\n\tprivate readonly debounceMs: number;\n\tprivate readonly now: () => number;\n\tprivate client?: DiscordClientLike;\n\tprivate botUserId?: string;\n\tprivate handler?: (event: MessageEvent) => void;\n\tprivate commandSuggestions?: Parameters<NonNullable<PlatformAdapter[\"setCommandSuggestions\"]>>[0];\n\tprivate callbackHandler?: (event: CallbackEvent) => void;\n\tprivate readonly pending = new Map<string, PendingEntry>();\n\tprivate readonly typing = new Map<string, TypingEntry>();\n\tprivate readonly channelCache = new Map<string, DiscordChannelLike>();\n\t/** Threads the bot has spoken in (session memory only; hermes persists this). */\n\tprivate readonly participatingThreads = new Set<string>();\n\t/** messageId → channel it was sent to, so editMessage finds thread messages. */\n\tprivate readonly sentMessageChannels = new Map<string, string>();\n\t/** callbackId → interaction, so answerCallback can acknowledge the click. */\n\tprivate readonly callbackInteractions = new Map<string, DiscordButtonInteractionLike>();\n\n\tconstructor(cfg: DiscordConfig, options: DiscordAdapterOptions = {}) {\n\t\tthis.cfg = cfg;\n\t\tthis.scheduler = options.scheduler ?? defaultScheduler;\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\t\tthis.now = options.now ?? Date.now;\n\t\tif (options.clientFactory) {\n\t\t\tthis.clientFactory = options.clientFactory;\n\t\t} else {\n\t\t\tif (!cfg.token) {\n\t\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t\t}\n\t\t\tthis.clientFactory = defaultClientFactory;\n\t\t}\n\t\tthis.downloadAttachment = options.downloadAttachment ?? downloadAttachment;\n\t\tthis.maxMediaBytes = options.maxMediaBytes ?? 10 * 1024 * 1024;\n\t}\n\n\tsetCommandSuggestions(\n\t\thandler: (source: MessageEvent[\"source\"], command: string) => Promise<readonly string[]>,\n\t): void {\n\t\tthis.commandSuggestions = handler;\n\t}\n\n\tonMessage(handler: (event: MessageEvent) => void): void {\n\t\tthis.handler = handler;\n\t}\n\n\tonCallback(handler: (event: CallbackEvent) => void): void {\n\t\tthis.callbackHandler = handler;\n\t}\n\n\tasync connect(): Promise<boolean> {\n\t\tif (!this.cfg.token) {\n\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t}\n\t\tconst client = this.clientFactory();\n\t\tthis.client = client;\n\t\tclient.on(Events.MessageCreate, (message) => {\n\t\t\tvoid this.handleMessage(message as DiscordMessageLike).catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord message handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tclient.on(Events.InteractionCreate, (interaction) => {\n\t\t\tconst event = interaction as Interaction;\n\t\t\tconst handling = event.isButton()\n\t\t\t\t? this.handleInteraction(interaction as DiscordButtonInteractionLike)\n\t\t\t\t: this.handleNativeCommand(event);\n\t\t\tvoid handling.catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord interaction handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tconst ready = new Promise<void>((resolve) => client.once(Events.ClientReady, () => resolve()));\n\t\ttry {\n\t\t\tawait client.login(this.cfg.token);\n\t\t} catch (err) {\n\t\t\tconsole.error(`[gateway] discord login failed: ${errMessage(err)}`);\n\t\t\treturn false;\n\t\t}\n\t\tawait ready;\n\t\tthis.botUserId = client.user?.id;\n\t\tif (this.botUserId === undefined) {\n\t\t\tconsole.error(\"[gateway] discord: client ready without a user — cannot map messages\");\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tfor (const key of [...this.pending.keys()]) this.cancelPending(key);\n\t\tfor (const key of [...this.typing.keys()]) this.stopTyping(key);\n\t\tthis.channelCache.clear();\n\t\tthis.callbackInteractions.clear();\n\t\tthis.client?.destroy();\n\t\tthis.client = undefined;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Inbound dispatch + debounce\n\t// -----------------------------------------------------------------------\n\n\tprivate async handleMessage(message: DiscordMessageLike): Promise<void> {\n\t\tif (!this.botUserId || !this.handler) return;\n\t\tif (markMessageSeen(message.id)) return; // gateway-resume replay\n\t\tconst event = messageToEvent(\n\t\t\tmessage,\n\t\t\t{ id: this.botUserId },\n\t\t\t{\n\t\t\t\tignoredChannels: this.cfg.ignoredChannels,\n\t\t\t\tparticipatingThreads: this.participatingThreads,\n\t\t\t},\n\t\t);\n\t\tif (!event) return;\n\t\tif (!isAuthorized(event.source, { ...loadGatewayConfig(), discord: this.cfg }, createPairingStore())) {\n\t\t\tthis.dispatchEvent(event);\n\t\t\treturn;\n\t\t}\n\t\tawait this.resolveReplyToText(message, event);\n\t\tawait this.maybeAutoThread(message, event);\n\t\tawait this.downloadAttachments(message, event);\n\t\tthis.dispatchEvent(event);\n\t}\n\n\t/** Download and encode attachments after authorization. */\n\tprivate async downloadAttachments(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tconst imageAttachments = Array.from(message.attachments?.values() ?? []);\n\t\tif (imageAttachments.length === 0) return;\n\t\tconst attachments: InboundAttachment[] = [];\n\t\tfor (const attachment of imageAttachments) {\n\t\t\tif (attachment.size !== undefined && attachment.size > this.maxMediaBytes) continue;\n\t\t\ttry {\n\t\t\t\tconst bytes = await this.downloadAttachment(attachment.url);\n\t\t\t\tif (bytes.byteLength > this.maxMediaBytes) continue;\n\t\t\t\tattachments.push({\n\t\t\t\t\tdata: Buffer.from(bytes).toString(\"base64\"),\n\t\t\t\t\tmimeType: attachment.contentType ?? \"application/octet-stream\",\n\t\t\t\t\t...(attachment.name ? { filename: attachment.name } : {}),\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Media download failed — drop this attachment, keep the text turn.\n\t\t\t}\n\t\t}\n\t\tif (attachments.length > 0) event.attachments = attachments;\n\t}\n\n\tasync registerCommands(commands: { name: string; description: string }[]): Promise<void> {\n\t\tif (!this.client?.application) return;\n\t\tawait this.client.application.commands.set(\n\t\t\tcommands.slice(0, 95).map((command) => ({\n\t\t\t\tname: command.name,\n\t\t\t\tdescription: command.description.slice(0, 100),\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"args\",\n\t\t\t\t\t\tdescription: \"Command arguments or search\",\n\t\t\t\t\t\ttype: ApplicationCommandOptionType.String,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tautocomplete: [\"mode\", \"thinking\", \"project\", \"sessions\"].includes(command.name),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t})),\n\t\t);\n\t}\n\n\tprivate async handleNativeCommand(interaction: Interaction): Promise<boolean> {\n\t\tif (!interaction.isChatInputCommand?.() && !interaction.isAutocomplete?.()) return false;\n\t\tif (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) return false;\n\t\tconst thread = interaction.channel?.isThread() ? interaction.channel : undefined;\n\t\tconst source = {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId: thread?.parentId ?? interaction.channelId ?? interaction.user.id,\n\t\t\tthreadId: thread?.id,\n\t\t\tchatType: thread ? (\"thread\" as const) : interaction.inGuild() ? (\"channel\" as const) : (\"dm\" as const),\n\t\t\tuserId: interaction.user.id,\n\t\t\tuserName: interaction.user.username,\n\t\t};\n\t\tconst cfg = loadGatewayConfig();\n\t\tconst authorized = isAuthorized(source, cfg, createPairingStore());\n\t\tif (interaction.isAutocomplete()) {\n\t\t\tif (!authorized || !isGatewayOwner(source, cfg)) {\n\t\t\t\tawait interaction.respond([]);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst query = String(interaction.options.getFocused()).toLowerCase();\n\t\t\tlet values: string[] = [];\n\t\t\tif (interaction.commandName === \"mode\") values = [\"yolo\", \"auto\", \"read-only\"];\n\t\t\tif (this.commandSuggestions && [\"thinking\", \"model\"].includes(interaction.commandName)) {\n\t\t\t\tvalues = [...(await this.commandSuggestions(source, interaction.commandName))];\n\t\t\t}\n\t\t\tif (interaction.commandName === \"project\") values = cfg.projectRoots ?? [];\n\t\t\tif (interaction.commandName === \"sessions\") {\n\t\t\t\tconst { SessionManager } = await import(\"../../core/session-manager.ts\");\n\t\t\t\tvalues = (\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tSessionManager.listAll(),\n\t\t\t\t\t\tnew Promise<[]>((resolve) => setTimeout(() => resolve([]), 1500)),\n\t\t\t\t\t])\n\t\t\t\t).map((s) => s.name || s.id);\n\t\t\t}\n\t\t\tawait interaction.respond(\n\t\t\t\tvalues\n\t\t\t\t\t.filter((s) => s.toLowerCase().includes(query))\n\t\t\t\t\t.slice(0, 25)\n\t\t\t\t\t.map((value) => ({ name: value.slice(0, 100), value: value.slice(0, 100) })),\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\tawait interaction.reply({\n\t\t\tcontent: authorized\n\t\t\t\t? \"Command received.\"\n\t\t\t\t: \"Pair your account before using this command. Send the bot a private message for a pairing code.\",\n\t\t\tephemeral: true,\n\t\t});\n\t\tif (authorized)\n\t\t\tthis.handler?.({\n\t\t\t\ttext: `/${interaction.commandName}${interaction.options.getString(\"args\") ? ` ${interaction.options.getString(\"args\")}` : \"\"}`,\n\t\t\t\tsource,\n\t\t\t\tmessageId: \"\",\n\t\t\t\tmetadata: { mentionedBot: true, nativeCommand: true },\n\t\t\t});\n\t\treturn true;\n\t}\n\n\tasync sendFile(chatId: string, file: string, opts?: SendOptions): Promise<SendResult> {\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel?.send) return { success: false, error: \"Channel is unavailable.\" };\n\t\ttry {\n\t\t\tconst message = await channel.send({ content: \"\", files: [file] });\n\t\t\treturn { success: true, messageId: message.id };\n\t\t} catch (error) {\n\t\t\treturn { success: false, error: errMessage(error) };\n\t\t}\n\t}\n\n\tprivate async handleInteraction(interaction: DiscordButtonInteractionLike): Promise<void> {\n\t\tif (!this.callbackHandler) return;\n\t\tif (!interaction.isButton()) return;\n\t\tconst event = buttonInteractionToEvent(interaction);\n\t\tif (!event) return;\n\t\tthis.callbackInteractions.set(interaction.id, interaction);\n\t\tif (this.callbackInteractions.size > CALLBACK_INTERACTION_CAP) {\n\t\t\tconst oldest = this.callbackInteractions.keys().next().value;\n\t\t\tif (oldest !== undefined) this.callbackInteractions.delete(oldest);\n\t\t}\n\t\tthis.callbackHandler(event);\n\t}\n\n\t/** Best-effort: fill replyToText from the referenced message when it wasn't cached. */\n\tprivate async resolveReplyToText(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (event.replyToText !== undefined) return;\n\t\tconst refId = message.reference?.messageId;\n\t\tif (refId == null) return;\n\t\tconst messages = message.channel.messages;\n\t\tif (!messages) return;\n\t\ttry {\n\t\t\tconst referenced = await messages.fetch(refId);\n\t\t\tif (referenced?.content) event.replyToText = referenced.content.slice(0, REPLY_TO_TEXT_MAX);\n\t\t} catch {\n\t\t\t// unknown/deleted reference — no reply context\n\t\t}\n\t}\n\n\t/** autoThread: spin a thread off a bot-mentioning guild-text message and re-target the event. */\n\tprivate async maybeAutoThread(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (!this.cfg.autoThread) return;\n\t\tif (event.metadata?.mentionedBot !== true) return;\n\t\t// Only guild TEXT channels: DMs can't thread and messages already inside\n\t\t// a thread don't need another one.\n\t\tif (message.channel.type !== ChannelType.GuildText) return;\n\t\tif (typeof message.startThread !== \"function\") return;\n\t\ttry {\n\t\t\tconst thread = await message.startThread({\n\t\t\t\tname: (event.text || \"conversation\").slice(0, THREAD_NAME_MAX),\n\t\t\t\tautoArchiveDuration: 60,\n\t\t\t});\n\t\t\tthis.participatingThreads.add(thread.id);\n\t\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t\t}\n\t\t\tevent.source = { ...event.source, chatType: \"thread\", chatId: message.channel.id, threadId: thread.id };\n\t\t} catch {\n\t\t\t// e.g. missing CREATE_THREADS permission — respond in-channel instead.\n\t\t}\n\t}\n\n\tprivate debounceKey(event: MessageEvent): string {\n\t\treturn `${event.source.chatId}:${event.source.threadId ?? \"\"}:${event.source.userId}`;\n\t}\n\n\tprivate dispatchEvent(event: MessageEvent): void {\n\t\tconst key = this.debounceKey(event);\n\t\tif (event.text.trimStart().startsWith(\"/\")) {\n\t\t\t// Commands must not merge: flush any pending text, emit immediately.\n\t\t\tthis.flushPending(key);\n\t\t\tthis.handler?.(event);\n\t\t\treturn;\n\t\t}\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tthis.scheduler.clearTimeout(existing.timer);\n\t\t\t// Merge: earlier text first, latest messageId (and other fields) win.\n\t\t\tconst mergedAttachments = [...(existing.event.attachments ?? []), ...(event.attachments ?? [])];\n\t\t\texisting.event = {\n\t\t\t\t...event,\n\t\t\t\ttext: `${existing.event.text}\\n${event.text}`,\n\t\t\t\t...(mergedAttachments.length > 0 ? { attachments: mergedAttachments } : {}),\n\t\t\t};\n\t\t\texisting.timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t} else {\n\t\t\tconst timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t\tthis.pending.set(key, { event, timer });\n\t\t}\n\t}\n\n\tprivate flushPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t\tthis.handler?.(entry.event);\n\t}\n\n\tprivate cancelPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Outbound\n\t// -----------------------------------------------------------------------\n\n\tprivate async fetchChannel(id: string): Promise<DiscordChannelLike | null> {\n\t\tif (this.channelCache.has(id)) return this.channelCache.get(id) ?? null;\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\tconst channel = await this.client.channels.fetch(id);\n\t\t\tif (channel) this.channelCache.set(id, channel);\n\t\t\tif (this.channelCache.size > CHANNEL_CACHE_CAP) {\n\t\t\t\tconst oldest = this.channelCache.keys().next().value;\n\t\t\t\tif (oldest !== undefined) this.channelCache.delete(oldest);\n\t\t\t}\n\t\t\treturn channel;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate recordSent(messageId: string, channel: DiscordChannelLike): void {\n\t\tif (THREAD_TYPES.includes(channel.type)) this.participatingThreads.add(channel.id);\n\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t}\n\t\tthis.sentMessageChannels.set(messageId, channel.id);\n\t\tif (this.sentMessageChannels.size > SENT_CHANNEL_CAP) {\n\t\t\tconst oldest = this.sentMessageChannels.keys().next().value;\n\t\t\tif (oldest !== undefined) this.sentMessageChannels.delete(oldest);\n\t\t}\n\t}\n\n\tprivate async trySend(\n\t\tchannel: DiscordChannelLike,\n\t\tpayload: { content: string; reply?: { messageReference: string }; components?: unknown[] },\n\t): Promise<SendResult> {\n\t\tif (typeof channel.send !== \"function\") {\n\t\t\treturn { success: false, error: `channel ${channel.id} is not sendable` };\n\t\t}\n\t\ttry {\n\t\t\tconst sent = await channel.send(payload);\n\t\t\tthis.recordSent(sent.id, channel);\n\t\t\treturn { success: true, messageId: sent.id };\n\t\t} catch (err) {\n\t\t\t// 429s are queued internally by discord.js and don't surface here.\n\t\t\treturn { success: false, error: errMessage(err), retryable: discordErrorCode(err) !== ERR_INVALID_FORM_BODY };\n\t\t}\n\t}\n\n\tasync send(chatId: string, text: string, opts?: SendOptions): Promise<SendResult> {\n\t\t// A completed send ends the typing bubble — stop the refresher (see sendTyping).\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\t// The reply target may be deleted/unknown — fall back to a plain send.\n\t\t\treturn this.trySend(channel, { content: text });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync sendButtons(chatId: string, text: string, buttons: ButtonSpec[][], opts?: SendOptions): Promise<SendResult> {\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t\tcomponents: buildDiscordComponents(buttons),\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\treturn this.trySend(channel, { content: text, components: buildDiscordComponents(buttons) });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync answerCallback(id: string, text?: string): Promise<void> {\n\t\tconst interaction = this.callbackInteractions.get(id);\n\t\tif (!interaction) return;\n\t\tthis.callbackInteractions.delete(id);\n\t\ttry {\n\t\t\tif (text !== undefined && interaction.reply) {\n\t\t\t\tawait interaction.reply({ content: text, ephemeral: true });\n\t\t\t} else {\n\t\t\t\tawait interaction.deferUpdate();\n\t\t\t}\n\t\t} catch {\n\t\t\t// best-effort acknowledgment\n\t\t}\n\t}\n\n\tasync editMessage(chatId: string, messageId: string, text: string, buttons?: ButtonSpec[][]): Promise<SendResult> {\n\t\t// The router passes only chatId; thread messages live in their own\n\t\t// channel, so prefer the channel recorded at send() time.\n\t\tconst channel = await this.fetchChannel(this.sentMessageChannels.get(messageId) ?? chatId);\n\t\tif (!channel?.messages) {\n\t\t\treturn { success: false, error: `channel ${this.sentMessageChannels.get(messageId) ?? chatId} not found` };\n\t\t}\n\t\ttry {\n\t\t\tconst message = await channel.messages.fetch(messageId);\n\t\t\tif (!message || typeof message.edit !== \"function\") {\n\t\t\t\treturn { success: false, error: `message ${messageId} not found` };\n\t\t\t}\n\t\t\tconst editOptions: { content: string; components?: unknown[] } = { content: text };\n\t\t\tif (buttons !== undefined) editOptions.components = buildDiscordComponents(buttons);\n\t\t\tawait message.edit(editOptions);\n\t\t\treturn { success: true, messageId };\n\t\t} catch (err) {\n\t\t\tconst code = discordErrorCode(err);\n\t\t\tif (code === ERR_UNKNOWN_MESSAGE) return { success: false, error: errMessage(err) }; // terminal\n\t\t\treturn { success: false, error: errMessage(err), retryable: true };\n\t\t}\n\t}\n\n\tasync sendTyping(chatId: string, threadId?: string): Promise<void> {\n\t\t// Self-contained typing refresher (see file header): re-send the action\n\t\t// every 8s until send() completes for this target or the 2min safety net\n\t\t// fires — all errors swallowed.\n\t\tconst key = typingKey(chatId, threadId);\n\t\tconst targetId = threadId ?? chatId;\n\t\tawait this.doTyping(targetId);\n\t\tconst existing = this.typing.get(key);\n\t\tif (existing) {\n\t\t\texisting.lastRefresh = this.now();\n\t\t\treturn;\n\t\t}\n\t\tconst interval = this.scheduler.setInterval(() => {\n\t\t\tconst entry = this.typing.get(key);\n\t\t\tif (!entry) return;\n\t\t\tif (this.now() - entry.lastRefresh > TYPING_SAFETY_MS) {\n\t\t\t\tthis.stopTyping(key);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvoid this.doTyping(targetId);\n\t\t}, TYPING_INTERVAL_MS);\n\t\tthis.typing.set(key, { interval, lastRefresh: this.now() });\n\t}\n\n\tprivate async doTyping(channelId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst channel = await this.fetchChannel(channelId);\n\t\t\tawait channel?.sendTyping?.();\n\t\t} catch {\n\t\t\t// typing is cosmetic — never surface\n\t\t}\n\t}\n\n\tprivate stopTyping(key: string): void {\n\t\tconst entry = this.typing.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearInterval(entry.interval);\n\t\tthis.typing.delete(key);\n\t}\n}\n"]}
1
+ {"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../../src/gateway/adapters/discord.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway Discord adapter (Phase 3).\n *\n * Built on discord.js v14 (gateway websocket; the library owns reconnects and\n * REST rate-limit queuing internally, so neither is handled here). No voice,\n * no native slash-command registration — commands arrive as plain message\n * text, same as Telegram.\n *\n * Intents: Guilds, GuildMessages, DirectMessages, MessageContent. GuildMembers\n * is deliberately NOT requested: it is a privileged intent, and asking for it\n * without the dev-portal opt-in makes Discord refuse the identify — the bot\n * connects and immediately goes offline. v1 has no role-based config\n * (SessionSource.roleAuthorized stays undefined), so there is nothing to gain.\n *\n * Inbound: messageToEvent() is the pure message → MessageEvent mapping\n * (dm/group/thread chat types, <@id>/<@!id> mention detection with the mention\n * stripped, thread-participation counts as a mention, own + all-bot + system +\n * attachment-only messages dropped, other-mention-without-us dropped outside\n * DMs — hermes DISCORD_IGNORE_NO_MENTION). A module-level seen-id Set\n * (cap 500, FIFO evict) drops gateway-resume replays. cfg.ignoredChannels is\n * enforced adapter-side (channel id or parent id; blacklist wins over\n * mentions). Consecutive texts from the same chat/thread/user are debounced\n * 600ms and merged (\"\\n\"-joined, latest messageId kept); texts starting with\n * \"/\" flush immediately so commands never merge.\n * Button interactions are routed via interactionCreate → buttonInteractionToEvent().\n *\n * chatId/threadId mapping (pairs with session-keys.ts buildSessionKey):\n * dm → chatId = channel.id → agent:main:discord:dm:<channelId>\n * thread → chatId = PARENT channel id, threadId = → agent:main:discord:thread:<parentId>:<threadId>\n * thread channel id (shared session, no userId)\n * guild text/announcement → chatId = channel.id → agent:main:discord:group:<channelId>[:<userId>]\n * (chatType \"group\"; userId appended when groupSessionsPerUser is on)\n *\n * autoThread (cfg.discord.autoThread, default true): a bot-mentioning message\n * in a guild TEXT channel gets a thread spun off it (startThread, 60min\n * archive); the event is re-targeted to the thread (chatType \"thread\", chatId\n * parent id, threadId thread id). On failure the event stays in-channel.\n *\n * Thread participation: an in-memory Set of thread ids the bot has sent to\n * (recorded on every successful send() into a thread and on autoThread\n * creation); messages in those threads count as mentionedBot so follow-ups\n * don't need a re-mention. NOT persisted in v1 (hermes persists it).\n *\n * Outbound: channel.send({ content, reply: { messageReference } }) with a\n * one-shot plain-send fallback when the reply reference fails. DiscordAPIError\n * 50035 (invalid form body, e.g. overlong content) is reported non-retryable;\n * 429s are queued internally by discord.js; everything else is retryable.\n * editMessage resolves the channel via a messageId → channel map recorded at\n * send() time (router passes only chatId, but thread messages live in their\n * own channel); Unknown Message (10008) is a terminal { success: false }.\n * Message splitting is NOT done here — the router owns that (text.ts).\n *\n * Buttons: ActionRowBuilder rows of ButtonBuilder components (max 5×5).\n * answerCallback acknowledges the interaction; a text argument produces an\n * ephemeral reply (used for unauthorized/expired taps), otherwise deferUpdate().\n *\n * Typing: sendTyping() is a self-contained refresher — Discord typing bubbles\n * last ~10s, so the action is re-sent on an unref'd 8s interval per target.\n * The interval stops when send() completes for that target or after 2 minutes\n * without a refresh (safety net). All errors swallowed.\n *\n * Testability: the discord.js Client goes through an injectable clientFactory\n * (tests inject a minimal fake), timers through an injectable Scheduler, and\n * messageToEvent() operates on the structural DiscordMessageLike — plain mock\n * objects, no real Client construction.\n */\n\nimport {\n\tActionRowBuilder,\n\tApplicationCommandOptionType,\n\tButtonBuilder,\n\tButtonStyle,\n\tChannelType,\n\tClient,\n\tEvents,\n\tGatewayIntentBits,\n\ttype Interaction,\n} from \"discord.js\";\nimport { isAuthorized } from \"../authz.ts\";\nimport { type DiscordConfig, loadGatewayConfig } from \"../config.ts\";\nimport { downloadAttachment } from \"../download.ts\";\nimport { createPairingStore } from \"../pairing.ts\";\nimport type {\n\tButtonSpec,\n\tCallbackEvent,\n\tChatType,\n\tInboundAttachment,\n\tMessageEvent,\n\tPlatformAdapter,\n\tSendOptions,\n\tSendResult,\n} from \"../types.ts\";\nimport type { Scheduler } from \"./telegram.ts\";\n\nconst DEFAULT_DEBOUNCE_MS = 600;\nconst TYPING_INTERVAL_MS = 8000; // Discord typing lasts ~10s\nconst TYPING_SAFETY_MS = 120_000; // auto-stop after 2min without a refresh\nconst REPLY_TO_TEXT_MAX = 500;\nconst THREAD_NAME_MAX = 40;\nconst SEEN_CAP = 500;\nconst SENT_CHANNEL_CAP = 500;\nconst CALLBACK_INTERACTION_CAP = 500;\nconst CHANNEL_CACHE_CAP = 500;\nconst PARTICIPATING_THREAD_CAP = 500;\nconst ERR_INVALID_FORM_BODY = 50035; // e.g. content over 2000 chars — non-retryable\nconst ERR_UNKNOWN_MESSAGE = 10008; // deleted/unknown message — terminal\n\nconst THREAD_TYPES: readonly number[] = [\n\tChannelType.PublicThread,\n\tChannelType.PrivateThread,\n\tChannelType.AnnouncementThread,\n];\n\n// ---------------------------------------------------------------------------\n// Structural discord.js shapes (only the fields the adapter reads)\n// ---------------------------------------------------------------------------\n\nexport interface DiscordSentMessageLike {\n\tid: string;\n}\n\nexport interface DiscordFetchedMessageLike {\n\tcontent?: string;\n\tedit?(options: string | { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordChannelLike {\n\tid: string;\n\ttype: number;\n\tparentId?: string | null;\n\tsend?(payload: {\n\t\tcontent: string;\n\t\tfiles?: string[];\n\t\treply?: { messageReference: string };\n\t\tcomponents?: unknown[];\n\t}): Promise<DiscordSentMessageLike>;\n\tsendTyping?(): Promise<unknown>;\n\tmessages?: {\n\t\tcache?: { get(id: string): { content?: string } | undefined };\n\t\tfetch(id: string): Promise<DiscordFetchedMessageLike | null>;\n\t};\n}\n\nexport interface DiscordButtonInteractionLike {\n\tid: string;\n\tcustomId: string;\n\tuser: { id: string; username?: string };\n\tmessage: { id: string; channel: DiscordChannelLike };\n\tisButton(): boolean;\n\tdeferUpdate(): Promise<unknown>;\n\treply?(options: { content: string; ephemeral: boolean }): Promise<unknown>;\n\tupdate?(options: { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordAttachmentLike {\n\tid: string;\n\t/** Direct CDN URL for the attachment content. */\n\turl: string;\n\tcontentType?: string;\n\tname?: string;\n\tsize?: number;\n}\n\nexport interface DiscordMessageLike {\n\tid: string;\n\tcontent?: string;\n\tsystem?: boolean;\n\tauthor: { id: string; bot?: boolean; username?: string };\n\tchannel: DiscordChannelLike;\n\treference?: { messageId?: string | null } | null;\n\tmentions?: {\n\t\thas(user: { id: string }): boolean;\n\t\trepliedUser?: { id: string } | null;\n\t};\n\tstartThread?(opts: { name: string; autoArchiveDuration: number }): Promise<DiscordChannelLike>;\n\tattachments?: DiscordAttachmentLike[];\n}\n\n/** Minimal Client surface; the real discord.js Client satisfies this. */\nexport interface DiscordClientLike {\n\tapplication?: Pick<NonNullable<Client[\"application\"]>, \"commands\"> | null;\n\tuser: { id: string } | null;\n\tlogin(token: string): Promise<unknown>;\n\ton(event: string, listener: (arg: unknown) => void): unknown;\n\tonce(event: string, listener: () => void): unknown;\n\tdestroy(): void;\n\tchannels: { fetch(id: string): Promise<DiscordChannelLike | null> };\n}\n\nexport type DiscordClientFactory = () => DiscordClientLike;\n\n// ---------------------------------------------------------------------------\n// Pure mapping + small helpers\n// ---------------------------------------------------------------------------\n\nfunction errMessage(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/** lunr: download an inbound Discord attachment from its CDN URL. */\n/** Discord REST error code (DiscordAPIError.code) when present. */\nfunction discordErrorCode(err: unknown): number | undefined {\n\tconst code = (err as { code?: unknown } | null)?.code;\n\treturn typeof code === \"number\" ? code : undefined;\n}\n\nexport interface MessageToEventOptions {\n\t/** Channel ids (or parent ids for threads) to drop outright — blacklist wins over mentions. */\n\tignoredChannels?: readonly string[];\n\t/** Threads the bot has spoken in; messages there count as mentionedBot. */\n\tparticipatingThreads?: ReadonlySet<string>;\n}\n\nfunction isImageContentType(contentType: string | undefined): boolean {\n\treturn typeof contentType === \"string\" && contentType.split(\";\")[0]?.trim().toLowerCase().startsWith(\"image/\");\n}\n\n/** lunr: collect inbound image attachments (contentType image/*) from a Discord message. */\nexport function collectDiscordImageAttachments(message: DiscordMessageLike): DiscordAttachmentLike[] {\n\treturn Array.from(message.attachments?.values() ?? []).filter((attachment) =>\n\t\tisImageContentType(attachment.contentType),\n\t);\n}\n\n/**\n * Map a Discord message to a MessageEvent; null = drop (own message, any bot,\n * system message, ignored channel, unsupported channel type, or a non-DM\n * message mentioning others but not us). Messages with no text but image\n * attachments are kept (v1: images inbound).\n */\nexport function messageToEvent(\n\tmessage: DiscordMessageLike,\n\tbotUser: { id: string },\n\toptions: MessageToEventOptions = {},\n): MessageEvent | null {\n\tconst author = message.author;\n\tif (!author) return null;\n\tif (author.id === botUser.id) return null; // own messages\n\tif (author.bot) return null; // all bots (same simplification as telegram)\n\tif (message.system) return null;\n\tconst content = (message.content ?? \"\").trim();\n\tconst imageAttachments = collectDiscordImageAttachments(message);\n\tif (content === \"\" && imageAttachments.length === 0 && !Array.from(message.attachments?.values() ?? []).length)\n\t\treturn null;\n\n\tconst channel = message.channel;\n\tconst ignored = options.ignoredChannels ?? [];\n\tif (ignored.includes(channel.id) || (channel.parentId != null && ignored.includes(channel.parentId))) {\n\t\treturn null; // blacklist wins over mentions\n\t}\n\n\t// chatId/threadId mapping — see the file header for the session-key pairing.\n\tlet chatType: ChatType;\n\tlet chatId: string;\n\tlet threadId: string | undefined;\n\tif (channel.type === ChannelType.DM) {\n\t\tchatType = \"dm\";\n\t\tchatId = channel.id;\n\t} else if (THREAD_TYPES.includes(channel.type)) {\n\t\tchatType = \"thread\";\n\t\tchatId = channel.parentId ?? channel.id;\n\t\tthreadId = channel.id;\n\t} else if (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\tchatType = \"group\";\n\t\tchatId = channel.id;\n\t} else {\n\t\treturn null; // voice/category/forum containers etc. — unsupported in v1\n\t}\n\n\tconst explicitMention = message.mentions?.has(botUser) === true;\n\tconst participating = threadId !== undefined && options.participatingThreads?.has(threadId) === true;\n\tconst mentionedBot = explicitMention || participating;\n\n\t// Multi-agent filter (hermes DISCORD_IGNORE_NO_MENTION): outside DMs, a\n\t// message that mentions other users/bots but not us is not for us.\n\tif (!mentionedBot && chatType !== \"dm\") {\n\t\tconst mentionedIds = [...content.matchAll(/<@!?(\\d+)>/g)].map((m) => m[1]);\n\t\tif (mentionedIds.some((id) => id !== botUser.id)) return null;\n\t}\n\n\tlet text = content;\n\tif (explicitMention) {\n\t\ttext = text.replace(new RegExp(`<@!?${botUser.id}>`, \"g\"), \"\").trim();\n\t}\n\n\tconst refId = message.reference?.messageId;\n\tconst cachedReply = refId != null ? channel.messages?.cache?.get(refId)?.content : undefined;\n\n\treturn {\n\t\ttext,\n\t\tmessageId: message.id,\n\t\treplyToText: cachedReply !== undefined ? cachedReply.slice(0, REPLY_TO_TEXT_MAX) : undefined,\n\t\tsource: {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId,\n\t\t\tchatType,\n\t\t\tuserId: author.id,\n\t\t\tuserName: author.username,\n\t\t\tthreadId,\n\t\t},\n\t\tmetadata: mentionedBot ? { mentionedBot: true } : undefined,\n\t};\n}\n\nfunction mapDiscordChannel(\n\tchannel: DiscordChannelLike,\n): { chatType: ChatType; chatId: string; threadId?: string } | null {\n\tif (channel.type === ChannelType.DM) {\n\t\treturn { chatType: \"dm\", chatId: channel.id };\n\t}\n\tif (THREAD_TYPES.includes(channel.type)) {\n\t\treturn { chatType: \"thread\", chatId: channel.parentId ?? channel.id, threadId: channel.id };\n\t}\n\tif (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\treturn { chatType: \"group\", chatId: channel.id };\n\t}\n\treturn null;\n}\n\nexport function buttonInteractionToEvent(interaction: DiscordButtonInteractionLike): CallbackEvent | null {\n\tconst channel = interaction.message.channel;\n\tconst mapped = mapDiscordChannel(channel);\n\tif (!mapped) return null;\n\treturn {\n\t\tid: interaction.id,\n\t\tchatId: mapped.chatId,\n\t\tmessageId: interaction.message.id,\n\t\tuserId: interaction.user.id,\n\t\tuserName: interaction.user.username,\n\t\tdata: interaction.customId,\n\t\tthreadId: mapped.threadId,\n\t};\n}\n\nfunction buildDiscordComponents(buttons: ButtonSpec[][]): unknown[] {\n\tconst rows: ActionRowBuilder<ButtonBuilder>[] = [];\n\tfor (const row of buttons) {\n\t\tconst actionRow = new ActionRowBuilder<ButtonBuilder>();\n\t\tfor (const button of row) {\n\t\t\tconst style = button.data === \"\" ? ButtonStyle.Secondary : ButtonStyle.Secondary;\n\t\t\tconst builder = new ButtonBuilder().setCustomId(button.data).setLabel(button.label).setStyle(style);\n\t\t\tif (button.data === \"\") builder.setDisabled(true);\n\t\t\tactionRow.addComponents(builder);\n\t\t}\n\t\trows.push(actionRow);\n\t}\n\treturn rows;\n}\n\n// ---------------------------------------------------------------------------\n// Gateway-resume replay dedup (module-level: one process = one bot connection)\n// ---------------------------------------------------------------------------\n\nconst seenMessageIds = new Set<string>();\n\n/** Returns true when this id was already seen (drop the replay). */\nfunction markMessageSeen(id: string): boolean {\n\tif (seenMessageIds.has(id)) return true;\n\tseenMessageIds.add(id);\n\tif (seenMessageIds.size > SEEN_CAP) {\n\t\tconst oldest = seenMessageIds.values().next().value; // Sets iterate in insertion order\n\t\tif (oldest !== undefined) seenMessageIds.delete(oldest);\n\t}\n\treturn false;\n}\n\n/** Test hook: reset RESUME dedup state between tests. */\nexport function resetSeenMessageIds(): void {\n\tseenMessageIds.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\nconst defaultScheduler: Scheduler = {\n\tsetTimeout: (fn, ms) => {\n\t\tconst handle = setTimeout(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearTimeout: (handle) => clearTimeout(handle),\n\tsetInterval: (fn, ms) => {\n\t\tconst handle = setInterval(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearInterval: (handle) => clearInterval(handle),\n};\n\nfunction defaultClientFactory(): DiscordClientLike {\n\t// No GatewayIntentBits.GuildMembers — see the file header for the\n\t// privileged-intent trap (bot stays offline without the portal opt-in).\n\treturn new Client({\n\t\tintents: [\n\t\t\tGatewayIntentBits.Guilds,\n\t\t\tGatewayIntentBits.GuildMessages,\n\t\t\tGatewayIntentBits.DirectMessages,\n\t\t\tGatewayIntentBits.MessageContent,\n\t\t],\n\t}) as unknown as DiscordClientLike;\n}\n\nexport interface DiscordAdapterOptions {\n\t/** Injected Client factory (tests). When omitted, a real discord.js Client is created. */\n\tclientFactory?: DiscordClientFactory;\n\t/** lunr: injected image downloader for inbound attachments (tests). Defaults to fetch(url). */\n\tdownloadAttachment?: (url: string) => Promise<Uint8Array>;\n\t/** lunr: skip inbound media larger than this many bytes. Defaults to 10MB. */\n\tmaxMediaBytes?: number;\n\tscheduler?: Scheduler;\n\tdebounceMs?: number;\n\tnow?: () => number;\n}\n\ninterface PendingEntry {\n\tevent: MessageEvent;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\ninterface TypingEntry {\n\tinterval: ReturnType<typeof setInterval>;\n\tlastRefresh: number;\n}\n\nfunction typingKey(chatId: string, threadId?: string): string {\n\treturn `${chatId}:${threadId ?? \"\"}`;\n}\n\nexport class DiscordAdapter implements PlatformAdapter {\n\treadonly platform = \"discord\";\n\treadonly maxMessageLength = 2000;\n\n\tprivate readonly cfg: DiscordConfig;\n\tprivate readonly clientFactory: DiscordClientFactory;\n\tprivate readonly downloadAttachment: (url: string) => Promise<Uint8Array>;\n\tprivate readonly maxMediaBytes: number;\n\tprivate readonly scheduler: Scheduler;\n\tprivate readonly debounceMs: number;\n\tprivate readonly now: () => number;\n\tprivate client?: DiscordClientLike;\n\tprivate botUserId?: string;\n\tprivate handler?: (event: MessageEvent) => void;\n\tprivate commandSuggestions?: Parameters<NonNullable<PlatformAdapter[\"setCommandSuggestions\"]>>[0];\n\tprivate callbackHandler?: (event: CallbackEvent) => void;\n\tprivate readonly pending = new Map<string, PendingEntry>();\n\tprivate readonly typing = new Map<string, TypingEntry>();\n\tprivate readonly channelCache = new Map<string, DiscordChannelLike>();\n\t/** Threads the bot has spoken in (session memory only; hermes persists this). */\n\tprivate readonly participatingThreads = new Set<string>();\n\t/** messageId → channel it was sent to, so editMessage finds thread messages. */\n\tprivate readonly sentMessageChannels = new Map<string, string>();\n\t/** callbackId → interaction, so answerCallback can acknowledge the click. */\n\tprivate readonly callbackInteractions = new Map<string, DiscordButtonInteractionLike>();\n\n\tconstructor(cfg: DiscordConfig, options: DiscordAdapterOptions = {}) {\n\t\tthis.cfg = cfg;\n\t\tthis.scheduler = options.scheduler ?? defaultScheduler;\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\t\tthis.now = options.now ?? Date.now;\n\t\tif (options.clientFactory) {\n\t\t\tthis.clientFactory = options.clientFactory;\n\t\t} else {\n\t\t\tif (!cfg.token) {\n\t\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t\t}\n\t\t\tthis.clientFactory = defaultClientFactory;\n\t\t}\n\t\tthis.downloadAttachment = options.downloadAttachment ?? downloadAttachment;\n\t\tthis.maxMediaBytes = options.maxMediaBytes ?? 10 * 1024 * 1024;\n\t}\n\n\tsetCommandSuggestions(\n\t\thandler: (source: MessageEvent[\"source\"], command: string) => Promise<readonly string[]>,\n\t): void {\n\t\tthis.commandSuggestions = handler;\n\t}\n\n\tonMessage(handler: (event: MessageEvent) => void): void {\n\t\tthis.handler = handler;\n\t}\n\n\tonCallback(handler: (event: CallbackEvent) => void): void {\n\t\tthis.callbackHandler = handler;\n\t}\n\n\tasync connect(): Promise<boolean> {\n\t\tif (!this.cfg.token) {\n\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t}\n\t\tconst client = this.clientFactory();\n\t\tthis.client = client;\n\t\tclient.on(Events.MessageCreate, (message) => {\n\t\t\tvoid this.handleMessage(message as DiscordMessageLike).catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord message handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tclient.on(Events.InteractionCreate, (interaction) => {\n\t\t\tconst event = interaction as Interaction;\n\t\t\tconst handling = event.isButton()\n\t\t\t\t? this.handleInteraction(interaction as DiscordButtonInteractionLike)\n\t\t\t\t: this.handleNativeCommand(event);\n\t\t\tvoid handling.catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord interaction handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tconst ready = new Promise<void>((resolve) => client.once(Events.ClientReady, () => resolve()));\n\t\ttry {\n\t\t\tawait client.login(this.cfg.token);\n\t\t} catch (err) {\n\t\t\tconsole.error(`[gateway] discord login failed: ${errMessage(err)}`);\n\t\t\treturn false;\n\t\t}\n\t\tawait ready;\n\t\tthis.botUserId = client.user?.id;\n\t\tif (this.botUserId === undefined) {\n\t\t\tconsole.error(\"[gateway] discord: client ready without a user — cannot map messages\");\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tfor (const key of [...this.pending.keys()]) this.cancelPending(key);\n\t\tfor (const key of [...this.typing.keys()]) this.stopTyping(key);\n\t\tthis.channelCache.clear();\n\t\tthis.callbackInteractions.clear();\n\t\tthis.client?.destroy();\n\t\tthis.client = undefined;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Inbound dispatch + debounce\n\t// -----------------------------------------------------------------------\n\n\tprivate async handleMessage(message: DiscordMessageLike): Promise<void> {\n\t\tif (!this.botUserId || !this.handler) return;\n\t\tif (markMessageSeen(message.id)) return; // gateway-resume replay\n\t\tconst event = messageToEvent(\n\t\t\tmessage,\n\t\t\t{ id: this.botUserId },\n\t\t\t{\n\t\t\t\tignoredChannels: this.cfg.ignoredChannels,\n\t\t\t\tparticipatingThreads: this.participatingThreads,\n\t\t\t},\n\t\t);\n\t\tif (!event) return;\n\t\tif (!isAuthorized(event.source, { ...loadGatewayConfig(), discord: this.cfg }, createPairingStore())) {\n\t\t\tthis.dispatchEvent(event);\n\t\t\treturn;\n\t\t}\n\t\tawait this.resolveReplyToText(message, event);\n\t\tawait this.maybeAutoThread(message, event);\n\t\tawait this.downloadAttachments(message, event);\n\t\tthis.dispatchEvent(event);\n\t}\n\n\t/** Download and encode attachments after authorization. */\n\tprivate async downloadAttachments(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tconst imageAttachments = Array.from(message.attachments?.values() ?? []);\n\t\tif (imageAttachments.length === 0) return;\n\t\tconst attachments: InboundAttachment[] = [];\n\t\tfor (const attachment of imageAttachments) {\n\t\t\tif (attachment.size !== undefined && attachment.size > this.maxMediaBytes) continue;\n\t\t\ttry {\n\t\t\t\tconst bytes = await this.downloadAttachment(attachment.url);\n\t\t\t\tif (bytes.byteLength > this.maxMediaBytes) continue;\n\t\t\t\tattachments.push({\n\t\t\t\t\tdata: Buffer.from(bytes).toString(\"base64\"),\n\t\t\t\t\tmimeType: attachment.contentType ?? \"application/octet-stream\",\n\t\t\t\t\t...(attachment.name ? { filename: attachment.name } : {}),\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Media download failed — drop this attachment, keep the text turn.\n\t\t\t}\n\t\t}\n\t\tif (attachments.length > 0) event.attachments = attachments;\n\t}\n\n\tasync registerCommands(commands: { name: string; description: string }[]): Promise<void> {\n\t\tif (!this.client?.application) return;\n\t\tawait this.client.application.commands.set(\n\t\t\tcommands.slice(0, 95).map((command) => ({\n\t\t\t\tname: command.name,\n\t\t\t\tdescription: command.description.slice(0, 100),\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"args\",\n\t\t\t\t\t\tdescription: \"Command arguments or search\",\n\t\t\t\t\t\ttype: ApplicationCommandOptionType.String,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tautocomplete: [\"mode\", \"thinking\", \"project\", \"sessions\"].includes(command.name),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t})),\n\t\t);\n\t}\n\n\tprivate async handleNativeCommand(interaction: Interaction): Promise<boolean> {\n\t\tif (!interaction.isChatInputCommand?.() && !interaction.isAutocomplete?.()) return false;\n\t\tif (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) return false;\n\t\tconst thread = interaction.channel?.isThread() ? interaction.channel : undefined;\n\t\tconst source = {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId: thread?.parentId ?? interaction.channelId ?? interaction.user.id,\n\t\t\tthreadId: thread?.id,\n\t\t\tchatType: thread ? (\"thread\" as const) : interaction.inGuild() ? (\"channel\" as const) : (\"dm\" as const),\n\t\t\tuserId: interaction.user.id,\n\t\t\tuserName: interaction.user.username,\n\t\t};\n\t\tconst cfg = loadGatewayConfig();\n\t\tconst authorized = isAuthorized(source, cfg, createPairingStore());\n\t\tif (interaction.isAutocomplete()) {\n\t\t\tif (!authorized) {\n\t\t\t\tawait interaction.respond([]);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst query = String(interaction.options.getFocused()).toLowerCase();\n\t\t\tlet values: string[] = [];\n\t\t\tif (interaction.commandName === \"mode\") values = [\"yolo\", \"auto\", \"read-only\"];\n\t\t\tif (this.commandSuggestions && [\"thinking\", \"model\"].includes(interaction.commandName)) {\n\t\t\t\tvalues = [...(await this.commandSuggestions(source, interaction.commandName))];\n\t\t\t}\n\t\t\tif (interaction.commandName === \"project\") values = cfg.projectRoots ?? [];\n\t\t\tif (interaction.commandName === \"sessions\") {\n\t\t\t\tconst { SessionManager } = await import(\"../../core/session-manager.ts\");\n\t\t\t\tvalues = (\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tSessionManager.listAll(),\n\t\t\t\t\t\tnew Promise<[]>((resolve) => setTimeout(() => resolve([]), 1500)),\n\t\t\t\t\t])\n\t\t\t\t).map((s) => s.name || s.id);\n\t\t\t}\n\t\t\tawait interaction.respond(\n\t\t\t\tvalues\n\t\t\t\t\t.filter((s) => s.toLowerCase().includes(query))\n\t\t\t\t\t.slice(0, 25)\n\t\t\t\t\t.map((value) => ({ name: value.slice(0, 100), value: value.slice(0, 100) })),\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\tawait interaction.reply({\n\t\t\tcontent: authorized\n\t\t\t\t? \"Command received.\"\n\t\t\t\t: \"Pair your account before using this command. Send the bot a private message for a pairing code.\",\n\t\t\tephemeral: true,\n\t\t});\n\t\tif (authorized)\n\t\t\tthis.handler?.({\n\t\t\t\ttext: `/${interaction.commandName}${interaction.options.getString(\"args\") ? ` ${interaction.options.getString(\"args\")}` : \"\"}`,\n\t\t\t\tsource,\n\t\t\t\tmessageId: \"\",\n\t\t\t\tmetadata: { mentionedBot: true, nativeCommand: true },\n\t\t\t});\n\t\treturn true;\n\t}\n\n\tasync sendFile(chatId: string, file: string, opts?: SendOptions): Promise<SendResult> {\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel?.send) return { success: false, error: \"Channel is unavailable.\" };\n\t\ttry {\n\t\t\tconst message = await channel.send({ content: \"\", files: [file] });\n\t\t\treturn { success: true, messageId: message.id };\n\t\t} catch (error) {\n\t\t\treturn { success: false, error: errMessage(error) };\n\t\t}\n\t}\n\n\tprivate async handleInteraction(interaction: DiscordButtonInteractionLike): Promise<void> {\n\t\tif (!this.callbackHandler) return;\n\t\tif (!interaction.isButton()) return;\n\t\tconst event = buttonInteractionToEvent(interaction);\n\t\tif (!event) return;\n\t\tthis.callbackInteractions.set(interaction.id, interaction);\n\t\tif (this.callbackInteractions.size > CALLBACK_INTERACTION_CAP) {\n\t\t\tconst oldest = this.callbackInteractions.keys().next().value;\n\t\t\tif (oldest !== undefined) this.callbackInteractions.delete(oldest);\n\t\t}\n\t\tthis.callbackHandler(event);\n\t}\n\n\t/** Best-effort: fill replyToText from the referenced message when it wasn't cached. */\n\tprivate async resolveReplyToText(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (event.replyToText !== undefined) return;\n\t\tconst refId = message.reference?.messageId;\n\t\tif (refId == null) return;\n\t\tconst messages = message.channel.messages;\n\t\tif (!messages) return;\n\t\ttry {\n\t\t\tconst referenced = await messages.fetch(refId);\n\t\t\tif (referenced?.content) event.replyToText = referenced.content.slice(0, REPLY_TO_TEXT_MAX);\n\t\t} catch {\n\t\t\t// unknown/deleted reference — no reply context\n\t\t}\n\t}\n\n\t/** autoThread: spin a thread off a bot-mentioning guild-text message and re-target the event. */\n\tprivate async maybeAutoThread(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (!this.cfg.autoThread) return;\n\t\tif (event.metadata?.mentionedBot !== true) return;\n\t\t// Only guild TEXT channels: DMs can't thread and messages already inside\n\t\t// a thread don't need another one.\n\t\tif (message.channel.type !== ChannelType.GuildText) return;\n\t\tif (typeof message.startThread !== \"function\") return;\n\t\ttry {\n\t\t\tconst thread = await message.startThread({\n\t\t\t\tname: (event.text || \"conversation\").slice(0, THREAD_NAME_MAX),\n\t\t\t\tautoArchiveDuration: 60,\n\t\t\t});\n\t\t\tthis.participatingThreads.add(thread.id);\n\t\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t\t}\n\t\t\tevent.source = { ...event.source, chatType: \"thread\", chatId: message.channel.id, threadId: thread.id };\n\t\t} catch {\n\t\t\t// e.g. missing CREATE_THREADS permission — respond in-channel instead.\n\t\t}\n\t}\n\n\tprivate debounceKey(event: MessageEvent): string {\n\t\treturn `${event.source.chatId}:${event.source.threadId ?? \"\"}:${event.source.userId}`;\n\t}\n\n\tprivate dispatchEvent(event: MessageEvent): void {\n\t\tconst key = this.debounceKey(event);\n\t\tif (event.text.trimStart().startsWith(\"/\")) {\n\t\t\t// Commands must not merge: flush any pending text, emit immediately.\n\t\t\tthis.flushPending(key);\n\t\t\tthis.handler?.(event);\n\t\t\treturn;\n\t\t}\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tthis.scheduler.clearTimeout(existing.timer);\n\t\t\t// Merge: earlier text first, latest messageId (and other fields) win.\n\t\t\tconst mergedAttachments = [...(existing.event.attachments ?? []), ...(event.attachments ?? [])];\n\t\t\texisting.event = {\n\t\t\t\t...event,\n\t\t\t\ttext: `${existing.event.text}\\n${event.text}`,\n\t\t\t\t...(mergedAttachments.length > 0 ? { attachments: mergedAttachments } : {}),\n\t\t\t};\n\t\t\texisting.timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t} else {\n\t\t\tconst timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t\tthis.pending.set(key, { event, timer });\n\t\t}\n\t}\n\n\tprivate flushPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t\tthis.handler?.(entry.event);\n\t}\n\n\tprivate cancelPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Outbound\n\t// -----------------------------------------------------------------------\n\n\tprivate async fetchChannel(id: string): Promise<DiscordChannelLike | null> {\n\t\tif (this.channelCache.has(id)) return this.channelCache.get(id) ?? null;\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\tconst channel = await this.client.channels.fetch(id);\n\t\t\tif (channel) this.channelCache.set(id, channel);\n\t\t\tif (this.channelCache.size > CHANNEL_CACHE_CAP) {\n\t\t\t\tconst oldest = this.channelCache.keys().next().value;\n\t\t\t\tif (oldest !== undefined) this.channelCache.delete(oldest);\n\t\t\t}\n\t\t\treturn channel;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate recordSent(messageId: string, channel: DiscordChannelLike): void {\n\t\tif (THREAD_TYPES.includes(channel.type)) this.participatingThreads.add(channel.id);\n\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t}\n\t\tthis.sentMessageChannels.set(messageId, channel.id);\n\t\tif (this.sentMessageChannels.size > SENT_CHANNEL_CAP) {\n\t\t\tconst oldest = this.sentMessageChannels.keys().next().value;\n\t\t\tif (oldest !== undefined) this.sentMessageChannels.delete(oldest);\n\t\t}\n\t}\n\n\tprivate async trySend(\n\t\tchannel: DiscordChannelLike,\n\t\tpayload: { content: string; reply?: { messageReference: string }; components?: unknown[] },\n\t): Promise<SendResult> {\n\t\tif (typeof channel.send !== \"function\") {\n\t\t\treturn { success: false, error: `channel ${channel.id} is not sendable` };\n\t\t}\n\t\ttry {\n\t\t\tconst sent = await channel.send(payload);\n\t\t\tthis.recordSent(sent.id, channel);\n\t\t\treturn { success: true, messageId: sent.id };\n\t\t} catch (err) {\n\t\t\t// 429s are queued internally by discord.js and don't surface here.\n\t\t\treturn { success: false, error: errMessage(err), retryable: discordErrorCode(err) !== ERR_INVALID_FORM_BODY };\n\t\t}\n\t}\n\n\tasync send(chatId: string, text: string, opts?: SendOptions): Promise<SendResult> {\n\t\t// A completed send ends the typing bubble — stop the refresher (see sendTyping).\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\t// The reply target may be deleted/unknown — fall back to a plain send.\n\t\t\treturn this.trySend(channel, { content: text });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync sendButtons(chatId: string, text: string, buttons: ButtonSpec[][], opts?: SendOptions): Promise<SendResult> {\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t\tcomponents: buildDiscordComponents(buttons),\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\treturn this.trySend(channel, { content: text, components: buildDiscordComponents(buttons) });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync answerCallback(id: string, text?: string): Promise<void> {\n\t\tconst interaction = this.callbackInteractions.get(id);\n\t\tif (!interaction) return;\n\t\tthis.callbackInteractions.delete(id);\n\t\ttry {\n\t\t\tif (text !== undefined && interaction.reply) {\n\t\t\t\tawait interaction.reply({ content: text, ephemeral: true });\n\t\t\t} else {\n\t\t\t\tawait interaction.deferUpdate();\n\t\t\t}\n\t\t} catch {\n\t\t\t// best-effort acknowledgment\n\t\t}\n\t}\n\n\tasync editMessage(chatId: string, messageId: string, text: string, buttons?: ButtonSpec[][]): Promise<SendResult> {\n\t\t// The router passes only chatId; thread messages live in their own\n\t\t// channel, so prefer the channel recorded at send() time.\n\t\tconst channel = await this.fetchChannel(this.sentMessageChannels.get(messageId) ?? chatId);\n\t\tif (!channel?.messages) {\n\t\t\treturn { success: false, error: `channel ${this.sentMessageChannels.get(messageId) ?? chatId} not found` };\n\t\t}\n\t\ttry {\n\t\t\tconst message = await channel.messages.fetch(messageId);\n\t\t\tif (!message || typeof message.edit !== \"function\") {\n\t\t\t\treturn { success: false, error: `message ${messageId} not found` };\n\t\t\t}\n\t\t\tconst editOptions: { content: string; components?: unknown[] } = { content: text };\n\t\t\tif (buttons !== undefined) editOptions.components = buildDiscordComponents(buttons);\n\t\t\tawait message.edit(editOptions);\n\t\t\treturn { success: true, messageId };\n\t\t} catch (err) {\n\t\t\tconst code = discordErrorCode(err);\n\t\t\tif (code === ERR_UNKNOWN_MESSAGE) return { success: false, error: errMessage(err) }; // terminal\n\t\t\treturn { success: false, error: errMessage(err), retryable: true };\n\t\t}\n\t}\n\n\tasync sendTyping(chatId: string, threadId?: string): Promise<void> {\n\t\t// Self-contained typing refresher (see file header): re-send the action\n\t\t// every 8s until send() completes for this target or the 2min safety net\n\t\t// fires — all errors swallowed.\n\t\tconst key = typingKey(chatId, threadId);\n\t\tconst targetId = threadId ?? chatId;\n\t\tawait this.doTyping(targetId);\n\t\tconst existing = this.typing.get(key);\n\t\tif (existing) {\n\t\t\texisting.lastRefresh = this.now();\n\t\t\treturn;\n\t\t}\n\t\tconst interval = this.scheduler.setInterval(() => {\n\t\t\tconst entry = this.typing.get(key);\n\t\t\tif (!entry) return;\n\t\t\tif (this.now() - entry.lastRefresh > TYPING_SAFETY_MS) {\n\t\t\t\tthis.stopTyping(key);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvoid this.doTyping(targetId);\n\t\t}, TYPING_INTERVAL_MS);\n\t\tthis.typing.set(key, { interval, lastRefresh: this.now() });\n\t}\n\n\tprivate async doTyping(channelId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst channel = await this.fetchChannel(channelId);\n\t\t\tawait channel?.sendTyping?.();\n\t\t} catch {\n\t\t\t// typing is cosmetic — never surface\n\t\t}\n\t}\n\n\tprivate stopTyping(key: string): void {\n\t\tconst entry = this.typing.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearInterval(entry.interval);\n\t\tthis.typing.delete(key);\n\t}\n}\n"]}
@@ -65,7 +65,7 @@
65
65
  * objects, no real Client construction.
66
66
  */
67
67
  import { ActionRowBuilder, ApplicationCommandOptionType, ButtonBuilder, ButtonStyle, ChannelType, Client, Events, GatewayIntentBits, } from "discord.js";
68
- import { isAuthorized, isGatewayOwner } from "../authz.js";
68
+ import { isAuthorized } from "../authz.js";
69
69
  import { loadGatewayConfig } from "../config.js";
70
70
  import { downloadAttachment } from "../download.js";
71
71
  import { createPairingStore } from "../pairing.js";
@@ -454,7 +454,7 @@ export class DiscordAdapter {
454
454
  const cfg = loadGatewayConfig();
455
455
  const authorized = isAuthorized(source, cfg, createPairingStore());
456
456
  if (interaction.isAutocomplete()) {
457
- if (!authorized || !isGatewayOwner(source, cfg)) {
457
+ if (!authorized) {
458
458
  await interaction.respond([]);
459
459
  return true;
460
460
  }