@ashx-j/lunr-dev 0.2.13-dev.7.1 → 0.2.16-dev.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -1
- package/dist/builtin-extensions/ashxj-tui.d.ts +14 -0
- package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
- package/dist/builtin-extensions/ashxj-tui.js +37 -3
- package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
- package/dist/builtin-extensions/index.d.ts +1 -15
- package/dist/builtin-extensions/index.d.ts.map +1 -1
- package/dist/builtin-extensions/index.js +7 -21
- package/dist/builtin-extensions/index.js.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.d.ts.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js +6 -2
- package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js.map +1 -1
- package/dist/builtin-extensions/pi-lsp-extension/src/index.d.ts +4 -0
- package/dist/builtin-extensions/pi-lsp-extension/src/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-lsp-extension/src/index.js +163 -169
- package/dist/builtin-extensions/pi-lsp-extension/src/index.js.map +1 -1
- package/dist/builtin-extensions/pi-lsp-extension/src/runtime.d.ts +46 -0
- package/dist/builtin-extensions/pi-lsp-extension/src/runtime.d.ts.map +1 -0
- package/dist/builtin-extensions/pi-lsp-extension/src/runtime.js +109 -0
- package/dist/builtin-extensions/pi-lsp-extension/src/runtime.js.map +1 -0
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.d.ts +7 -0
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.d.ts.map +1 -0
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.js +211 -0
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tool-executor.js.map +1 -0
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.d.ts +0 -5
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.js +0 -202
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.js.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/index.js +251 -94
- package/dist/builtin-extensions/pi-mcp-adapter/index.js.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/init.d.ts +4 -1
- package/dist/builtin-extensions/pi-mcp-adapter/init.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/init.js +10 -3
- package/dist/builtin-extensions/pi-mcp-adapter/init.js.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/metadata-cache.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/metadata-cache.js +6 -1
- package/dist/builtin-extensions/pi-mcp-adapter/metadata-cache.js.map +1 -1
- package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.js +2 -1
- package/dist/builtin-extensions/pi-prompt-template-model/prompt-loader.js.map +1 -1
- package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.d.ts +1 -1
- package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.js +1 -1
- package/dist/builtin-extensions/pi-prompt-template-model/tool-manager.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.js +38 -14
- package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/fork-context.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/fork-context.js +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/fork-context.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts +1 -1
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +1 -1
- package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js +2 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/watchdog/review.d.ts +1 -1
- package/dist/builtin-extensions/pi-subagents/src/watchdog/review.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/watchdog/review.js +2 -1
- package/dist/builtin-extensions/pi-subagents/src/watchdog/review.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/extract.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/extract.js +105 -54
- package/dist/builtin-extensions/pi-web-access/extract.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/gemini-search.d.ts +1 -1
- package/dist/builtin-extensions/pi-web-access/gemini-search.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/gemini-search.js +82 -65
- package/dist/builtin-extensions/pi-web-access/gemini-search.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/github-extract.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/github-extract.js +2 -0
- package/dist/builtin-extensions/pi-web-access/github-extract.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/index.js +167 -77
- package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/lazy.d.ts +18 -0
- package/dist/builtin-extensions/pi-web-access/lazy.d.ts.map +1 -0
- package/dist/builtin-extensions/pi-web-access/lazy.js +19 -0
- package/dist/builtin-extensions/pi-web-access/lazy.js.map +1 -0
- package/dist/builtin-extensions/pi-web-access/session-cleanup.d.ts +6 -0
- package/dist/builtin-extensions/pi-web-access/session-cleanup.d.ts.map +1 -0
- package/dist/builtin-extensions/pi-web-access/session-cleanup.js +17 -0
- package/dist/builtin-extensions/pi-web-access/session-cleanup.js.map +1 -0
- package/dist/catalog/models.json +1 -1
- package/dist/catalog/providers/amazon-bedrock.json +1 -1
- package/dist/catalog/providers/azure-openai-responses.json +1 -1
- package/dist/catalog/providers/fireworks.json +1 -1
- package/dist/catalog/providers/github-copilot.json +1 -1
- package/dist/catalog/providers/openai-codex.json +1 -1
- package/dist/catalog/providers/openai.json +1 -1
- package/dist/catalog/providers/opencode.json +1 -1
- package/dist/catalog/providers/openrouter.json +1 -1
- package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
- package/dist/catalog/publication.json +131 -3
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +1 -0
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli-runtime.d.ts +3 -0
- package/dist/cli-runtime.d.ts.map +1 -0
- package/dist/cli-runtime.js +73 -0
- package/dist/cli-runtime.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -53
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +3 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +56 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/catalog-cache-scope.d.ts +3 -0
- package/dist/core/catalog-cache-scope.d.ts.map +1 -0
- package/dist/core/catalog-cache-scope.js +12 -0
- package/dist/core/catalog-cache-scope.js.map +1 -0
- package/dist/core/catalog-merge.d.ts +2 -0
- package/dist/core/catalog-merge.d.ts.map +1 -1
- package/dist/core/catalog-merge.js +29 -2
- package/dist/core/catalog-merge.js.map +1 -1
- package/dist/core/catalog-refresh-polling.d.ts +8 -0
- package/dist/core/catalog-refresh-polling.d.ts.map +1 -0
- package/dist/core/catalog-refresh-polling.js +30 -0
- package/dist/core/catalog-refresh-polling.js.map +1 -0
- package/dist/core/codex-catalog.d.ts +3 -0
- package/dist/core/codex-catalog.d.ts.map +1 -0
- package/dist/core/codex-catalog.js +15 -0
- package/dist/core/codex-catalog.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +10 -5
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/host-static.d.ts.map +1 -1
- package/dist/core/extensions/host-static.js +34 -12
- package/dist/core/extensions/host-static.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +10 -64
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +2 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/live-catalog.d.ts +5 -2
- package/dist/core/live-catalog.d.ts.map +1 -1
- package/dist/core/live-catalog.js +108 -27
- package/dist/core/live-catalog.js.map +1 -1
- package/dist/core/model-runtime.d.ts +8 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +64 -12
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/official-catalog.d.ts +4 -0
- package/dist/core/official-catalog.d.ts.map +1 -1
- package/dist/core/official-catalog.js +86 -21
- package/dist/core/official-catalog.js.map +1 -1
- package/dist/core/permissions.d.ts +0 -2
- package/dist/core/permissions.d.ts.map +1 -1
- package/dist/core/permissions.js +10 -18
- package/dist/core/permissions.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +0 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/timings.d.ts +2 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +17 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/gateway/commands.d.ts +1 -1
- package/dist/gateway/commands.d.ts.map +1 -1
- package/dist/gateway/commands.js +1 -18
- package/dist/gateway/commands.js.map +1 -1
- package/dist/main.d.ts +2 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +66 -48
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +4 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +13 -22
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +141 -207
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +95 -84
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/node-runtime/agent-session-services-IFJMSX7Z.js +36 -0
- package/dist/node-runtime/benchmark-request-UC7PD3V3.js +57 -0
- package/dist/node-runtime/brave-UWDA3I22.js +143 -0
- package/dist/node-runtime/builtin-extensions-LYZ3BFWQ.js +28 -0
- package/dist/node-runtime/chunk-2QKLKMKC.js +173 -0
- package/dist/node-runtime/chunk-3GHXVUAK.js +21 -0
- package/dist/node-runtime/chunk-3RCWXWAY.js +466 -0
- package/dist/node-runtime/chunk-3WFAEVKS.js +115 -0
- package/dist/node-runtime/chunk-3X3KZKMI.js +49 -0
- package/dist/node-runtime/chunk-4HXHDDCF.js +16 -0
- package/dist/node-runtime/chunk-4TLO4PO2.js +3427 -0
- package/dist/node-runtime/chunk-4ZWGEEAS.js +69 -0
- package/dist/node-runtime/chunk-53YJI7JU.js +137 -0
- package/dist/node-runtime/chunk-5P7PZMRU.js +610 -0
- package/dist/node-runtime/chunk-7N3YHTYF.js +26 -0
- package/dist/node-runtime/chunk-7NBZ5P7I.js +24 -0
- package/dist/node-runtime/chunk-A2KQXUJD.js +105 -0
- package/dist/node-runtime/chunk-AD6VXAO7.js +342 -0
- package/dist/node-runtime/chunk-AHSTJBIB.js +64 -0
- package/dist/node-runtime/chunk-AJX357CL.js +20211 -0
- package/dist/node-runtime/chunk-ARC4KOTI.js +11178 -0
- package/dist/node-runtime/chunk-AUXVGBO4.js +19 -0
- package/dist/node-runtime/chunk-AY6CAJSL.js +30 -0
- package/dist/node-runtime/chunk-BHO7O6E5.js +296 -0
- package/dist/node-runtime/chunk-BI3DHYCN.js +8845 -0
- package/dist/node-runtime/chunk-BR7YDUKA.js +109 -0
- package/dist/node-runtime/chunk-BRJX2PRC.js +53 -0
- package/dist/node-runtime/chunk-C4O2RSQQ.js +37 -0
- package/dist/node-runtime/chunk-CCZJ3PYY.js +17 -0
- package/dist/node-runtime/chunk-CDFZEWPU.js +8388 -0
- package/dist/node-runtime/chunk-CGBXPL3E.js +1171 -0
- package/dist/node-runtime/chunk-CJECDQKP.js +19 -0
- package/dist/node-runtime/chunk-DAPBPCBP.js +53 -0
- package/dist/node-runtime/chunk-DDGMHUOW.js +305 -0
- package/dist/node-runtime/chunk-DMMTJQMQ.js +43 -0
- package/dist/node-runtime/chunk-DVGWF6A2.js +1180 -0
- package/dist/node-runtime/chunk-ECD5L7YI.js +1369 -0
- package/dist/node-runtime/chunk-EDBJNRSM.js +46 -0
- package/dist/node-runtime/chunk-F4VQFJI2.js +1269 -0
- package/dist/node-runtime/chunk-FVDOXRUD.js +96 -0
- package/dist/node-runtime/chunk-GZQHMEWL.js +126 -0
- package/dist/node-runtime/chunk-H2EOZYMP.js +57 -0
- package/dist/node-runtime/chunk-HQCKFM2O.js +56 -0
- package/dist/node-runtime/chunk-HYHJIRDW.js +19 -0
- package/dist/node-runtime/chunk-I6R76AG7.js +22 -0
- package/dist/node-runtime/chunk-IPA627GP.js +296 -0
- package/dist/node-runtime/chunk-JHYKXIMM.js +378 -0
- package/dist/node-runtime/chunk-KKSIVROT.js +88 -0
- package/dist/node-runtime/chunk-LZAW4ZX3.js +633 -0
- package/dist/node-runtime/chunk-M3LZLQW2.js +98 -0
- package/dist/node-runtime/chunk-MGWCVY6Y.js +47 -0
- package/dist/node-runtime/chunk-MJQDX3BA.js +72 -0
- package/dist/node-runtime/chunk-MKKH27PH.js +8956 -0
- package/dist/node-runtime/chunk-MX3GJNXJ.js +986 -0
- package/dist/node-runtime/chunk-N77MY65L.js +966 -0
- package/dist/node-runtime/chunk-NUIFPZWX.js +209 -0
- package/dist/node-runtime/chunk-OBMWP3I5.js +68 -0
- package/dist/node-runtime/chunk-OR4L3IB2.js +42 -0
- package/dist/node-runtime/chunk-PBC2Y6GS.js +903 -0
- package/dist/node-runtime/chunk-PCGNAY2O.js +1324 -0
- package/dist/node-runtime/chunk-PCIGF2XK.js +250 -0
- package/dist/node-runtime/chunk-PGAJ5MO5.js +7558 -0
- package/dist/node-runtime/chunk-PL6JKXWI.js +3622 -0
- package/dist/node-runtime/chunk-PVCDKGR6.js +55 -0
- package/dist/node-runtime/chunk-Q4Q6D4IY.js +364 -0
- package/dist/node-runtime/chunk-Q5ZU2VRT.js +83 -0
- package/dist/node-runtime/chunk-QFWG2RVL.js +5477 -0
- package/dist/node-runtime/chunk-QH42CDX3.js +9451 -0
- package/dist/node-runtime/chunk-RQ2Q5IYR.js +72 -0
- package/dist/node-runtime/chunk-RQ5DNUNG.js +429 -0
- package/dist/node-runtime/chunk-RRIZE6QQ.js +17 -0
- package/dist/node-runtime/chunk-SN6G4OU6.js +4753 -0
- package/dist/node-runtime/chunk-SWSN6HAB.js +53 -0
- package/dist/node-runtime/chunk-TQNSPERU.js +949 -0
- package/dist/node-runtime/chunk-VRB2IYGG.js +53 -0
- package/dist/node-runtime/chunk-VSELYKWU.js +772 -0
- package/dist/node-runtime/chunk-W3CVH5RX.js +62 -0
- package/dist/node-runtime/chunk-WJVFOIB5.js +24921 -0
- package/dist/node-runtime/chunk-WOVTOV5P.js +5653 -0
- package/dist/node-runtime/chunk-XBWPBZBN.js +125 -0
- package/dist/node-runtime/chunk-XTHQ7XR7.js +509 -0
- package/dist/node-runtime/chunk-YF63P5ZT.js +864 -0
- package/dist/node-runtime/chunk-YSKSB3BJ.js +295 -0
- package/dist/node-runtime/chunk-ZCHMJ34K.js +130 -0
- package/dist/node-runtime/chunk-ZKMFUD3Q.js +2834 -0
- package/dist/node-runtime/cli-runtime.js +81 -0
- package/dist/node-runtime/clipboard-OZ63BBC6.js +12 -0
- package/dist/node-runtime/clipboard-image-7QR6GXVA.js +13 -0
- package/dist/node-runtime/command-L3LAHDIH.js +13 -0
- package/dist/node-runtime/commands-RL5YBW5I.js +291 -0
- package/dist/node-runtime/config-FMEOZSWE.js +77 -0
- package/dist/node-runtime/curator-server-XCM43VEF.js +3816 -0
- package/dist/node-runtime/customize-XEXL2DGQ.js +12 -0
- package/dist/node-runtime/direct-tool-executor-JVDXTO7A.js +188 -0
- package/dist/node-runtime/exa-LL64QZR2.js +329 -0
- package/dist/node-runtime/export-html-KR26FOUZ.js +150 -0
- package/dist/node-runtime/extension-input-RV5N6APL.js +14 -0
- package/dist/node-runtime/extension-selector-CPD5LWDD.js +14 -0
- package/dist/node-runtime/extract-XLQSJN4V.js +17 -0
- package/dist/node-runtime/file-sync-HCOP2QVF.js +117 -0
- package/dist/node-runtime/gateway-X2TI5X7F.js +2484 -0
- package/dist/node-runtime/gemini-api-QE7ANVDZ.js +29 -0
- package/dist/node-runtime/gemini-search-U7SUMPQA.js +330 -0
- package/dist/node-runtime/gemini-url-context-PXWCBQHK.js +103 -0
- package/dist/node-runtime/gemini-web-MM446SEH.js +14 -0
- package/dist/node-runtime/github-extract-F64RZOEA.js +619 -0
- package/dist/node-runtime/goal-CZQ7FFQJ.js +1831 -0
- package/dist/node-runtime/index.js +859 -0
- package/dist/node-runtime/init-NJV2T55N.js +28 -0
- package/dist/node-runtime/install-cli-TH7IJJQQ.js +308 -0
- package/dist/node-runtime/interactive-mode-KOM35YF2.js +56 -0
- package/dist/node-runtime/interactive-view-U6OILKF3.js +249 -0
- package/dist/node-runtime/lsp-manager-3ASUSO4T.js +1104 -0
- package/dist/node-runtime/lunr-cron-YJ5UHYRD.js +268 -0
- package/dist/node-runtime/main.js +54 -0
- package/dist/node-runtime/mcp-auth-flow-QIXHW6QN.js +32 -0
- package/dist/node-runtime/mcp-panel-IIGMXQGP.js +501 -0
- package/dist/node-runtime/mcp-setup-panel-FEK7663I.js +355 -0
- package/dist/node-runtime/memory-cap-3S4XRLNP.js +20 -0
- package/dist/node-runtime/model-tiers-NJLHUEGK.js +20 -0
- package/dist/node-runtime/openai-search-B3MSNCQA.js +288 -0
- package/dist/node-runtime/package-manager-cli-TZZHJCVI.js +749 -0
- package/dist/node-runtime/parallel-AI2MNSCU.js +265 -0
- package/dist/node-runtime/parser-manager-TQHWYDH7.js +152 -0
- package/dist/node-runtime/pdf-extract-IPE6H4HP.js +57 -0
- package/dist/node-runtime/perplexity-QVJW3I35.js +12 -0
- package/dist/node-runtime/pi-intercom-EEMBGQID.js +2215 -0
- package/dist/node-runtime/pi-mcp-adapter-QFB37AVN.js +422 -0
- package/dist/node-runtime/pi-ollama-cloud-4VZRMULP.js +1440 -0
- package/dist/node-runtime/pi-prompt-template-model-4QT4LLXF.js +3946 -0
- package/dist/node-runtime/pi-subagents-7XJ4H53P.js +6023 -0
- package/dist/node-runtime/pi-web-access-DPGAVKOA.js +1864 -0
- package/dist/node-runtime/print-mode-VWIRFDAC.js +12 -0
- package/dist/node-runtime/proxy-modes-HEEFK4KC.js +685 -0
- package/dist/node-runtime/rpc-entry.js +62 -0
- package/dist/node-runtime/rpc-mode-SJNSOA4G.js +13 -0
- package/dist/node-runtime/runtime-bridges-PPHNBFWL.js +14 -0
- package/dist/node-runtime/session-manager-MQNBYACL.js +35 -0
- package/dist/node-runtime/session-selector-5AZLJS6U.js +14 -0
- package/dist/node-runtime/settings-manager-5LF2VQWC.js +21 -0
- package/dist/node-runtime/src-Y7JSCK64.js +2407 -0
- package/dist/node-runtime/subagent-executor-5RNDEPIJ.js +8483 -0
- package/dist/node-runtime/summary-review-YSMXKITO.js +228 -0
- package/dist/node-runtime/tavily-WEZFVFP7.js +154 -0
- package/dist/node-runtime/timings-UAD5BEAS.js +16 -0
- package/dist/node-runtime/tool-renderer-MV5O452K.js +204 -0
- package/dist/node-runtime/update-cli-A253SZ7P.js +73 -0
- package/dist/node-runtime/v4-5MNC3H7G.js +424 -0
- package/dist/node-runtime/video-extract-OYT5WKS5.js +304 -0
- package/dist/node-runtime/workspace-index-6DBXSTJ6.js +163 -0
- package/dist/node-runtime/youtube-extract-TQYKGFGS.js +264 -0
- package/dist/startup/benchmark-request.d.ts +3 -0
- package/dist/startup/benchmark-request.d.ts.map +1 -0
- package/dist/startup/benchmark-request.js +65 -0
- package/dist/startup/benchmark-request.js.map +1 -0
- package/dist/startup/interactive-view.d.ts +55 -0
- package/dist/startup/interactive-view.d.ts.map +1 -0
- package/dist/startup/interactive-view.js +332 -0
- package/dist/startup/interactive-view.js.map +1 -0
- package/dist/startup/launch-routing.d.ts.map +1 -1
- package/dist/startup/launch-routing.js +9 -10
- package/dist/startup/launch-routing.js.map +1 -1
- package/dist/startup/startup-milestones.d.ts +1 -1
- package/dist/startup/startup-milestones.d.ts.map +1 -1
- package/dist/startup/startup-milestones.js +3 -2
- package/dist/startup/startup-milestones.js.map +1 -1
- package/dist/utils/await-with-abort.d.ts +2 -0
- package/dist/utils/await-with-abort.d.ts.map +1 -0
- package/dist/utils/await-with-abort.js +19 -0
- package/dist/utils/await-with-abort.js.map +1 -0
- package/dist/utils/syntax-highlight.d.ts.map +1 -1
- package/dist/utils/syntax-highlight.js +10 -4
- package/dist/utils/syntax-highlight.js.map +1 -1
- package/docs/compaction.md +5 -3
- package/docs/development.md +5 -1
- package/docs/features.md +0 -1
- package/docs/interactive-startup.md +98 -0
- package/docs/model-catalog.md +29 -0
- package/docs/usage.md +0 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +20 -20
- package/dist/core/swarm.d.ts +0 -23
- package/dist/core/swarm.d.ts.map +0 -1
- package/dist/core/swarm.js +0 -45
- package/dist/core/swarm.js.map +0 -1
|
@@ -0,0 +1,2484 @@
|
|
|
1
|
+
import { createRequire as __lunrRequire } from "node:module"; const require = __lunrRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
runWithOrigin,
|
|
4
|
+
splitMessage,
|
|
5
|
+
startGatewayCron
|
|
6
|
+
} from "./chunk-ECD5L7YI.js";
|
|
7
|
+
import {
|
|
8
|
+
gatewayConfigPath,
|
|
9
|
+
isFeatureEnabled,
|
|
10
|
+
loadGatewayConfig,
|
|
11
|
+
platformConfigFor,
|
|
12
|
+
resolvePlatformToken,
|
|
13
|
+
saveGatewayConfig
|
|
14
|
+
} from "./chunk-RQ5DNUNG.js";
|
|
15
|
+
import "./chunk-CJECDQKP.js";
|
|
16
|
+
import {
|
|
17
|
+
computeContextBreakdown
|
|
18
|
+
} from "./chunk-A2KQXUJD.js";
|
|
19
|
+
import {
|
|
20
|
+
findExactModelReferenceMatch
|
|
21
|
+
} from "./chunk-TQNSPERU.js";
|
|
22
|
+
import "./chunk-CGBXPL3E.js";
|
|
23
|
+
import {
|
|
24
|
+
processImage
|
|
25
|
+
} from "./chunk-AD6VXAO7.js";
|
|
26
|
+
import {
|
|
27
|
+
NO_HANDLER_REASON,
|
|
28
|
+
createPermissionContext,
|
|
29
|
+
deletePermissionContext,
|
|
30
|
+
registerApprovalHandler
|
|
31
|
+
} from "./chunk-LZAW4ZX3.js";
|
|
32
|
+
import "./chunk-M3LZLQW2.js";
|
|
33
|
+
import {
|
|
34
|
+
SessionManager
|
|
35
|
+
} from "./chunk-PBC2Y6GS.js";
|
|
36
|
+
import "./chunk-FVDOXRUD.js";
|
|
37
|
+
import "./chunk-RQ2Q5IYR.js";
|
|
38
|
+
import "./chunk-DDGMHUOW.js";
|
|
39
|
+
import {
|
|
40
|
+
getAgentDir
|
|
41
|
+
} from "./chunk-PCIGF2XK.js";
|
|
42
|
+
import "./chunk-Q4Q6D4IY.js";
|
|
43
|
+
import "./chunk-BRJX2PRC.js";
|
|
44
|
+
|
|
45
|
+
// packages/coding-agent/dist/gateway/adapters/discord.js
|
|
46
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, Client, Events, GatewayIntentBits } from "discord.js";
|
|
47
|
+
var DEFAULT_DEBOUNCE_MS = 600, TYPING_INTERVAL_MS = 8e3, TYPING_SAFETY_MS = 12e4, REPLY_TO_TEXT_MAX = 500, THREAD_NAME_MAX = 40, SEEN_CAP = 500, SENT_CHANNEL_CAP = 500, CALLBACK_INTERACTION_CAP = 500, CHANNEL_CACHE_CAP = 500, PARTICIPATING_THREAD_CAP = 500, ERR_INVALID_FORM_BODY = 50035, ERR_UNKNOWN_MESSAGE = 10008, THREAD_TYPES = [
|
|
48
|
+
ChannelType.PublicThread,
|
|
49
|
+
ChannelType.PrivateThread,
|
|
50
|
+
ChannelType.AnnouncementThread
|
|
51
|
+
];
|
|
52
|
+
function errMessage(err) {
|
|
53
|
+
return err instanceof Error ? err.message : String(err);
|
|
54
|
+
}
|
|
55
|
+
async function defaultDownloadAttachment(url) {
|
|
56
|
+
let res = await fetch(url);
|
|
57
|
+
if (!res.ok)
|
|
58
|
+
throw new Error(`attachment download HTTP ${res.status}`);
|
|
59
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
60
|
+
}
|
|
61
|
+
function discordErrorCode(err) {
|
|
62
|
+
let code = err?.code;
|
|
63
|
+
return typeof code == "number" ? code : void 0;
|
|
64
|
+
}
|
|
65
|
+
function isImageContentType(contentType) {
|
|
66
|
+
return typeof contentType == "string" && contentType.split(";")[0]?.trim().toLowerCase().startsWith("image/");
|
|
67
|
+
}
|
|
68
|
+
function collectDiscordImageAttachments(message) {
|
|
69
|
+
return (message.attachments ?? []).filter((attachment) => isImageContentType(attachment.contentType));
|
|
70
|
+
}
|
|
71
|
+
function messageToEvent(message, botUser, options = {}) {
|
|
72
|
+
let author = message.author;
|
|
73
|
+
if (!author || author.id === botUser.id || author.bot || message.system)
|
|
74
|
+
return null;
|
|
75
|
+
let content = (message.content ?? "").trim(), imageAttachments = collectDiscordImageAttachments(message);
|
|
76
|
+
if (content === "" && imageAttachments.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
let channel = message.channel, ignored = options.ignoredChannels ?? [];
|
|
79
|
+
if (ignored.includes(channel.id) || channel.parentId != null && ignored.includes(channel.parentId))
|
|
80
|
+
return null;
|
|
81
|
+
let chatType, chatId, threadId;
|
|
82
|
+
if (channel.type === ChannelType.DM)
|
|
83
|
+
chatType = "dm", chatId = channel.id;
|
|
84
|
+
else if (THREAD_TYPES.includes(channel.type))
|
|
85
|
+
chatType = "thread", chatId = channel.parentId ?? channel.id, threadId = channel.id;
|
|
86
|
+
else if (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement)
|
|
87
|
+
chatType = "group", chatId = channel.id;
|
|
88
|
+
else
|
|
89
|
+
return null;
|
|
90
|
+
let explicitMention = message.mentions?.has(botUser) === !0, participating = threadId !== void 0 && options.participatingThreads?.has(threadId) === !0, mentionedBot2 = explicitMention || participating;
|
|
91
|
+
if (!mentionedBot2 && chatType !== "dm" && [...content.matchAll(/<@!?(\d+)>/g)].map((m) => m[1]).some((id) => id !== botUser.id))
|
|
92
|
+
return null;
|
|
93
|
+
let text = content;
|
|
94
|
+
explicitMention && (text = text.replace(new RegExp(`<@!?${botUser.id}>`, "g"), "").trim());
|
|
95
|
+
let refId = message.reference?.messageId, cachedReply = refId != null ? channel.messages?.cache?.get(refId)?.content : void 0;
|
|
96
|
+
return {
|
|
97
|
+
text,
|
|
98
|
+
messageId: message.id,
|
|
99
|
+
replyToText: cachedReply !== void 0 ? cachedReply.slice(0, REPLY_TO_TEXT_MAX) : void 0,
|
|
100
|
+
source: {
|
|
101
|
+
platform: "discord",
|
|
102
|
+
chatId,
|
|
103
|
+
chatType,
|
|
104
|
+
userId: author.id,
|
|
105
|
+
userName: author.username,
|
|
106
|
+
threadId
|
|
107
|
+
},
|
|
108
|
+
metadata: mentionedBot2 ? { mentionedBot: !0 } : void 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function mapDiscordChannel(channel) {
|
|
112
|
+
return channel.type === ChannelType.DM ? { chatType: "dm", chatId: channel.id } : THREAD_TYPES.includes(channel.type) ? { chatType: "thread", chatId: channel.parentId ?? channel.id, threadId: channel.id } : channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement ? { chatType: "group", chatId: channel.id } : null;
|
|
113
|
+
}
|
|
114
|
+
function buttonInteractionToEvent(interaction) {
|
|
115
|
+
let channel = interaction.message.channel, mapped = mapDiscordChannel(channel);
|
|
116
|
+
return mapped ? {
|
|
117
|
+
id: interaction.id,
|
|
118
|
+
chatId: mapped.chatId,
|
|
119
|
+
messageId: interaction.message.id,
|
|
120
|
+
userId: interaction.user.id,
|
|
121
|
+
userName: interaction.user.username,
|
|
122
|
+
data: interaction.customId,
|
|
123
|
+
threadId: mapped.threadId
|
|
124
|
+
} : null;
|
|
125
|
+
}
|
|
126
|
+
function buildDiscordComponents(buttons) {
|
|
127
|
+
let rows = [];
|
|
128
|
+
for (let row of buttons) {
|
|
129
|
+
let actionRow = new ActionRowBuilder();
|
|
130
|
+
for (let button of row) {
|
|
131
|
+
let style = button.data === "" ? ButtonStyle.Secondary : ButtonStyle.Secondary, builder = new ButtonBuilder().setCustomId(button.data).setLabel(button.label).setStyle(style);
|
|
132
|
+
button.data === "" && builder.setDisabled(!0), actionRow.addComponents(builder);
|
|
133
|
+
}
|
|
134
|
+
rows.push(actionRow);
|
|
135
|
+
}
|
|
136
|
+
return rows;
|
|
137
|
+
}
|
|
138
|
+
var seenMessageIds = /* @__PURE__ */ new Set();
|
|
139
|
+
function markMessageSeen(id) {
|
|
140
|
+
if (seenMessageIds.has(id))
|
|
141
|
+
return !0;
|
|
142
|
+
if (seenMessageIds.add(id), seenMessageIds.size > SEEN_CAP) {
|
|
143
|
+
let oldest = seenMessageIds.values().next().value;
|
|
144
|
+
oldest !== void 0 && seenMessageIds.delete(oldest);
|
|
145
|
+
}
|
|
146
|
+
return !1;
|
|
147
|
+
}
|
|
148
|
+
var defaultScheduler = {
|
|
149
|
+
setTimeout: (fn, ms) => {
|
|
150
|
+
let handle = setTimeout(fn, ms);
|
|
151
|
+
return handle.unref?.(), handle;
|
|
152
|
+
},
|
|
153
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
154
|
+
setInterval: (fn, ms) => {
|
|
155
|
+
let handle = setInterval(fn, ms);
|
|
156
|
+
return handle.unref?.(), handle;
|
|
157
|
+
},
|
|
158
|
+
clearInterval: (handle) => clearInterval(handle)
|
|
159
|
+
};
|
|
160
|
+
function defaultClientFactory() {
|
|
161
|
+
return new Client({
|
|
162
|
+
intents: [
|
|
163
|
+
GatewayIntentBits.Guilds,
|
|
164
|
+
GatewayIntentBits.GuildMessages,
|
|
165
|
+
GatewayIntentBits.DirectMessages,
|
|
166
|
+
GatewayIntentBits.MessageContent
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function typingKey(chatId, threadId) {
|
|
171
|
+
return `${chatId}:${threadId ?? ""}`;
|
|
172
|
+
}
|
|
173
|
+
var DiscordAdapter = class {
|
|
174
|
+
platform = "discord";
|
|
175
|
+
maxMessageLength = 2e3;
|
|
176
|
+
cfg;
|
|
177
|
+
clientFactory;
|
|
178
|
+
downloadAttachment;
|
|
179
|
+
maxMediaBytes;
|
|
180
|
+
scheduler;
|
|
181
|
+
debounceMs;
|
|
182
|
+
now;
|
|
183
|
+
client;
|
|
184
|
+
botUserId;
|
|
185
|
+
handler;
|
|
186
|
+
callbackHandler;
|
|
187
|
+
pending = /* @__PURE__ */ new Map();
|
|
188
|
+
typing = /* @__PURE__ */ new Map();
|
|
189
|
+
channelCache = /* @__PURE__ */ new Map();
|
|
190
|
+
/** Threads the bot has spoken in (session memory only; hermes persists this). */
|
|
191
|
+
participatingThreads = /* @__PURE__ */ new Set();
|
|
192
|
+
/** messageId → channel it was sent to, so editMessage finds thread messages. */
|
|
193
|
+
sentMessageChannels = /* @__PURE__ */ new Map();
|
|
194
|
+
/** callbackId → interaction, so answerCallback can acknowledge the click. */
|
|
195
|
+
callbackInteractions = /* @__PURE__ */ new Map();
|
|
196
|
+
constructor(cfg, options = {}) {
|
|
197
|
+
if (this.cfg = cfg, this.scheduler = options.scheduler ?? defaultScheduler, this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS, this.now = options.now ?? Date.now, options.clientFactory)
|
|
198
|
+
this.clientFactory = options.clientFactory;
|
|
199
|
+
else {
|
|
200
|
+
if (!cfg.token)
|
|
201
|
+
throw new Error("DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)");
|
|
202
|
+
this.clientFactory = defaultClientFactory;
|
|
203
|
+
}
|
|
204
|
+
this.downloadAttachment = options.downloadAttachment ?? defaultDownloadAttachment, this.maxMediaBytes = options.maxMediaBytes ?? 10 * 1024 * 1024;
|
|
205
|
+
}
|
|
206
|
+
onMessage(handler) {
|
|
207
|
+
this.handler = handler;
|
|
208
|
+
}
|
|
209
|
+
onCallback(handler) {
|
|
210
|
+
this.callbackHandler = handler;
|
|
211
|
+
}
|
|
212
|
+
async connect() {
|
|
213
|
+
if (!this.cfg.token)
|
|
214
|
+
throw new Error("DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)");
|
|
215
|
+
let client = this.clientFactory();
|
|
216
|
+
this.client = client, client.on(Events.MessageCreate, (message) => {
|
|
217
|
+
this.handleMessage(message).catch((err) => {
|
|
218
|
+
console.error(`[gateway] discord message handling error: ${errMessage(err)}`);
|
|
219
|
+
});
|
|
220
|
+
}), client.on(Events.InteractionCreate, (interaction) => {
|
|
221
|
+
this.handleInteraction(interaction).catch((err) => {
|
|
222
|
+
console.error(`[gateway] discord interaction handling error: ${errMessage(err)}`);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
let ready = new Promise((resolve) => client.once(Events.ClientReady, () => resolve()));
|
|
226
|
+
try {
|
|
227
|
+
await client.login(this.cfg.token);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return console.error(`[gateway] discord login failed: ${errMessage(err)}`), !1;
|
|
230
|
+
}
|
|
231
|
+
return await ready, this.botUserId = client.user?.id, this.botUserId === void 0 ? (console.error("[gateway] discord: client ready without a user \u2014 cannot map messages"), !1) : !0;
|
|
232
|
+
}
|
|
233
|
+
async disconnect() {
|
|
234
|
+
for (let key of [...this.pending.keys()])
|
|
235
|
+
this.cancelPending(key);
|
|
236
|
+
for (let key of [...this.typing.keys()])
|
|
237
|
+
this.stopTyping(key);
|
|
238
|
+
this.channelCache.clear(), this.callbackInteractions.clear(), this.client?.destroy(), this.client = void 0;
|
|
239
|
+
}
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// Inbound dispatch + debounce
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
async handleMessage(message) {
|
|
244
|
+
if (!this.botUserId || !this.handler || markMessageSeen(message.id))
|
|
245
|
+
return;
|
|
246
|
+
let event = messageToEvent(message, { id: this.botUserId }, {
|
|
247
|
+
ignoredChannels: this.cfg.ignoredChannels,
|
|
248
|
+
participatingThreads: this.participatingThreads
|
|
249
|
+
});
|
|
250
|
+
event && (await this.resolveReplyToText(message, event), await this.maybeAutoThread(message, event), await this.downloadAttachments(message, event), this.dispatchEvent(event));
|
|
251
|
+
}
|
|
252
|
+
/** lunr: download + base64-encode inbound image attachments for one Discord message. */
|
|
253
|
+
async downloadAttachments(message, event) {
|
|
254
|
+
let imageAttachments = collectDiscordImageAttachments(message);
|
|
255
|
+
if (imageAttachments.length === 0)
|
|
256
|
+
return;
|
|
257
|
+
let attachments = [];
|
|
258
|
+
for (let attachment of imageAttachments)
|
|
259
|
+
if (!(attachment.size !== void 0 && attachment.size > this.maxMediaBytes))
|
|
260
|
+
try {
|
|
261
|
+
let bytes = await this.downloadAttachment(attachment.url);
|
|
262
|
+
if (bytes.byteLength > this.maxMediaBytes)
|
|
263
|
+
continue;
|
|
264
|
+
attachments.push({
|
|
265
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
266
|
+
mimeType: attachment.contentType ?? "image/png",
|
|
267
|
+
...attachment.name ? { filename: attachment.name } : {}
|
|
268
|
+
});
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
attachments.length > 0 && (event.attachments = attachments);
|
|
272
|
+
}
|
|
273
|
+
async handleInteraction(interaction) {
|
|
274
|
+
if (!this.callbackHandler || !interaction.isButton())
|
|
275
|
+
return;
|
|
276
|
+
let event = buttonInteractionToEvent(interaction);
|
|
277
|
+
if (event) {
|
|
278
|
+
if (this.callbackInteractions.set(interaction.id, interaction), this.callbackInteractions.size > CALLBACK_INTERACTION_CAP) {
|
|
279
|
+
let oldest = this.callbackInteractions.keys().next().value;
|
|
280
|
+
oldest !== void 0 && this.callbackInteractions.delete(oldest);
|
|
281
|
+
}
|
|
282
|
+
this.callbackHandler(event);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Best-effort: fill replyToText from the referenced message when it wasn't cached. */
|
|
286
|
+
async resolveReplyToText(message, event) {
|
|
287
|
+
if (event.replyToText !== void 0)
|
|
288
|
+
return;
|
|
289
|
+
let refId = message.reference?.messageId;
|
|
290
|
+
if (refId == null)
|
|
291
|
+
return;
|
|
292
|
+
let messages = message.channel.messages;
|
|
293
|
+
if (messages)
|
|
294
|
+
try {
|
|
295
|
+
let referenced = await messages.fetch(refId);
|
|
296
|
+
referenced?.content && (event.replyToText = referenced.content.slice(0, REPLY_TO_TEXT_MAX));
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** autoThread: spin a thread off a bot-mentioning guild-text message and re-target the event. */
|
|
301
|
+
async maybeAutoThread(message, event) {
|
|
302
|
+
if (this.cfg.autoThread && event.metadata?.mentionedBot === !0 && message.channel.type === ChannelType.GuildText && typeof message.startThread == "function")
|
|
303
|
+
try {
|
|
304
|
+
let thread = await message.startThread({
|
|
305
|
+
name: (event.text || "conversation").slice(0, THREAD_NAME_MAX),
|
|
306
|
+
autoArchiveDuration: 60
|
|
307
|
+
});
|
|
308
|
+
if (this.participatingThreads.add(thread.id), this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {
|
|
309
|
+
let oldest = this.participatingThreads.values().next().value;
|
|
310
|
+
oldest !== void 0 && this.participatingThreads.delete(oldest);
|
|
311
|
+
}
|
|
312
|
+
event.source = { ...event.source, chatType: "thread", chatId: message.channel.id, threadId: thread.id };
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
debounceKey(event) {
|
|
317
|
+
return `${event.source.chatId}:${event.source.threadId ?? ""}:${event.source.userId}`;
|
|
318
|
+
}
|
|
319
|
+
dispatchEvent(event) {
|
|
320
|
+
let key = this.debounceKey(event);
|
|
321
|
+
if (event.text.trimStart().startsWith("/")) {
|
|
322
|
+
this.flushPending(key), this.handler?.(event);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let existing = this.pending.get(key);
|
|
326
|
+
if (existing) {
|
|
327
|
+
this.scheduler.clearTimeout(existing.timer);
|
|
328
|
+
let mergedAttachments = [...existing.event.attachments ?? [], ...event.attachments ?? []];
|
|
329
|
+
existing.event = {
|
|
330
|
+
...event,
|
|
331
|
+
text: `${existing.event.text}
|
|
332
|
+
${event.text}`,
|
|
333
|
+
...mergedAttachments.length > 0 ? { attachments: mergedAttachments } : {}
|
|
334
|
+
}, existing.timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);
|
|
335
|
+
} else {
|
|
336
|
+
let timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);
|
|
337
|
+
this.pending.set(key, { event, timer });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
flushPending(key) {
|
|
341
|
+
let entry = this.pending.get(key);
|
|
342
|
+
entry && (this.scheduler.clearTimeout(entry.timer), this.pending.delete(key), this.handler?.(entry.event));
|
|
343
|
+
}
|
|
344
|
+
cancelPending(key) {
|
|
345
|
+
let entry = this.pending.get(key);
|
|
346
|
+
entry && (this.scheduler.clearTimeout(entry.timer), this.pending.delete(key));
|
|
347
|
+
}
|
|
348
|
+
// -----------------------------------------------------------------------
|
|
349
|
+
// Outbound
|
|
350
|
+
// -----------------------------------------------------------------------
|
|
351
|
+
async fetchChannel(id) {
|
|
352
|
+
if (this.channelCache.has(id))
|
|
353
|
+
return this.channelCache.get(id) ?? null;
|
|
354
|
+
if (!this.client)
|
|
355
|
+
return null;
|
|
356
|
+
try {
|
|
357
|
+
let channel = await this.client.channels.fetch(id);
|
|
358
|
+
if (this.channelCache.set(id, channel), this.channelCache.size > CHANNEL_CACHE_CAP) {
|
|
359
|
+
let oldest = this.channelCache.keys().next().value;
|
|
360
|
+
oldest !== void 0 && this.channelCache.delete(oldest);
|
|
361
|
+
}
|
|
362
|
+
return channel;
|
|
363
|
+
} catch {
|
|
364
|
+
if (this.channelCache.set(id, null), this.channelCache.size > CHANNEL_CACHE_CAP) {
|
|
365
|
+
let oldest = this.channelCache.keys().next().value;
|
|
366
|
+
oldest !== void 0 && this.channelCache.delete(oldest);
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
recordSent(messageId, channel) {
|
|
372
|
+
if (THREAD_TYPES.includes(channel.type) && this.participatingThreads.add(channel.id), this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {
|
|
373
|
+
let oldest = this.participatingThreads.values().next().value;
|
|
374
|
+
oldest !== void 0 && this.participatingThreads.delete(oldest);
|
|
375
|
+
}
|
|
376
|
+
if (this.sentMessageChannels.set(messageId, channel.id), this.sentMessageChannels.size > SENT_CHANNEL_CAP) {
|
|
377
|
+
let oldest = this.sentMessageChannels.keys().next().value;
|
|
378
|
+
oldest !== void 0 && this.sentMessageChannels.delete(oldest);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async trySend(channel, payload) {
|
|
382
|
+
if (typeof channel.send != "function")
|
|
383
|
+
return { success: !1, error: `channel ${channel.id} is not sendable` };
|
|
384
|
+
try {
|
|
385
|
+
let sent = await channel.send(payload);
|
|
386
|
+
return this.recordSent(sent.id, channel), { success: !0, messageId: sent.id };
|
|
387
|
+
} catch (err) {
|
|
388
|
+
return { success: !1, error: errMessage(err), retryable: discordErrorCode(err) !== ERR_INVALID_FORM_BODY };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async send(chatId, text, opts) {
|
|
392
|
+
this.stopTyping(typingKey(chatId, opts?.threadId));
|
|
393
|
+
let channel = await this.fetchChannel(opts?.threadId ?? chatId);
|
|
394
|
+
if (!channel)
|
|
395
|
+
return { success: !1, error: `channel ${opts?.threadId ?? chatId} not found` };
|
|
396
|
+
let result = await this.trySend(channel, {
|
|
397
|
+
content: text,
|
|
398
|
+
reply: opts?.replyTo !== void 0 ? { messageReference: opts.replyTo } : void 0
|
|
399
|
+
});
|
|
400
|
+
return !result.success && opts?.replyTo !== void 0 ? this.trySend(channel, { content: text }) : result;
|
|
401
|
+
}
|
|
402
|
+
async sendButtons(chatId, text, buttons, opts) {
|
|
403
|
+
this.stopTyping(typingKey(chatId, opts?.threadId));
|
|
404
|
+
let channel = await this.fetchChannel(opts?.threadId ?? chatId);
|
|
405
|
+
if (!channel)
|
|
406
|
+
return { success: !1, error: `channel ${opts?.threadId ?? chatId} not found` };
|
|
407
|
+
let result = await this.trySend(channel, {
|
|
408
|
+
content: text,
|
|
409
|
+
reply: opts?.replyTo !== void 0 ? { messageReference: opts.replyTo } : void 0,
|
|
410
|
+
components: buildDiscordComponents(buttons)
|
|
411
|
+
});
|
|
412
|
+
return !result.success && opts?.replyTo !== void 0 ? this.trySend(channel, { content: text, components: buildDiscordComponents(buttons) }) : result;
|
|
413
|
+
}
|
|
414
|
+
async answerCallback(id, text) {
|
|
415
|
+
let interaction = this.callbackInteractions.get(id);
|
|
416
|
+
if (interaction) {
|
|
417
|
+
this.callbackInteractions.delete(id);
|
|
418
|
+
try {
|
|
419
|
+
text !== void 0 && interaction.reply ? await interaction.reply({ content: text, ephemeral: !0 }) : await interaction.deferUpdate();
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async editMessage(chatId, messageId, text, buttons) {
|
|
425
|
+
let channel = await this.fetchChannel(this.sentMessageChannels.get(messageId) ?? chatId);
|
|
426
|
+
if (!channel?.messages)
|
|
427
|
+
return { success: !1, error: `channel ${this.sentMessageChannels.get(messageId) ?? chatId} not found` };
|
|
428
|
+
try {
|
|
429
|
+
let message = await channel.messages.fetch(messageId);
|
|
430
|
+
if (!message || typeof message.edit != "function")
|
|
431
|
+
return { success: !1, error: `message ${messageId} not found` };
|
|
432
|
+
let editOptions = { content: text };
|
|
433
|
+
return buttons !== void 0 && (editOptions.components = buildDiscordComponents(buttons)), await message.edit(editOptions), { success: !0, messageId };
|
|
434
|
+
} catch (err) {
|
|
435
|
+
return discordErrorCode(err) === ERR_UNKNOWN_MESSAGE ? { success: !1, error: errMessage(err) } : { success: !1, error: errMessage(err), retryable: !0 };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async sendTyping(chatId, threadId) {
|
|
439
|
+
let key = typingKey(chatId, threadId), targetId = threadId ?? chatId;
|
|
440
|
+
await this.doTyping(targetId);
|
|
441
|
+
let existing = this.typing.get(key);
|
|
442
|
+
if (existing) {
|
|
443
|
+
existing.lastRefresh = this.now();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
let interval = this.scheduler.setInterval(() => {
|
|
447
|
+
let entry = this.typing.get(key);
|
|
448
|
+
if (entry) {
|
|
449
|
+
if (this.now() - entry.lastRefresh > TYPING_SAFETY_MS) {
|
|
450
|
+
this.stopTyping(key);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
this.doTyping(targetId);
|
|
454
|
+
}
|
|
455
|
+
}, TYPING_INTERVAL_MS);
|
|
456
|
+
this.typing.set(key, { interval, lastRefresh: this.now() });
|
|
457
|
+
}
|
|
458
|
+
async doTyping(channelId) {
|
|
459
|
+
try {
|
|
460
|
+
await (await this.fetchChannel(channelId))?.sendTyping?.();
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
stopTyping(key) {
|
|
465
|
+
let entry = this.typing.get(key);
|
|
466
|
+
entry && (this.scheduler.clearInterval(entry.interval), this.typing.delete(key));
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// packages/coding-agent/dist/gateway/adapters/telegram.js
|
|
471
|
+
var API_BASE = "https://api.telegram.org";
|
|
472
|
+
var MARKDOWN_PARSE_ERROR_RE = /can't parse|parse entities/i, NOT_MODIFIED_RE = /message is not modified/i, TelegramApiError = class extends Error {
|
|
473
|
+
status;
|
|
474
|
+
retryAfter;
|
|
475
|
+
constructor(status, description, retryAfter) {
|
|
476
|
+
super(description), this.name = "TelegramApiError", this.status = status, this.retryAfter = retryAfter;
|
|
477
|
+
}
|
|
478
|
+
}, BACKOFF_DELAYS_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
479
|
+
function backoffDelayMs(consecutiveFailures) {
|
|
480
|
+
let index = Math.min(Math.max(consecutiveFailures, 0), BACKOFF_DELAYS_MS.length - 1);
|
|
481
|
+
return BACKOFF_DELAYS_MS[index];
|
|
482
|
+
}
|
|
483
|
+
var defaultScheduler2 = {
|
|
484
|
+
setTimeout: (fn, ms) => {
|
|
485
|
+
let handle = setTimeout(fn, ms);
|
|
486
|
+
return handle.unref?.(), handle;
|
|
487
|
+
},
|
|
488
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
489
|
+
setInterval: (fn, ms) => {
|
|
490
|
+
let handle = setInterval(fn, ms);
|
|
491
|
+
return handle.unref?.(), handle;
|
|
492
|
+
},
|
|
493
|
+
clearInterval: (handle) => clearInterval(handle)
|
|
494
|
+
};
|
|
495
|
+
async function defaultCallApi(token, method, body, signal) {
|
|
496
|
+
let res = await fetch(`${API_BASE}/bot${token}/${method}`, {
|
|
497
|
+
method: "POST",
|
|
498
|
+
headers: { "content-type": "application/json" },
|
|
499
|
+
body: JSON.stringify(body),
|
|
500
|
+
signal
|
|
501
|
+
}), data = await res.json().catch(() => {
|
|
502
|
+
});
|
|
503
|
+
if (res.status === 429)
|
|
504
|
+
throw new TelegramApiError(429, data?.description ?? "rate limited", data?.parameters?.retry_after);
|
|
505
|
+
if (!res.ok || !data?.ok)
|
|
506
|
+
throw new TelegramApiError(data?.error_code ?? res.status, data?.description ?? `HTTP ${res.status}`, data?.parameters?.retry_after);
|
|
507
|
+
return data.result;
|
|
508
|
+
}
|
|
509
|
+
async function defaultDownloadFile(token, filePath) {
|
|
510
|
+
let res = await fetch(`${API_BASE}/file/bot${token}/${filePath}`);
|
|
511
|
+
if (!res.ok)
|
|
512
|
+
throw new TelegramApiError(res.status, `file download HTTP ${res.status}`);
|
|
513
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
514
|
+
}
|
|
515
|
+
function errMessage2(err) {
|
|
516
|
+
return err instanceof Error ? err.message : String(err);
|
|
517
|
+
}
|
|
518
|
+
function chatTypeOf(chat) {
|
|
519
|
+
switch (chat.type) {
|
|
520
|
+
case "private":
|
|
521
|
+
return "dm";
|
|
522
|
+
case "group":
|
|
523
|
+
case "supergroup":
|
|
524
|
+
return "group";
|
|
525
|
+
case "channel":
|
|
526
|
+
return "channel";
|
|
527
|
+
default:
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function extractMention(text, entities, botInfo) {
|
|
532
|
+
if (!botInfo.botUsername)
|
|
533
|
+
return { text: text.trim(), mentioned: !1 };
|
|
534
|
+
let botAt = `@${botInfo.botUsername}`.toLowerCase(), removals = [], mentioned = !1;
|
|
535
|
+
for (let entity of entities) {
|
|
536
|
+
let slice = text.slice(entity.offset, entity.offset + entity.length);
|
|
537
|
+
if (entity.type === "mention" && slice.toLowerCase() === botAt)
|
|
538
|
+
mentioned = !0, removals.push([entity.offset, entity.offset + entity.length]);
|
|
539
|
+
else if (entity.type === "text_mention" && entity.user?.id === botInfo.botId)
|
|
540
|
+
mentioned = !0, removals.push([entity.offset, entity.offset + entity.length]);
|
|
541
|
+
else if (entity.type === "bot_command" && slice.toLowerCase().includes(botAt)) {
|
|
542
|
+
mentioned = !0;
|
|
543
|
+
let at = slice.toLowerCase().indexOf(botAt);
|
|
544
|
+
removals.push([entity.offset + at, entity.offset + at + botAt.length]);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (removals.length === 0)
|
|
548
|
+
return { text: text.trim(), mentioned };
|
|
549
|
+
removals.sort((a, b) => a[0] - b[0]);
|
|
550
|
+
let out = "", pos = 0;
|
|
551
|
+
for (let [start, end] of removals)
|
|
552
|
+
out += text.slice(pos, start), pos = Math.max(pos, end);
|
|
553
|
+
return out += text.slice(pos), { text: out.trim(), mentioned };
|
|
554
|
+
}
|
|
555
|
+
function isImageMimeType(mimeType) {
|
|
556
|
+
return typeof mimeType == "string" && mimeType.split(";")[0]?.trim().toLowerCase().startsWith("image/");
|
|
557
|
+
}
|
|
558
|
+
function collectTelegramMedia(message) {
|
|
559
|
+
let descriptors = [];
|
|
560
|
+
if (message.photo && message.photo.length > 0) {
|
|
561
|
+
let largest = message.photo[message.photo.length - 1];
|
|
562
|
+
descriptors.push({
|
|
563
|
+
fileId: largest.file_id,
|
|
564
|
+
mimeType: "image/jpeg",
|
|
565
|
+
...largest.file_size !== void 0 ? { fileSize: largest.file_size } : {}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
return message.document && isImageMimeType(message.document.mime_type) && descriptors.push({
|
|
569
|
+
fileId: message.document.file_id,
|
|
570
|
+
mimeType: message.document.mime_type ?? "image/jpeg",
|
|
571
|
+
...message.document.file_name ? { filename: message.document.file_name } : {},
|
|
572
|
+
...message.document.file_size !== void 0 ? { fileSize: message.document.file_size } : {}
|
|
573
|
+
}), descriptors;
|
|
574
|
+
}
|
|
575
|
+
function updateToEvent(update, botInfo) {
|
|
576
|
+
let message = update.message;
|
|
577
|
+
if (!message)
|
|
578
|
+
return null;
|
|
579
|
+
let from = message.from;
|
|
580
|
+
if (!from || from.id === botInfo.botId || from.is_bot)
|
|
581
|
+
return null;
|
|
582
|
+
let hasMedia = collectTelegramMedia(message).length > 0, rawText = message.text ?? message.caption;
|
|
583
|
+
if (!rawText && !hasMedia)
|
|
584
|
+
return null;
|
|
585
|
+
let rawEntities = message.text !== void 0 ? message.entities : message.caption_entities, chatType = chatTypeOf(message.chat);
|
|
586
|
+
if (chatType === null)
|
|
587
|
+
return null;
|
|
588
|
+
let threadId = message.chat.is_forum === !0 && message.message_thread_id !== void 0 && message.message_thread_id !== 1 ? String(message.message_thread_id) : void 0, { text, mentioned } = extractMention(rawText ?? "", rawEntities ?? [], botInfo), replyText = message.reply_to_message?.text;
|
|
589
|
+
return {
|
|
590
|
+
text,
|
|
591
|
+
messageId: String(message.message_id),
|
|
592
|
+
replyToText: replyText !== void 0 ? replyText.slice(0, 500) : void 0,
|
|
593
|
+
source: {
|
|
594
|
+
platform: "telegram",
|
|
595
|
+
chatId: String(message.chat.id),
|
|
596
|
+
chatType,
|
|
597
|
+
userId: String(from.id),
|
|
598
|
+
userName: from.username ?? from.first_name,
|
|
599
|
+
threadId
|
|
600
|
+
},
|
|
601
|
+
metadata: mentioned ? { mentionedBot: !0 } : void 0
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function callbackQueryToEvent(query) {
|
|
605
|
+
let message = query.message;
|
|
606
|
+
if (!message)
|
|
607
|
+
return null;
|
|
608
|
+
let from = query.from;
|
|
609
|
+
return chatTypeOf(message.chat) === null ? null : {
|
|
610
|
+
id: query.id,
|
|
611
|
+
chatId: String(message.chat.id),
|
|
612
|
+
messageId: String(message.message_id),
|
|
613
|
+
userId: String(from.id),
|
|
614
|
+
userName: from.username ?? from.first_name,
|
|
615
|
+
data: query.data ?? "",
|
|
616
|
+
threadId: message.chat.is_forum === !0 && message.message_thread_id !== void 0 && message.message_thread_id !== 1 ? String(message.message_thread_id) : void 0
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
var TelegramAdapter = class {
|
|
620
|
+
platform = "telegram";
|
|
621
|
+
maxMessageLength = 4096;
|
|
622
|
+
callApi;
|
|
623
|
+
downloadFile;
|
|
624
|
+
maxMediaBytes;
|
|
625
|
+
scheduler;
|
|
626
|
+
debounceMs;
|
|
627
|
+
now;
|
|
628
|
+
handler;
|
|
629
|
+
callbackHandler;
|
|
630
|
+
botInfo;
|
|
631
|
+
offset = 0;
|
|
632
|
+
abortController;
|
|
633
|
+
pending = /* @__PURE__ */ new Map();
|
|
634
|
+
typing = /* @__PURE__ */ new Map();
|
|
635
|
+
constructor(cfg, options = {}) {
|
|
636
|
+
if (this.scheduler = options.scheduler ?? defaultScheduler2, this.debounceMs = options.debounceMs ?? 300, this.now = options.now ?? Date.now, options.callApi)
|
|
637
|
+
this.callApi = options.callApi;
|
|
638
|
+
else {
|
|
639
|
+
if (!cfg.token)
|
|
640
|
+
throw new Error("TelegramAdapter: no bot token (set telegram.token or LUNR_TELEGRAM_BOT_TOKEN)");
|
|
641
|
+
let token = cfg.token;
|
|
642
|
+
this.callApi = (method, body, signal) => defaultCallApi(token, method, body, signal);
|
|
643
|
+
}
|
|
644
|
+
this.downloadFile = options.downloadFile ?? ((filePath) => defaultDownloadFile(cfg.token ?? "", filePath)), this.maxMediaBytes = options.maxMediaBytes ?? 10 * 1024 * 1024;
|
|
645
|
+
}
|
|
646
|
+
onMessage(handler) {
|
|
647
|
+
this.handler = handler;
|
|
648
|
+
}
|
|
649
|
+
onCallback(handler) {
|
|
650
|
+
this.callbackHandler = handler;
|
|
651
|
+
}
|
|
652
|
+
async connect() {
|
|
653
|
+
let me = await this.callApi("getMe", {});
|
|
654
|
+
return this.botInfo = { botId: me.id, botUsername: me.username ?? "" }, this.abortController = new AbortController(), this.pollLoop(this.abortController.signal), !0;
|
|
655
|
+
}
|
|
656
|
+
async disconnect() {
|
|
657
|
+
this.abortController?.abort();
|
|
658
|
+
for (let key of [...this.pending.keys()])
|
|
659
|
+
this.cancelPending(key);
|
|
660
|
+
for (let key of [...this.typing.keys()])
|
|
661
|
+
this.stopTyping(key);
|
|
662
|
+
}
|
|
663
|
+
// -----------------------------------------------------------------------
|
|
664
|
+
// Long-poll loop
|
|
665
|
+
// -----------------------------------------------------------------------
|
|
666
|
+
async pollLoop(signal) {
|
|
667
|
+
let failures = 0;
|
|
668
|
+
for (; !signal.aborted; )
|
|
669
|
+
try {
|
|
670
|
+
let updates = await this.callApi("getUpdates", { offset: this.offset, timeout: 30, allowed_updates: ["message", "callback_query"] }, signal);
|
|
671
|
+
failures = 0;
|
|
672
|
+
for (let update of updates)
|
|
673
|
+
this.offset = update.update_id + 1, await this.dispatchUpdate(update);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
if (signal.aborted)
|
|
676
|
+
return;
|
|
677
|
+
if (err instanceof TelegramApiError && err.status === 429) {
|
|
678
|
+
let retryAfter = Math.min(err.retryAfter ?? 1, 30);
|
|
679
|
+
await this.sleep(retryAfter * 1e3, signal);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
let delay = backoffDelayMs(failures);
|
|
683
|
+
failures += 1, console.error(`[gateway] telegram poll error (retry in ${delay}ms): ${errMessage2(err)}`), await this.sleep(delay, signal);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
sleep(ms, signal) {
|
|
687
|
+
return new Promise((resolve) => {
|
|
688
|
+
let timer = this.scheduler.setTimeout(resolve, ms);
|
|
689
|
+
signal?.addEventListener("abort", () => {
|
|
690
|
+
this.scheduler.clearTimeout(timer), resolve();
|
|
691
|
+
}, { once: !0 });
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
// -----------------------------------------------------------------------
|
|
695
|
+
// Inbound dispatch + debounce
|
|
696
|
+
// -----------------------------------------------------------------------
|
|
697
|
+
debounceKey(event) {
|
|
698
|
+
return `${event.source.chatId}:${event.source.threadId ?? ""}:${event.source.userId}`;
|
|
699
|
+
}
|
|
700
|
+
async dispatchUpdate(update) {
|
|
701
|
+
if (!this.botInfo)
|
|
702
|
+
return;
|
|
703
|
+
if (update.callback_query) {
|
|
704
|
+
let event2 = callbackQueryToEvent(update.callback_query);
|
|
705
|
+
event2 && this.callbackHandler?.(event2);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (!this.handler)
|
|
709
|
+
return;
|
|
710
|
+
let event = updateToEvent(update, this.botInfo);
|
|
711
|
+
if (!event)
|
|
712
|
+
return;
|
|
713
|
+
if (update.message) {
|
|
714
|
+
let attachments = await this.downloadMedia(update.message);
|
|
715
|
+
attachments.length > 0 && (event.attachments = attachments);
|
|
716
|
+
}
|
|
717
|
+
let key = this.debounceKey(event);
|
|
718
|
+
if (event.text.trimStart().startsWith("/")) {
|
|
719
|
+
this.flushPending(key), this.handler(event);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
let existing = this.pending.get(key);
|
|
723
|
+
if (existing) {
|
|
724
|
+
this.scheduler.clearTimeout(existing.timer);
|
|
725
|
+
let mergedAttachments = [...existing.event.attachments ?? [], ...event.attachments ?? []];
|
|
726
|
+
existing.event = {
|
|
727
|
+
...event,
|
|
728
|
+
text: `${existing.event.text}
|
|
729
|
+
${event.text}`,
|
|
730
|
+
...mergedAttachments.length > 0 ? { attachments: mergedAttachments } : {}
|
|
731
|
+
}, existing.timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);
|
|
732
|
+
} else {
|
|
733
|
+
let timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);
|
|
734
|
+
this.pending.set(key, { event, timer });
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** lunr: download + base64-encode inbound image media for one Telegram message. */
|
|
738
|
+
async downloadMedia(message) {
|
|
739
|
+
let descriptors = collectTelegramMedia(message);
|
|
740
|
+
if (descriptors.length === 0)
|
|
741
|
+
return [];
|
|
742
|
+
let attachments = [];
|
|
743
|
+
for (let descriptor of descriptors)
|
|
744
|
+
if (!(descriptor.fileSize !== void 0 && descriptor.fileSize > this.maxMediaBytes))
|
|
745
|
+
try {
|
|
746
|
+
let fileResult = await this.callApi("getFile", { file_id: descriptor.fileId }), filePath = fileResult?.file_path;
|
|
747
|
+
if (!filePath || fileResult?.file_size !== void 0 && fileResult.file_size > this.maxMediaBytes)
|
|
748
|
+
continue;
|
|
749
|
+
let bytes = await this.downloadFile(filePath);
|
|
750
|
+
if (bytes.byteLength > this.maxMediaBytes)
|
|
751
|
+
continue;
|
|
752
|
+
attachments.push({
|
|
753
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
754
|
+
mimeType: descriptor.mimeType,
|
|
755
|
+
...descriptor.filename ? { filename: descriptor.filename } : {}
|
|
756
|
+
});
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
return attachments;
|
|
760
|
+
}
|
|
761
|
+
flushPending(key) {
|
|
762
|
+
let entry = this.pending.get(key);
|
|
763
|
+
entry && (this.scheduler.clearTimeout(entry.timer), this.pending.delete(key), this.handler?.(entry.event));
|
|
764
|
+
}
|
|
765
|
+
cancelPending(key) {
|
|
766
|
+
let entry = this.pending.get(key);
|
|
767
|
+
entry && (this.scheduler.clearTimeout(entry.timer), this.pending.delete(key));
|
|
768
|
+
}
|
|
769
|
+
// -----------------------------------------------------------------------
|
|
770
|
+
// Outbound
|
|
771
|
+
// -----------------------------------------------------------------------
|
|
772
|
+
buildSendBody(chatId, text, opts) {
|
|
773
|
+
let body = { chat_id: chatId, text, parse_mode: "Markdown" };
|
|
774
|
+
return opts?.threadId !== void 0 && (body.message_thread_id = Number(opts.threadId)), opts?.replyTo !== void 0 && (body.reply_parameters = { message_id: Number(opts.replyTo) }), body;
|
|
775
|
+
}
|
|
776
|
+
buildReplyMarkup(buttons) {
|
|
777
|
+
if (buttons !== void 0)
|
|
778
|
+
return {
|
|
779
|
+
inline_keyboard: buttons.map((row) => row.map((b) => ({ text: b.label, callback_data: b.data })))
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
async send(chatId, text, opts) {
|
|
783
|
+
this.stopTyping(typingKey2(chatId, opts?.threadId));
|
|
784
|
+
let body = this.buildSendBody(chatId, text, opts);
|
|
785
|
+
return this.callSend(body);
|
|
786
|
+
}
|
|
787
|
+
async sendButtons(chatId, text, buttons, opts) {
|
|
788
|
+
this.stopTyping(typingKey2(chatId, opts?.threadId));
|
|
789
|
+
let body = this.buildSendBody(chatId, text, opts), markup = this.buildReplyMarkup(buttons);
|
|
790
|
+
return markup && (body.reply_markup = markup), this.callSend(body);
|
|
791
|
+
}
|
|
792
|
+
async answerCallback(id, text) {
|
|
793
|
+
let body = { callback_query_id: id };
|
|
794
|
+
text !== void 0 && (body.text = text), await this.callApi("answerCallbackQuery", body).catch(() => {
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
/** lunr: register the bot's slash-command menu (Telegram client autocomplete). Best-effort — caller catches. */
|
|
798
|
+
async registerCommands(commands) {
|
|
799
|
+
let valid = commands.filter((c) => /^[a-z0-9_]{1,32}$/.test(c.name) && c.description.trim().length > 0).map((c) => ({ command: c.name, description: c.description.trim().slice(0, 256) }));
|
|
800
|
+
valid.length !== 0 && await this.callApi("setMyCommands", { commands: valid });
|
|
801
|
+
}
|
|
802
|
+
async callSend(body) {
|
|
803
|
+
try {
|
|
804
|
+
let result = await this.callApi("sendMessage", body);
|
|
805
|
+
return { success: !0, messageId: String(result.message_id) };
|
|
806
|
+
} catch (err) {
|
|
807
|
+
if (err instanceof TelegramApiError && err.status === 400 && body.parse_mode !== void 0 && MARKDOWN_PARSE_ERROR_RE.test(err.message)) {
|
|
808
|
+
let { parse_mode: _dropped, ...plain } = body;
|
|
809
|
+
return this.callSend(plain);
|
|
810
|
+
}
|
|
811
|
+
if (err instanceof TelegramApiError && err.status === 429) {
|
|
812
|
+
await this.sleep((err.retryAfter ?? 1) * 1e3);
|
|
813
|
+
try {
|
|
814
|
+
let result = await this.callApi("sendMessage", body);
|
|
815
|
+
return { success: !0, messageId: String(result.message_id) };
|
|
816
|
+
} catch (retryErr) {
|
|
817
|
+
return { success: !1, error: errMessage2(retryErr), retryable: !0 };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
success: !1,
|
|
822
|
+
error: errMessage2(err),
|
|
823
|
+
retryable: !(err instanceof TelegramApiError) || err.status >= 500
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async editMessage(chatId, messageId, text, buttons) {
|
|
828
|
+
let body = {
|
|
829
|
+
chat_id: chatId,
|
|
830
|
+
message_id: Number(messageId),
|
|
831
|
+
text,
|
|
832
|
+
parse_mode: "Markdown"
|
|
833
|
+
}, markup = this.buildReplyMarkup(buttons);
|
|
834
|
+
markup !== void 0 && (body.reply_markup = markup);
|
|
835
|
+
try {
|
|
836
|
+
return await this.callApi("editMessageText", body), { success: !0, messageId };
|
|
837
|
+
} catch (err) {
|
|
838
|
+
if (err instanceof TelegramApiError && err.status === 400) {
|
|
839
|
+
if (NOT_MODIFIED_RE.test(err.message))
|
|
840
|
+
return { success: !0, messageId };
|
|
841
|
+
if (MARKDOWN_PARSE_ERROR_RE.test(err.message) && body.parse_mode !== void 0) {
|
|
842
|
+
let { parse_mode: _dropped, ...plain } = body;
|
|
843
|
+
try {
|
|
844
|
+
return await this.callApi("editMessageText", plain), { success: !0, messageId };
|
|
845
|
+
} catch (retryErr) {
|
|
846
|
+
return { success: !1, error: errMessage2(retryErr) };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
success: !1,
|
|
852
|
+
error: errMessage2(err),
|
|
853
|
+
retryable: !(err instanceof TelegramApiError) || err.status >= 500
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async sendTyping(chatId, threadId) {
|
|
858
|
+
let key = typingKey2(chatId, threadId), body = { chat_id: chatId, action: "typing" };
|
|
859
|
+
threadId !== void 0 && (body.message_thread_id = Number(threadId)), await this.callApi("sendChatAction", body).catch(() => {
|
|
860
|
+
});
|
|
861
|
+
let existing = this.typing.get(key);
|
|
862
|
+
if (existing) {
|
|
863
|
+
existing.lastRefresh = this.now();
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
let interval = this.scheduler.setInterval(() => {
|
|
867
|
+
let entry = this.typing.get(key);
|
|
868
|
+
if (entry) {
|
|
869
|
+
if (this.now() - entry.lastRefresh > 12e4) {
|
|
870
|
+
this.stopTyping(key);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
this.callApi("sendChatAction", body).catch(() => {
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}, 4e3);
|
|
877
|
+
this.typing.set(key, { interval, lastRefresh: this.now() });
|
|
878
|
+
}
|
|
879
|
+
stopTyping(key) {
|
|
880
|
+
let entry = this.typing.get(key);
|
|
881
|
+
entry && (this.scheduler.clearInterval(entry.interval), this.typing.delete(key));
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
function typingKey2(chatId, threadId) {
|
|
885
|
+
return `${chatId}:${threadId ?? ""}`;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// packages/coding-agent/dist/gateway/agent-bridge.js
|
|
889
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
890
|
+
|
|
891
|
+
// packages/coding-agent/dist/gateway/store.js
|
|
892
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
893
|
+
import { dirname, join } from "node:path";
|
|
894
|
+
var storePathOverride;
|
|
895
|
+
function storePath() {
|
|
896
|
+
return storePathOverride ?? join(getAgentDir(), "gateway-sessions.json");
|
|
897
|
+
}
|
|
898
|
+
function readStore() {
|
|
899
|
+
let path = storePath();
|
|
900
|
+
if (!existsSync(path))
|
|
901
|
+
return {};
|
|
902
|
+
try {
|
|
903
|
+
let parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
904
|
+
if (parsed == null || typeof parsed != "object" || Array.isArray(parsed))
|
|
905
|
+
return {};
|
|
906
|
+
let out = {};
|
|
907
|
+
for (let [key, value] of Object.entries(parsed)) {
|
|
908
|
+
if (value == null || typeof value != "object")
|
|
909
|
+
continue;
|
|
910
|
+
let v = value;
|
|
911
|
+
typeof v.sessionId != "string" || typeof v.sessionFile != "string" || (out[key] = {
|
|
912
|
+
sessionId: v.sessionId,
|
|
913
|
+
sessionFile: v.sessionFile,
|
|
914
|
+
createdAt: typeof v.createdAt == "string" ? v.createdAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
915
|
+
lastActiveAt: typeof v.lastActiveAt == "string" ? v.lastActiveAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return out;
|
|
919
|
+
} catch {
|
|
920
|
+
return {};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function writeStore(data) {
|
|
924
|
+
let path = storePath(), dir = dirname(path);
|
|
925
|
+
existsSync(dir) || mkdirSync(dir, { recursive: !0 });
|
|
926
|
+
let tmp = `${path}.tmp-${process.pid}`;
|
|
927
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}
|
|
928
|
+
`, "utf-8"), renameSync(tmp, path);
|
|
929
|
+
}
|
|
930
|
+
function getSession(key) {
|
|
931
|
+
return readStore()[key];
|
|
932
|
+
}
|
|
933
|
+
function putSession(key, entry) {
|
|
934
|
+
let data = readStore(), existing = data[key], stored = {
|
|
935
|
+
sessionId: entry.sessionId,
|
|
936
|
+
sessionFile: entry.sessionFile,
|
|
937
|
+
createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
938
|
+
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
939
|
+
};
|
|
940
|
+
return data[key] = stored, writeStore(data), stored;
|
|
941
|
+
}
|
|
942
|
+
function touchSession(key) {
|
|
943
|
+
let data = readStore(), existing = data[key];
|
|
944
|
+
existing && (existing.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString(), writeStore(data));
|
|
945
|
+
}
|
|
946
|
+
function removeSession(key) {
|
|
947
|
+
let data = readStore();
|
|
948
|
+
return key in data ? (delete data[key], writeStore(data), !0) : !1;
|
|
949
|
+
}
|
|
950
|
+
function listSessions() {
|
|
951
|
+
return readStore();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// packages/coding-agent/dist/gateway/agent-bridge.js
|
|
955
|
+
function isUserMessageEntry(entry) {
|
|
956
|
+
return entry.type === "message";
|
|
957
|
+
}
|
|
958
|
+
var QUEUED = "__lunr_gateway_queued__", CACHE_CAP = 20, QUEUE_CAP = 5, runDepth = 0;
|
|
959
|
+
async function defaultSessionFactory(key, reopen) {
|
|
960
|
+
let [{ loadAllBuiltinExtensions }, { getAgentDir: getAgentDir2 }, { registerCustomizeBridge }, { registerMemoryCapBridge }, { registerModelTierBridge }, { createAgentSessionFromServices, createAgentSessionServices }, { SessionManager: SessionManager2 }, { SettingsManager }, { bindRuntimeBridges }] = await Promise.all([
|
|
961
|
+
import("./builtin-extensions-LYZ3BFWQ.js"),
|
|
962
|
+
import("./config-FMEOZSWE.js"),
|
|
963
|
+
import("./customize-XEXL2DGQ.js"),
|
|
964
|
+
import("./memory-cap-3S4XRLNP.js"),
|
|
965
|
+
import("./model-tiers-NJLHUEGK.js"),
|
|
966
|
+
import("./agent-session-services-IFJMSX7Z.js"),
|
|
967
|
+
import("./session-manager-MQNBYACL.js"),
|
|
968
|
+
import("./settings-manager-5LF2VQWC.js"),
|
|
969
|
+
import("./runtime-bridges-PPHNBFWL.js")
|
|
970
|
+
]), cwd = process.cwd(), agentDir = getAgentDir2(), settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: !1 });
|
|
971
|
+
registerModelTierBridge(settingsManager), registerMemoryCapBridge(settingsManager), registerCustomizeBridge(settingsManager);
|
|
972
|
+
let sessionManager;
|
|
973
|
+
reopen && existsSync2(reopen.sessionFile) ? sessionManager = SessionManager2.open(reopen.sessionFile) : (reopen && console.warn(`[gateway] stored session file missing for ${key}; starting a fresh session`), sessionManager = SessionManager2.create(cwd));
|
|
974
|
+
let services = await createAgentSessionServices({
|
|
975
|
+
cwd,
|
|
976
|
+
agentDir,
|
|
977
|
+
settingsManager,
|
|
978
|
+
resourceLoaderOptions: { extensionFactories: await loadAllBuiltinExtensions() }
|
|
979
|
+
}), { session } = await createAgentSessionFromServices({ services, sessionManager });
|
|
980
|
+
return bindRuntimeBridges({ session, services }), await session.bindExtensions({
|
|
981
|
+
mode: "print",
|
|
982
|
+
onError: (err) => console.error(`[gateway] extension error (${err.extensionPath}): ${err.error}`)
|
|
983
|
+
}), session;
|
|
984
|
+
}
|
|
985
|
+
function extractFinalText(session) {
|
|
986
|
+
let messages = session.state.messages, last = messages[messages.length - 1];
|
|
987
|
+
if (last?.role !== "assistant")
|
|
988
|
+
return "";
|
|
989
|
+
let assistant = last;
|
|
990
|
+
if (assistant.stopReason === "error" || assistant.stopReason === "aborted")
|
|
991
|
+
throw new Error(assistant.errorMessage || `Request ${assistant.stopReason}`);
|
|
992
|
+
let text = "";
|
|
993
|
+
for (let content of assistant.content)
|
|
994
|
+
content.type === "text" && (text += content.text);
|
|
995
|
+
return text;
|
|
996
|
+
}
|
|
997
|
+
var AgentBridge = class {
|
|
998
|
+
cap;
|
|
999
|
+
sessionFactory;
|
|
1000
|
+
cache = /* @__PURE__ */ new Map();
|
|
1001
|
+
redoStack = /* @__PURE__ */ new Map();
|
|
1002
|
+
creating = /* @__PURE__ */ new Map();
|
|
1003
|
+
constructor(options = {}) {
|
|
1004
|
+
this.cap = options.cacheCap ?? CACHE_CAP, this.sessionFactory = options.sessionFactory ?? defaultSessionFactory;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Run one conversational turn. Resolves with the final assistant text, or
|
|
1008
|
+
* the QUEUED sentinel when the session was busy (router must not reply).
|
|
1009
|
+
*/
|
|
1010
|
+
async runTurn(key, event, callbacks = {}) {
|
|
1011
|
+
let entry = await this.getOrCreate(key);
|
|
1012
|
+
if (entry.busy)
|
|
1013
|
+
return entry.queue.length >= QUEUE_CAP && (entry.queue.shift(), entry.dropped++), entry.queue.push(event), QUEUED;
|
|
1014
|
+
entry.busy = !0;
|
|
1015
|
+
let result;
|
|
1016
|
+
try {
|
|
1017
|
+
result = await this.runPrompt(entry, event.text, callbacks, event.source, event.attachments);
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
throw entry.busy = !1, err;
|
|
1020
|
+
}
|
|
1021
|
+
return touchSession(key), this.drainQueue(entry, callbacks).finally(() => {
|
|
1022
|
+
entry.busy = !1;
|
|
1023
|
+
}), result;
|
|
1024
|
+
}
|
|
1025
|
+
async abort(key) {
|
|
1026
|
+
await this.cache.get(key)?.session.abort();
|
|
1027
|
+
}
|
|
1028
|
+
/** Return the live session for a key, or null when none exists yet. */
|
|
1029
|
+
async getSession(key) {
|
|
1030
|
+
let cached = this.cache.get(key);
|
|
1031
|
+
return cached ? cached.session : getSession(key) ? (await this.getOrCreate(key)).session : null;
|
|
1032
|
+
}
|
|
1033
|
+
/** Switch the chat to a different persisted session file. */
|
|
1034
|
+
async switchSession(key, sessionFile) {
|
|
1035
|
+
let entry = this.cache.get(key);
|
|
1036
|
+
if (entry?.busy)
|
|
1037
|
+
throw new Error("Session is busy \u2014 /stop first or wait");
|
|
1038
|
+
if (entry) {
|
|
1039
|
+
let oldSessionId = entry.session.sessionManager?.getSessionId();
|
|
1040
|
+
entry.unsubscribe?.(), this.cache.delete(key), entry.session.dispose?.(), oldSessionId && deletePermissionContext(oldSessionId);
|
|
1041
|
+
}
|
|
1042
|
+
removeSession(key), this.redoStack.delete(key);
|
|
1043
|
+
let session = await this.sessionFactory(key, { sessionFile }), newFile = session.sessionManager?.getSessionFile(), newSessionId = session.sessionManager?.getSessionId();
|
|
1044
|
+
newFile && putSession(key, { sessionId: newSessionId ?? key, sessionFile: newFile }), newSessionId && createPermissionContext(newSessionId);
|
|
1045
|
+
let newEntry = { session, busy: !1, queue: [], dropped: 0 };
|
|
1046
|
+
this.cache.set(key, newEntry), this._enforceCacheCap(key), newEntry.unsubscribe = this._subscribeToSession(key, newEntry);
|
|
1047
|
+
}
|
|
1048
|
+
/** Undo the last user turn and push the previous leaf onto the redo stack. */
|
|
1049
|
+
async undo(key) {
|
|
1050
|
+
let session = await this.getSession(key);
|
|
1051
|
+
if (!session)
|
|
1052
|
+
throw new Error("No session");
|
|
1053
|
+
if (session.isStreaming)
|
|
1054
|
+
throw new Error("Wait for the current response to finish");
|
|
1055
|
+
let branch = session.sessionManager?.getBranch() ?? [], lastUserIndex = -1;
|
|
1056
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
1057
|
+
let entry = branch[i];
|
|
1058
|
+
if (isUserMessageEntry(entry) && entry.message.role === "user") {
|
|
1059
|
+
lastUserIndex = i;
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (lastUserIndex === -1)
|
|
1064
|
+
throw new Error("Nothing to undo");
|
|
1065
|
+
let lastUser = branch[lastUserIndex];
|
|
1066
|
+
if (!isUserMessageEntry(lastUser))
|
|
1067
|
+
throw new Error("Nothing to undo");
|
|
1068
|
+
let leafId = session.sessionManager?.getLeafId() ?? null, targetId;
|
|
1069
|
+
if (lastUser.id === leafId) {
|
|
1070
|
+
if (!lastUser.parentId)
|
|
1071
|
+
throw new Error("Nothing to undo");
|
|
1072
|
+
targetId = lastUser.parentId;
|
|
1073
|
+
} else
|
|
1074
|
+
targetId = lastUser.id;
|
|
1075
|
+
if ((await session.navigateTree(targetId, {})).cancelled)
|
|
1076
|
+
throw new Error("Navigation cancelled");
|
|
1077
|
+
if (leafId) {
|
|
1078
|
+
let stack = this.redoStack.get(key) ?? [];
|
|
1079
|
+
stack.push(leafId), this.redoStack.set(key, stack);
|
|
1080
|
+
}
|
|
1081
|
+
return { userText: extractUserText(lastUser.message) };
|
|
1082
|
+
}
|
|
1083
|
+
/** Redo to the leaf saved by the most recent undo. */
|
|
1084
|
+
async redo(key) {
|
|
1085
|
+
let session = await this.getSession(key);
|
|
1086
|
+
if (!session)
|
|
1087
|
+
throw new Error("No session");
|
|
1088
|
+
if (session.isStreaming)
|
|
1089
|
+
throw new Error("Wait for the current response to finish");
|
|
1090
|
+
let stack = this.redoStack.get(key) ?? [], targetId = stack.pop();
|
|
1091
|
+
if (!targetId)
|
|
1092
|
+
throw new Error("Nothing to redo");
|
|
1093
|
+
if (this.redoStack.set(key, stack), (await session.navigateTree(targetId, {})).cancelled)
|
|
1094
|
+
throw stack.push(targetId), this.redoStack.set(key, stack), new Error("Navigation cancelled");
|
|
1095
|
+
}
|
|
1096
|
+
/** Test/debug hook: how many redo targets are saved for a key. */
|
|
1097
|
+
getRedoStackLength(key) {
|
|
1098
|
+
return this.redoStack.get(key)?.length ?? 0;
|
|
1099
|
+
}
|
|
1100
|
+
/** Drop the cached session (disposing it) and forget the store entry (/new, /reset). */
|
|
1101
|
+
async reset(key) {
|
|
1102
|
+
let entry = this.cache.get(key);
|
|
1103
|
+
if (entry) {
|
|
1104
|
+
entry.queue.length = 0, entry.dropped = 0;
|
|
1105
|
+
try {
|
|
1106
|
+
await entry.session.abort();
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
entry.unsubscribe?.(), this.cache.delete(key), entry.session.dispose?.();
|
|
1110
|
+
}
|
|
1111
|
+
removeSession(key), this.redoStack.delete(key);
|
|
1112
|
+
}
|
|
1113
|
+
getStatus(key) {
|
|
1114
|
+
let entry = this.cache.get(key);
|
|
1115
|
+
return {
|
|
1116
|
+
busy: entry?.busy ?? !1,
|
|
1117
|
+
queueDepth: entry?.queue.length ?? 0,
|
|
1118
|
+
createdAt: getSession(key)?.createdAt
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
/** One prompt → final text. Increments runDepth; streams deltas when asked. */
|
|
1122
|
+
async runPrompt(entry, text, callbacks, source, attachments) {
|
|
1123
|
+
let { session } = entry, unsubscribe;
|
|
1124
|
+
if (callbacks.onDelta) {
|
|
1125
|
+
let onDelta = callbacks.onDelta, streamedLen = 0;
|
|
1126
|
+
unsubscribe = session.subscribe((event) => {
|
|
1127
|
+
if (event.type !== "message_update")
|
|
1128
|
+
return;
|
|
1129
|
+
let assistantEvent = event.assistantMessageEvent;
|
|
1130
|
+
if (assistantEvent.type !== "text_delta")
|
|
1131
|
+
return;
|
|
1132
|
+
let full = "";
|
|
1133
|
+
for (let content of assistantEvent.partial.content)
|
|
1134
|
+
content.type === "text" && (full += content.text);
|
|
1135
|
+
full.length < streamedLen && (streamedLen = 0), full.length > streamedLen && (onDelta(full.slice(streamedLen)), streamedLen = full.length);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
runDepth++;
|
|
1139
|
+
try {
|
|
1140
|
+
let images = [], notes = [];
|
|
1141
|
+
for (let attachment of attachments ?? []) {
|
|
1142
|
+
let bytes = Buffer.from(attachment.data, "base64"), processed = await processImage(new Uint8Array(bytes), attachment.mimeType);
|
|
1143
|
+
if (processed.ok) {
|
|
1144
|
+
images.push({ type: "image", data: processed.data, mimeType: processed.mimeType });
|
|
1145
|
+
for (let hint of processed.hints)
|
|
1146
|
+
notes.push(hint);
|
|
1147
|
+
} else
|
|
1148
|
+
notes.push(processed.message);
|
|
1149
|
+
}
|
|
1150
|
+
let promptText = notes.length > 0 ? `${text}
|
|
1151
|
+
|
|
1152
|
+
${notes.join(" ")}` : text;
|
|
1153
|
+
await runWithOrigin({
|
|
1154
|
+
platform: source.platform,
|
|
1155
|
+
chatId: source.chatId,
|
|
1156
|
+
threadId: source.threadId,
|
|
1157
|
+
chatType: source.chatType
|
|
1158
|
+
}, () => session.prompt(promptText, { source: "extension", ...images.length ? { images } : {} }));
|
|
1159
|
+
} finally {
|
|
1160
|
+
runDepth--, unsubscribe?.();
|
|
1161
|
+
}
|
|
1162
|
+
return extractFinalText(session);
|
|
1163
|
+
}
|
|
1164
|
+
/** Drain the queue FIFO: all queued messages become one concatenated turn. */
|
|
1165
|
+
async drainQueue(entry, callbacks) {
|
|
1166
|
+
for (; entry.queue.length > 0; ) {
|
|
1167
|
+
let items = entry.queue.splice(0), parts = items.map((item) => item.text), attachments = items.flatMap((item) => item.attachments ?? []);
|
|
1168
|
+
entry.dropped > 0 && (parts.unshift(`[gateway: ${entry.dropped} earlier message(s) were dropped \u2014 the queue was full]`), entry.dropped = 0);
|
|
1169
|
+
try {
|
|
1170
|
+
let text = await this.runPrompt(entry, parts.join(`
|
|
1171
|
+
|
|
1172
|
+
`), {}, items[items.length - 1].source, attachments);
|
|
1173
|
+
callbacks.onFollowUpResult?.(text);
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
callbacks.onError?.(err instanceof Error ? err.message : String(err));
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async getOrCreate(key) {
|
|
1181
|
+
let cached = this.cache.get(key);
|
|
1182
|
+
if (cached)
|
|
1183
|
+
return this.cache.delete(key), this.cache.set(key, cached), cached;
|
|
1184
|
+
let pending2 = this.creating.get(key);
|
|
1185
|
+
if (!pending2) {
|
|
1186
|
+
pending2 = this.createEntry(key), this.creating.set(key, pending2);
|
|
1187
|
+
try {
|
|
1188
|
+
await pending2;
|
|
1189
|
+
} finally {
|
|
1190
|
+
this.creating.delete(key);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return pending2;
|
|
1194
|
+
}
|
|
1195
|
+
async createEntry(key) {
|
|
1196
|
+
let stored = getSession(key), session = await this.sessionFactory(key, stored ? { sessionFile: stored.sessionFile } : void 0), sessionFile = session.sessionManager?.getSessionFile(), sessionId = session.sessionManager?.getSessionId();
|
|
1197
|
+
sessionFile && putSession(key, { sessionId: sessionId ?? key, sessionFile }), sessionId && createPermissionContext(sessionId);
|
|
1198
|
+
let entry = { session, busy: !1, queue: [], dropped: 0, unsubscribe: void 0 };
|
|
1199
|
+
return this.cache.set(key, entry), this._enforceCacheCap(key), entry.unsubscribe = this._subscribeToSession(key, entry), entry;
|
|
1200
|
+
}
|
|
1201
|
+
_enforceCacheCap(protectedKey) {
|
|
1202
|
+
for (; this.cache.size > this.cap; ) {
|
|
1203
|
+
let oldestKey, oldest;
|
|
1204
|
+
for (let [key, entry] of this.cache)
|
|
1205
|
+
if (key !== protectedKey && !entry.busy) {
|
|
1206
|
+
oldestKey = key, oldest = entry;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
if (!oldest || !oldestKey) {
|
|
1210
|
+
this.cap = this.cache.size;
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
oldest.unsubscribe?.(), this.cache.delete(oldestKey), oldest.session.dispose?.();
|
|
1214
|
+
let evictedSessionId = oldest.session.sessionManager?.getSessionId();
|
|
1215
|
+
evictedSessionId && deletePermissionContext(evictedSessionId);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
_subscribeToSession(key, entry) {
|
|
1219
|
+
return entry.session.subscribe((event) => this._handleSessionEvent(key, event));
|
|
1220
|
+
}
|
|
1221
|
+
_handleSessionEvent(key, event) {
|
|
1222
|
+
event.type === "message_start" && event.message?.role === "user" && this.redoStack.delete(key);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
function extractUserText(message) {
|
|
1226
|
+
let text = "";
|
|
1227
|
+
if ("content" in message && Array.isArray(message.content))
|
|
1228
|
+
for (let block of message.content)
|
|
1229
|
+
block && typeof block == "object" && "type" in block && block.type === "text" && "text" in block && typeof block.text == "string" && (text += block.text);
|
|
1230
|
+
return text;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// packages/coding-agent/dist/gateway/approval.js
|
|
1234
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1235
|
+
import { randomBytes } from "node:crypto";
|
|
1236
|
+
var CALLBACK_PREFIX = "ap:", ID_LENGTH = 8, CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789", DEFAULT_TIMEOUT_MS = 300 * 1e3, approvalContext = new AsyncLocalStorage(), pending = /* @__PURE__ */ new Map();
|
|
1237
|
+
function generateId() {
|
|
1238
|
+
let id = "", bytes = randomBytes(ID_LENGTH);
|
|
1239
|
+
for (let i = 0; i < ID_LENGTH; i++)
|
|
1240
|
+
id += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];
|
|
1241
|
+
return id;
|
|
1242
|
+
}
|
|
1243
|
+
function makeCallbackData(id, action) {
|
|
1244
|
+
return `${CALLBACK_PREFIX}${id}:${action}`;
|
|
1245
|
+
}
|
|
1246
|
+
function parseCallbackData(data) {
|
|
1247
|
+
if (!data.startsWith(CALLBACK_PREFIX))
|
|
1248
|
+
return null;
|
|
1249
|
+
let rest = data.slice(CALLBACK_PREFIX.length), colon = rest.indexOf(":");
|
|
1250
|
+
if (colon === -1)
|
|
1251
|
+
return null;
|
|
1252
|
+
let id = rest.slice(0, colon), action = rest.slice(colon + 1);
|
|
1253
|
+
return action === "once" || action === "session" || action === "reject" || action === "cancel" ? { id, action } : null;
|
|
1254
|
+
}
|
|
1255
|
+
function runWithApprovalContext(ctx, fn) {
|
|
1256
|
+
return approvalContext.run(ctx, fn);
|
|
1257
|
+
}
|
|
1258
|
+
async function createApprovalPrompt(adapter, source, req, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1259
|
+
let id = generateId(), title = req.kind === "large-subagent-launch" ? `Approve large subagent launch?
|
|
1260
|
+
${req.detail}` : `Approve ${req.toolName}?
|
|
1261
|
+
${req.detail}`, rows = [
|
|
1262
|
+
[
|
|
1263
|
+
{ label: "\u2713 Approve once", data: makeCallbackData(id, "once") },
|
|
1264
|
+
{ label: "\u2713 Approve session", data: makeCallbackData(id, "session") }
|
|
1265
|
+
],
|
|
1266
|
+
[{ label: "\u2717 Reject", data: makeCallbackData(id, "reject") }]
|
|
1267
|
+
];
|
|
1268
|
+
return new Promise((resolve, reject) => {
|
|
1269
|
+
let messageId, timeout = setTimeout(() => {
|
|
1270
|
+
pending.delete(id), reject(new Error("Approval request timed out")), messageId && adapter.editMessage(source.chatId, messageId, "\u23F1 Approval expired.", []).catch(() => {
|
|
1271
|
+
});
|
|
1272
|
+
}, timeoutMs);
|
|
1273
|
+
(async () => {
|
|
1274
|
+
try {
|
|
1275
|
+
let result = await adapter.sendButtons(source.chatId, title, rows, { threadId: source.threadId });
|
|
1276
|
+
if (!result.success || !result.messageId) {
|
|
1277
|
+
clearTimeout(timeout), reject(new Error(result.error ?? "Failed to send approval prompt"));
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
messageId = result.messageId, pending.set(id, {
|
|
1281
|
+
resolve,
|
|
1282
|
+
reject,
|
|
1283
|
+
timeout,
|
|
1284
|
+
messageId,
|
|
1285
|
+
adapter,
|
|
1286
|
+
chatId: source.chatId,
|
|
1287
|
+
threadId: source.threadId,
|
|
1288
|
+
userId: source.userId
|
|
1289
|
+
});
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
clearTimeout(timeout), reject(err);
|
|
1292
|
+
}
|
|
1293
|
+
})();
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
async function createGatewayApprovalRequest(req) {
|
|
1297
|
+
let ctx = approvalContext.getStore();
|
|
1298
|
+
if (!ctx)
|
|
1299
|
+
throw new Error(NO_HANDLER_REASON);
|
|
1300
|
+
return createApprovalPrompt(ctx.adapter, ctx.source, req, ctx.timeoutMs);
|
|
1301
|
+
}
|
|
1302
|
+
function registerGatewayApprovalHandler() {
|
|
1303
|
+
registerApprovalHandler(createGatewayApprovalRequest);
|
|
1304
|
+
}
|
|
1305
|
+
async function handleApprovalCallback(event, adapter) {
|
|
1306
|
+
let parsed = parseCallbackData(event.data);
|
|
1307
|
+
if (!parsed)
|
|
1308
|
+
return !1;
|
|
1309
|
+
let entry = pending.get(parsed.id);
|
|
1310
|
+
if (!entry)
|
|
1311
|
+
return await adapter?.answerCallback(event.id, "Approval expired.").catch(() => {
|
|
1312
|
+
}), !0;
|
|
1313
|
+
if (event.userId !== entry.userId)
|
|
1314
|
+
return await entry.adapter.answerCallback(event.id, "\u26D4 Not your approval.").catch(() => {
|
|
1315
|
+
}), !0;
|
|
1316
|
+
clearTimeout(entry.timeout), pending.delete(parsed.id);
|
|
1317
|
+
let response = parsed.action === "reject" || parsed.action === "cancel" ? "reject" : parsed.action;
|
|
1318
|
+
entry.resolve(response);
|
|
1319
|
+
let label = parsed.action === "reject" || parsed.action === "cancel" ? "Rejected." : `Approved (${parsed.action}).`;
|
|
1320
|
+
return await entry.adapter.editMessage(entry.chatId, entry.messageId ?? "", label, []).catch(() => {
|
|
1321
|
+
}), await entry.adapter.answerCallback(event.id).catch(() => {
|
|
1322
|
+
}), !0;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// packages/coding-agent/dist/gateway/authz.js
|
|
1326
|
+
function envFlagEnabled(name) {
|
|
1327
|
+
let raw = process.env[name];
|
|
1328
|
+
return raw === void 0 ? !1 : ["1", "true", "yes", "on"].includes(raw.toLowerCase());
|
|
1329
|
+
}
|
|
1330
|
+
function globalAllowedUsers() {
|
|
1331
|
+
let raw = process.env.LUNR_GATEWAY_ALLOWED_USERS;
|
|
1332
|
+
return raw ? raw.split(",").map((u) => u.trim()).filter((u) => u.length > 0) : [];
|
|
1333
|
+
}
|
|
1334
|
+
function isAuthorized(source, cfg, pairing) {
|
|
1335
|
+
if (envFlagEnabled("LUNR_GATEWAY_ALLOW_ALL_USERS"))
|
|
1336
|
+
return !0;
|
|
1337
|
+
let platformCfg = platformConfigFor(cfg, source.platform);
|
|
1338
|
+
return !!(platformCfg?.allowedChats.includes(source.chatId) || source.roleAuthorized === !0 || source.userId && pairing.isPaired(source.platform, source.userId) || source.userId && (platformCfg?.allowedUsers.includes(source.userId) || globalAllowedUsers().includes(source.userId)));
|
|
1339
|
+
}
|
|
1340
|
+
function addAllowedUser(platform, userId) {
|
|
1341
|
+
let cfg = loadGatewayConfig(), platformCfg = platformConfigFor(cfg, platform);
|
|
1342
|
+
if (!platformCfg)
|
|
1343
|
+
throw new Error(`Unknown platform "${platform}"`);
|
|
1344
|
+
platformCfg.allowedUsers.includes(userId) || (platformCfg.allowedUsers.push(userId), saveGatewayConfig(cfg));
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// packages/coding-agent/dist/gateway/buttons.js
|
|
1348
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1349
|
+
var PICKER_TTL_MS = 900 * 1e3, SWEEP_INTERVAL_MS = 60 * 1e3, DEFAULT_PER_PAGE = 8, BUTTONS_PER_ROW = 2, CODE_ALPHABET2 = "ABCDEFGHJKMNPQRSTUVWXYZ23456789", CODE_LENGTH = 8, CALLBACK_PREFIX2 = "cp:", registry = /* @__PURE__ */ new Map(), sweeper;
|
|
1350
|
+
function generatePickerId() {
|
|
1351
|
+
let id = "", bytes = randomBytes2(CODE_LENGTH);
|
|
1352
|
+
for (let i = 0; i < CODE_LENGTH; i++)
|
|
1353
|
+
id += CODE_ALPHABET2[bytes[i] % CODE_ALPHABET2.length];
|
|
1354
|
+
return id;
|
|
1355
|
+
}
|
|
1356
|
+
function makeCallbackData2(id, action, payload) {
|
|
1357
|
+
return action === "noop" ? `${CALLBACK_PREFIX2}${id}:` : action === "cancel" ? `${CALLBACK_PREFIX2}${id}:x` : action === "page" ? `${CALLBACK_PREFIX2}${id}:p${payload ?? "0"}` : `${CALLBACK_PREFIX2}${id}:${payload ?? ""}`;
|
|
1358
|
+
}
|
|
1359
|
+
function parseCallbackData2(data) {
|
|
1360
|
+
if (!data.startsWith(CALLBACK_PREFIX2))
|
|
1361
|
+
return null;
|
|
1362
|
+
let rest = data.slice(CALLBACK_PREFIX2.length), colon = rest.indexOf(":");
|
|
1363
|
+
if (colon === -1)
|
|
1364
|
+
return null;
|
|
1365
|
+
let id = rest.slice(0, colon), tail = rest.slice(colon + 1);
|
|
1366
|
+
return tail === "" ? { id, action: "noop" } : tail === "x" ? { id, action: "cancel" } : tail.startsWith("p") ? { id, action: "page", payload: tail.slice(1) } : { id, action: "select", payload: tail };
|
|
1367
|
+
}
|
|
1368
|
+
function isSameChat(a, b) {
|
|
1369
|
+
return a.platform === b.platform && a.chatId === b.chatId && a.threadId === b.threadId;
|
|
1370
|
+
}
|
|
1371
|
+
function sourceForAuth(picker, cb) {
|
|
1372
|
+
return {
|
|
1373
|
+
platform: picker.source.platform,
|
|
1374
|
+
chatId: cb.chatId,
|
|
1375
|
+
chatType: picker.source.chatType,
|
|
1376
|
+
userId: cb.userId,
|
|
1377
|
+
userName: cb.userName,
|
|
1378
|
+
threadId: cb.threadId,
|
|
1379
|
+
// Do NOT inherit the original invoker's role authorization.
|
|
1380
|
+
roleAuthorized: void 0
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function renderPage(picker) {
|
|
1384
|
+
let { id, title, breadcrumbs, items, page, perPage } = picker, totalPages = Math.max(1, Math.ceil(items.length / perPage)), clampedPage = Math.max(0, Math.min(page, totalPages - 1)), start = clampedPage * perPage, pageItems = items.slice(start, start + perPage), rows = [];
|
|
1385
|
+
for (let i = 0; i < pageItems.length; i += BUTTONS_PER_ROW) {
|
|
1386
|
+
let row = [];
|
|
1387
|
+
for (let j = 0; j < BUTTONS_PER_ROW && i + j < pageItems.length; j++) {
|
|
1388
|
+
let index = start + i + j;
|
|
1389
|
+
row.push({ label: pageItems[i + j].label, data: makeCallbackData2(id, "select", String(index)) });
|
|
1390
|
+
}
|
|
1391
|
+
rows.push(row);
|
|
1392
|
+
}
|
|
1393
|
+
if (totalPages > 1) {
|
|
1394
|
+
let nav = [];
|
|
1395
|
+
clampedPage > 0 && nav.push({ label: "\u25C0 Prev", data: makeCallbackData2(id, "page", String(clampedPage - 1)) }), nav.push({ label: `${clampedPage + 1}/${totalPages}`, data: makeCallbackData2(id, "noop") }), clampedPage < totalPages - 1 && nav.push({ label: "Next \u25B6", data: makeCallbackData2(id, "page", String(clampedPage + 1)) }), rows.push(nav);
|
|
1396
|
+
}
|
|
1397
|
+
rows.push([{ label: "\u2717 Cancel", data: makeCallbackData2(id, "cancel") }]);
|
|
1398
|
+
let header = breadcrumbs ? `${title} \xB7 ${breadcrumbs}` : title;
|
|
1399
|
+
return { text: items.length === 0 ? `${header}
|
|
1400
|
+
|
|
1401
|
+
No options available.` : `${header} (${clampedPage + 1}/${totalPages}):`, buttons: rows };
|
|
1402
|
+
}
|
|
1403
|
+
async function createPicker(adapter, source, spec, opts) {
|
|
1404
|
+
let id = generatePickerId(), pending2 = {
|
|
1405
|
+
...spec,
|
|
1406
|
+
id,
|
|
1407
|
+
chatId: source.chatId,
|
|
1408
|
+
source,
|
|
1409
|
+
adapter,
|
|
1410
|
+
messageId: "",
|
|
1411
|
+
page: 0,
|
|
1412
|
+
perPage: spec.perPage ?? DEFAULT_PER_PAGE,
|
|
1413
|
+
createdAt: Date.now()
|
|
1414
|
+
}, { text, buttons } = renderPage(pending2), result = await adapter.sendButtons(source.chatId, text, buttons, opts);
|
|
1415
|
+
return result.success && result.messageId && (pending2.messageId = result.messageId, registry.set(id, pending2)), result;
|
|
1416
|
+
}
|
|
1417
|
+
async function answerExpired(adapter, cb) {
|
|
1418
|
+
await adapter.answerCallback(cb.id, "Picker expired \u2014 run the command again.").catch(() => {
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
async function answerUnauthorized(adapter, cb) {
|
|
1422
|
+
await adapter.answerCallback(cb.id, "\u26D4 Not authorized.").catch(() => {
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
async function answerNotYourPicker(adapter, cb) {
|
|
1426
|
+
await adapter.answerCallback(cb.id, "\u26D4 Not your picker.").catch(() => {
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
async function handleCallback(cb, deps) {
|
|
1430
|
+
let { adapter, cfg, pairing } = deps, parsed = parseCallbackData2(cb.data);
|
|
1431
|
+
if (!parsed)
|
|
1432
|
+
return;
|
|
1433
|
+
let picker = registry.get(parsed.id);
|
|
1434
|
+
if (!picker) {
|
|
1435
|
+
await answerExpired(adapter, cb);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (Date.now() > picker.createdAt + PICKER_TTL_MS) {
|
|
1439
|
+
registry.delete(parsed.id);
|
|
1440
|
+
try {
|
|
1441
|
+
await picker.adapter.editMessage(picker.chatId, picker.messageId, "\u23F1 Expired \u2014 run the command again.", []);
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
await answerExpired(adapter, cb);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (!isSameChat(picker.source, sourceForAuth(picker, cb))) {
|
|
1448
|
+
await answerExpired(adapter, cb);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
if (cb.userId !== picker.invokerId) {
|
|
1452
|
+
await answerNotYourPicker(adapter, cb);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (!isAuthorized(sourceForAuth(picker, cb), cfg, pairing)) {
|
|
1456
|
+
await answerUnauthorized(adapter, cb);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (parsed.action === "cancel") {
|
|
1460
|
+
registry.delete(parsed.id);
|
|
1461
|
+
try {
|
|
1462
|
+
await picker.adapter.editMessage(picker.chatId, picker.messageId, "Cancelled.", []);
|
|
1463
|
+
} catch {
|
|
1464
|
+
}
|
|
1465
|
+
await adapter.answerCallback(cb.id).catch(() => {
|
|
1466
|
+
});
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (parsed.action === "noop") {
|
|
1470
|
+
await adapter.answerCallback(cb.id).catch(() => {
|
|
1471
|
+
});
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (parsed.action === "page") {
|
|
1475
|
+
let page = Number.parseInt(parsed.payload ?? "0", 10), totalPages = Math.max(1, Math.ceil(picker.items.length / picker.perPage));
|
|
1476
|
+
picker.page = Math.max(0, Math.min(page, totalPages - 1));
|
|
1477
|
+
let { text, buttons } = renderPage(picker);
|
|
1478
|
+
try {
|
|
1479
|
+
await picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);
|
|
1480
|
+
} catch {
|
|
1481
|
+
}
|
|
1482
|
+
await adapter.answerCallback(cb.id).catch(() => {
|
|
1483
|
+
});
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
let index = Number.parseInt(parsed.payload ?? "", 10), item = picker.items[index];
|
|
1487
|
+
if (!item) {
|
|
1488
|
+
await adapter.answerCallback(cb.id, "Invalid selection.").catch(() => {
|
|
1489
|
+
});
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
let result = await picker.resolve(item);
|
|
1494
|
+
if (!result.done) {
|
|
1495
|
+
picker.items = result.items, picker.title = result.title, picker.breadcrumbs = result.breadcrumbs, picker.page = 0;
|
|
1496
|
+
let { text, buttons } = renderPage(picker);
|
|
1497
|
+
try {
|
|
1498
|
+
await picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);
|
|
1499
|
+
} catch {
|
|
1500
|
+
}
|
|
1501
|
+
await adapter.answerCallback(cb.id).catch(() => {
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
registry.delete(parsed.id);
|
|
1506
|
+
try {
|
|
1507
|
+
await picker.adapter.editMessage(picker.chatId, picker.messageId, result.text, []);
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1510
|
+
await adapter.answerCallback(cb.id).catch(() => {
|
|
1511
|
+
});
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
registry.delete(parsed.id);
|
|
1514
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
1515
|
+
try {
|
|
1516
|
+
await picker.adapter.editMessage(picker.chatId, picker.messageId, `\u26A0 ${message}`, []);
|
|
1517
|
+
} catch {
|
|
1518
|
+
}
|
|
1519
|
+
await adapter.answerCallback(cb.id).catch(() => {
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function startButtonSweeper() {
|
|
1524
|
+
sweeper || (sweeper = setInterval(() => {
|
|
1525
|
+
let cutoff = Date.now() - PICKER_TTL_MS;
|
|
1526
|
+
for (let [id, entry] of registry)
|
|
1527
|
+
if (!(entry.createdAt > cutoff)) {
|
|
1528
|
+
registry.delete(id);
|
|
1529
|
+
try {
|
|
1530
|
+
entry.adapter.editMessage(entry.chatId, entry.messageId, "\u23F1 Expired \u2014 run the command again.", []);
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}, SWEEP_INTERVAL_MS), sweeper.unref?.());
|
|
1535
|
+
}
|
|
1536
|
+
function stopButtonSweeper() {
|
|
1537
|
+
sweeper && (clearInterval(sweeper), sweeper = void 0);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// packages/coding-agent/dist/gateway/commands.js
|
|
1541
|
+
var MODEL_CACHE_TTL_MS = 300 * 1e3, SESSIONS_CACHE_TTL_MS = 300 * 1e3, MAX_MODEL_LIST_LINES = 15, PROVIDER_PER_PAGE = 10;
|
|
1542
|
+
var MAX_MODELS_PER_PROVIDER = 50, MODEL_ID_MAX = 38, MODEL_CACHE_CAP = 100, SESSIONS_CACHE_CAP = 100, modelCache = /* @__PURE__ */ new Map(), sessionsCache = /* @__PURE__ */ new Map();
|
|
1543
|
+
function evictLRU(map, cap) {
|
|
1544
|
+
for (; map.size > cap; ) {
|
|
1545
|
+
let first = map.keys().next().value;
|
|
1546
|
+
first !== void 0 && map.delete(first);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function getCachedModels(key) {
|
|
1550
|
+
let cached = modelCache.get(key);
|
|
1551
|
+
if (cached) {
|
|
1552
|
+
if (Date.now() > cached.expires) {
|
|
1553
|
+
modelCache.delete(key);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
return modelCache.delete(key), modelCache.set(key, cached), cached.models;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function setCachedModels(key, models) {
|
|
1560
|
+
modelCache.set(key, { models, expires: Date.now() + MODEL_CACHE_TTL_MS }), evictLRU(modelCache, MODEL_CACHE_CAP);
|
|
1561
|
+
}
|
|
1562
|
+
function getCachedSessions(key) {
|
|
1563
|
+
let cached = sessionsCache.get(key);
|
|
1564
|
+
if (cached) {
|
|
1565
|
+
if (Date.now() > cached.expires) {
|
|
1566
|
+
sessionsCache.delete(key);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
return sessionsCache.delete(key), sessionsCache.set(key, cached), cached.sessions;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function setCachedSessions(key, sessions) {
|
|
1573
|
+
sessionsCache.set(key, { sessions, expires: Date.now() + SESSIONS_CACHE_TTL_MS }), evictLRU(sessionsCache, SESSIONS_CACHE_CAP);
|
|
1574
|
+
}
|
|
1575
|
+
function oneLine(text) {
|
|
1576
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
1577
|
+
}
|
|
1578
|
+
function prefixLines(text, prefix) {
|
|
1579
|
+
return text.split(`
|
|
1580
|
+
`).map((line, index) => (index === 0 ? prefix : " ") + line).join(`
|
|
1581
|
+
`);
|
|
1582
|
+
}
|
|
1583
|
+
function truncate(text, max) {
|
|
1584
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
1585
|
+
}
|
|
1586
|
+
function formatRelativeTime(date) {
|
|
1587
|
+
let seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1e3));
|
|
1588
|
+
if (seconds < 60)
|
|
1589
|
+
return `${seconds}s ago`;
|
|
1590
|
+
let minutes = Math.floor(seconds / 60);
|
|
1591
|
+
if (minutes < 60)
|
|
1592
|
+
return `${minutes}m ago`;
|
|
1593
|
+
let hours = Math.floor(minutes / 60);
|
|
1594
|
+
return hours < 24 ? `${hours}h ago` : `${Math.floor(hours / 24)}d ago`;
|
|
1595
|
+
}
|
|
1596
|
+
function formatModelList(models, maxLines = MAX_MODEL_LIST_LINES) {
|
|
1597
|
+
if (models.length === 0)
|
|
1598
|
+
return ["No models available."];
|
|
1599
|
+
let byProvider = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (let model of models) {
|
|
1601
|
+
let list = byProvider.get(model.provider) ?? [];
|
|
1602
|
+
list.push(model), byProvider.set(model.provider, list);
|
|
1603
|
+
}
|
|
1604
|
+
let providers = [...byProvider.keys()].sort(), lines = [], index = 1;
|
|
1605
|
+
for (let provider of providers) {
|
|
1606
|
+
let list = (byProvider.get(provider) ?? []).sort((a, b) => a.id.localeCompare(b.id));
|
|
1607
|
+
for (let model of list)
|
|
1608
|
+
lines.push(`${index}) ${provider}/${model.id}`), index++;
|
|
1609
|
+
}
|
|
1610
|
+
return lines.length <= maxLines ? lines : [
|
|
1611
|
+
`${models.length} models across ${providers.length} providers.`,
|
|
1612
|
+
"Use /model <provider/id> or /model <query>.",
|
|
1613
|
+
...providers.map((p) => `- ${p}`)
|
|
1614
|
+
];
|
|
1615
|
+
}
|
|
1616
|
+
function groupModelsByProvider(models) {
|
|
1617
|
+
let byProvider = /* @__PURE__ */ new Map();
|
|
1618
|
+
for (let model of models) {
|
|
1619
|
+
let list = byProvider.get(model.provider) ?? [];
|
|
1620
|
+
list.push(model), byProvider.set(model.provider, list);
|
|
1621
|
+
}
|
|
1622
|
+
for (let list of byProvider.values())
|
|
1623
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
1624
|
+
return byProvider;
|
|
1625
|
+
}
|
|
1626
|
+
function truncateModelId(id) {
|
|
1627
|
+
let lastSlash = id.lastIndexOf("/"), name = lastSlash >= 0 ? id.slice(lastSlash + 1) : id;
|
|
1628
|
+
return truncate(name, MODEL_ID_MAX);
|
|
1629
|
+
}
|
|
1630
|
+
async function refreshAndCacheModels(session, key) {
|
|
1631
|
+
let runtime = session.modelRuntime;
|
|
1632
|
+
await runtime.refresh();
|
|
1633
|
+
let models = [...await runtime.getAvailable()].sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
|
|
1634
|
+
return setCachedModels(key, models), models;
|
|
1635
|
+
}
|
|
1636
|
+
async function setModelAndReply(ctx, model) {
|
|
1637
|
+
let session = ctx.session;
|
|
1638
|
+
try {
|
|
1639
|
+
await session.setModel(model);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
if ((error instanceof Error ? error.message : String(error)).includes("No API key")) {
|
|
1642
|
+
await ctx.reply(`No API key for ${model.provider}.`);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
throw error;
|
|
1646
|
+
}
|
|
1647
|
+
await ctx.reply(`Model \u2192 ${model.provider}/${model.id} (thinking level re-clamped to ${session.thinkingLevel})`);
|
|
1648
|
+
}
|
|
1649
|
+
var newCommand = {
|
|
1650
|
+
name: "new",
|
|
1651
|
+
aliases: ["reset"],
|
|
1652
|
+
description: "start a fresh session for this chat",
|
|
1653
|
+
// lunr: busy-session /new must bypass the guard (same as /stop).
|
|
1654
|
+
bypassBusy: !0,
|
|
1655
|
+
needsSession: !1,
|
|
1656
|
+
async handler(ctx) {
|
|
1657
|
+
await ctx.bridge.reset(ctx.key), await ctx.reply("Session reset \u2014 next message starts a fresh session.");
|
|
1658
|
+
}
|
|
1659
|
+
}, undoCommand = {
|
|
1660
|
+
name: "undo",
|
|
1661
|
+
description: "rewind the session tree before the last user turn",
|
|
1662
|
+
bypassBusy: !1,
|
|
1663
|
+
needsSession: !0,
|
|
1664
|
+
async handler(ctx) {
|
|
1665
|
+
let { userText } = await ctx.bridge.undo(ctx.key), text = userText ? ` "${truncate(userText, 60)}"` : "";
|
|
1666
|
+
await ctx.reply(`Undone \u2014 rewound before${text}. /redo to restore.`);
|
|
1667
|
+
}
|
|
1668
|
+
}, redoCommand = {
|
|
1669
|
+
name: "redo",
|
|
1670
|
+
description: "restore the last undone turn",
|
|
1671
|
+
bypassBusy: !1,
|
|
1672
|
+
needsSession: !0,
|
|
1673
|
+
async handler(ctx) {
|
|
1674
|
+
await ctx.bridge.redo(ctx.key), await ctx.reply("Redone.");
|
|
1675
|
+
}
|
|
1676
|
+
}, stopCommand = {
|
|
1677
|
+
name: "stop",
|
|
1678
|
+
description: "abort the running turn",
|
|
1679
|
+
bypassBusy: !0,
|
|
1680
|
+
needsSession: !1,
|
|
1681
|
+
async handler(ctx) {
|
|
1682
|
+
await ctx.bridge.abort(ctx.key), await ctx.reply("Stopped.");
|
|
1683
|
+
}
|
|
1684
|
+
}, statusCommand = {
|
|
1685
|
+
name: "status",
|
|
1686
|
+
description: "session status",
|
|
1687
|
+
bypassBusy: !0,
|
|
1688
|
+
needsSession: !1,
|
|
1689
|
+
async handler(ctx) {
|
|
1690
|
+
let status = ctx.bridge.getStatus(ctx.key), age = status.createdAt ? `${Math.max(0, Math.round((Date.now() - Date.parse(status.createdAt)) / 1e3))}s` : "no session yet";
|
|
1691
|
+
await ctx.reply(`${ctx.event.source.platform} \xB7 session age ${age} \xB7 ${status.busy ? "busy" : "idle"} \xB7 queue ${status.queueDepth}`);
|
|
1692
|
+
}
|
|
1693
|
+
}, whoamiCommand = {
|
|
1694
|
+
name: "whoami",
|
|
1695
|
+
description: "your ids and session key",
|
|
1696
|
+
bypassBusy: !0,
|
|
1697
|
+
needsSession: !1,
|
|
1698
|
+
async handler(ctx) {
|
|
1699
|
+
let { userId, chatId } = ctx.event.source;
|
|
1700
|
+
await ctx.reply(`userId: ${userId}
|
|
1701
|
+
chatId: ${chatId}
|
|
1702
|
+
key: ${ctx.key}`);
|
|
1703
|
+
}
|
|
1704
|
+
}, helpCommand = {
|
|
1705
|
+
name: "help",
|
|
1706
|
+
description: "this message",
|
|
1707
|
+
bypassBusy: !0,
|
|
1708
|
+
needsSession: !1,
|
|
1709
|
+
async handler(ctx) {
|
|
1710
|
+
await ctx.reply(formatHelpText());
|
|
1711
|
+
}
|
|
1712
|
+
}, modelCommand = {
|
|
1713
|
+
name: "model",
|
|
1714
|
+
description: "list or switch models",
|
|
1715
|
+
bypassBusy: !1,
|
|
1716
|
+
needsSession: !0,
|
|
1717
|
+
async handler(ctx) {
|
|
1718
|
+
let session = ctx.session, arg = ctx.args.trim();
|
|
1719
|
+
if (!arg) {
|
|
1720
|
+
let models2 = getCachedModels(ctx.key) ?? await refreshAndCacheModels(session, ctx.key), byProvider = groupModelsByProvider(models2), current = session.model, currentRef = current ? `${current.provider}/${current.id}` : "", providerItems = [...byProvider.keys()].sort().map((provider) => ({
|
|
1721
|
+
label: `${current?.provider === provider ? "\u2713 " : ""}${provider} (${byProvider.get(provider)?.length ?? 0})`,
|
|
1722
|
+
value: provider
|
|
1723
|
+
})), pickerOpts = {
|
|
1724
|
+
replyTo: ctx.event.messageId,
|
|
1725
|
+
threadId: ctx.event.source.threadId
|
|
1726
|
+
};
|
|
1727
|
+
if (!(await createPicker(ctx.adapter, ctx.event.source, {
|
|
1728
|
+
kind: "model",
|
|
1729
|
+
sessionKey: ctx.key,
|
|
1730
|
+
invokerId: ctx.event.source.userId,
|
|
1731
|
+
items: providerItems,
|
|
1732
|
+
perPage: PROVIDER_PER_PAGE,
|
|
1733
|
+
title: "Pick a model provider",
|
|
1734
|
+
async resolve(item) {
|
|
1735
|
+
if (item.value === "__back__")
|
|
1736
|
+
return { done: !1, items: providerItems, title: "Pick a model provider" };
|
|
1737
|
+
if (!item.value.includes("/")) {
|
|
1738
|
+
let provider = item.value, list = byProvider.get(provider) ?? [], capped = list.slice(0, MAX_MODELS_PER_PROVIDER), note = list.length > MAX_MODELS_PER_PROVIDER ? ` \u2014 ${list.length - MAX_MODELS_PER_PROVIDER} more available` : "", modelItems = [];
|
|
1739
|
+
for (let m of capped) {
|
|
1740
|
+
let ref = `${provider}/${m.id}`;
|
|
1741
|
+
modelItems.push({
|
|
1742
|
+
label: `${currentRef === ref ? "\u2713 " : ""}${truncateModelId(m.id)}`,
|
|
1743
|
+
value: ref
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return modelItems.unshift({ label: "\u25C0 Back", value: "__back__" }), {
|
|
1747
|
+
done: !1,
|
|
1748
|
+
items: modelItems,
|
|
1749
|
+
title: `${provider} (${list.length})${note}`,
|
|
1750
|
+
breadcrumbs: provider
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
let exact2 = findExactModelReferenceMatch(item.value, models2);
|
|
1754
|
+
if (!exact2)
|
|
1755
|
+
return { done: !0, text: "That model is no longer available." };
|
|
1756
|
+
try {
|
|
1757
|
+
await session.setModel(exact2);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if ((error instanceof Error ? error.message : String(error)).includes("No API key"))
|
|
1760
|
+
return { done: !0, text: `No API key for ${exact2.provider}.` };
|
|
1761
|
+
throw error;
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
done: !0,
|
|
1765
|
+
text: `\u263E Model \u2192 ${exact2.provider}/${exact2.id} (thinking level re-clamped to ${session.thinkingLevel})`
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
}, pickerOpts)).success) {
|
|
1769
|
+
let currentText = `${current?.provider ?? "none"}/${current?.id ?? "none"}`, lines = formatModelList(models2);
|
|
1770
|
+
await ctx.reply([`Current: ${currentText}`, ...lines, "Use /model <n> or /model <provider/id>"].join(`
|
|
1771
|
+
`));
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
let numeric = Number.parseInt(arg, 10);
|
|
1776
|
+
if (!Number.isNaN(numeric) && /^\d+$/.test(arg)) {
|
|
1777
|
+
let cached = getCachedModels(ctx.key);
|
|
1778
|
+
if (!cached || numeric < 1 || numeric > cached.length) {
|
|
1779
|
+
await ctx.reply("Invalid selection \u2014 run /model to see the current list.");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
await setModelAndReply(ctx, cached[numeric - 1]);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
let models = getCachedModels(ctx.key) ?? await refreshAndCacheModels(session, ctx.key), exact = findExactModelReferenceMatch(arg, models);
|
|
1786
|
+
if (exact) {
|
|
1787
|
+
await setModelAndReply(ctx, exact);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
let query = arg.toLowerCase(), matches = models.filter((m) => m.id.toLowerCase().includes(query) || `${m.provider}/${m.id}`.toLowerCase().includes(query) || (m.name?.toLowerCase().includes(query) ?? !1));
|
|
1791
|
+
if (matches.length === 0) {
|
|
1792
|
+
await ctx.reply(`No model matches "${arg}".`);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
if (matches.length > 1) {
|
|
1796
|
+
await ctx.reply(formatModelList(matches).join(`
|
|
1797
|
+
`));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
await setModelAndReply(ctx, matches[0]);
|
|
1801
|
+
}
|
|
1802
|
+
}, sessionsCommand = {
|
|
1803
|
+
name: "sessions",
|
|
1804
|
+
description: "list or switch sessions",
|
|
1805
|
+
bypassBusy: !1,
|
|
1806
|
+
needsSession: !0,
|
|
1807
|
+
async handler(ctx) {
|
|
1808
|
+
let session = ctx.session, arg = ctx.args.trim(), currentFile = session.sessionManager?.getSessionFile();
|
|
1809
|
+
if (!arg) {
|
|
1810
|
+
let all = await SessionManager.list(process.cwd());
|
|
1811
|
+
all.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1812
|
+
let sessions = all.slice(0, 10);
|
|
1813
|
+
setCachedSessions(ctx.key, sessions);
|
|
1814
|
+
let items = sessions.map((s) => ({
|
|
1815
|
+
label: `${s.path === currentFile ? "\u263E " : ""}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || "(empty)"}`,
|
|
1816
|
+
value: s.path
|
|
1817
|
+
}));
|
|
1818
|
+
if (!(await createPicker(ctx.adapter, ctx.event.source, {
|
|
1819
|
+
kind: "sessions",
|
|
1820
|
+
sessionKey: ctx.key,
|
|
1821
|
+
invokerId: ctx.event.source.userId,
|
|
1822
|
+
items,
|
|
1823
|
+
perPage: 8,
|
|
1824
|
+
title: "Pick a session",
|
|
1825
|
+
async resolve(item) {
|
|
1826
|
+
await ctx.bridge.switchSession(ctx.key, item.value);
|
|
1827
|
+
let switched2 = await ctx.bridge.getSession(ctx.key), name2 = switched2?.sessionManager?.getSessionName() ?? "unnamed", count2 = switched2?.sessionManager?.getEntries().length ?? 0;
|
|
1828
|
+
return { done: !0, text: `\u263E Switched to "${name2}" (${count2} msgs). History continues here.` };
|
|
1829
|
+
}
|
|
1830
|
+
}, { replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId })).success) {
|
|
1831
|
+
let lines = sessions.map((s, i) => {
|
|
1832
|
+
let marker = s.path === currentFile ? " \u263E" : "", label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || "(empty)";
|
|
1833
|
+
return `${i + 1}) ${label}${marker} \xB7 ${formatRelativeTime(s.modified)} \xB7 ${s.messageCount} msgs`;
|
|
1834
|
+
});
|
|
1835
|
+
await ctx.reply([`Sessions for ${process.cwd()}`, ...lines, "Use /sessions <n> or /sessions <id-prefix>"].join(`
|
|
1836
|
+
`));
|
|
1837
|
+
}
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
let path, numeric = Number.parseInt(arg, 10), cached = getCachedSessions(ctx.key);
|
|
1841
|
+
if (!Number.isNaN(numeric) && /^\d+$/.test(arg)) {
|
|
1842
|
+
if (!cached || numeric < 1 || numeric > cached.length) {
|
|
1843
|
+
await ctx.reply("Invalid selection \u2014 run /sessions to see the current list.");
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
path = cached[numeric - 1].path;
|
|
1847
|
+
} else {
|
|
1848
|
+
let matches = (cached ?? await SessionManager.list(process.cwd())).filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));
|
|
1849
|
+
if (matches.length === 0) {
|
|
1850
|
+
await ctx.reply(`No session matches "${arg}".`);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (matches.length > 1) {
|
|
1854
|
+
await ctx.reply(`Multiple sessions match "${arg}" \u2014 use a longer prefix.`);
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
path = matches[0].path;
|
|
1858
|
+
}
|
|
1859
|
+
if (!path) {
|
|
1860
|
+
await ctx.reply("No session selected.");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
await ctx.bridge.switchSession(ctx.key, path);
|
|
1864
|
+
let switched = await ctx.bridge.getSession(ctx.key), name = switched?.sessionManager?.getSessionName() ?? "unnamed", count = switched?.sessionManager?.getEntries().length ?? 0;
|
|
1865
|
+
await ctx.reply(`Switched to "${name}" (${count} msgs). History continues here.`);
|
|
1866
|
+
}
|
|
1867
|
+
}, titleCommand = {
|
|
1868
|
+
name: "title",
|
|
1869
|
+
aliases: ["name"],
|
|
1870
|
+
description: "name the current session",
|
|
1871
|
+
bypassBusy: !0,
|
|
1872
|
+
needsSession: !0,
|
|
1873
|
+
async handler(ctx) {
|
|
1874
|
+
let session = ctx.session, name = ctx.args.trim();
|
|
1875
|
+
if (!name) {
|
|
1876
|
+
let current = session.sessionManager?.getSessionName();
|
|
1877
|
+
await ctx.reply(current ? `Session title: ${current}` : "(unnamed)");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
session.setSessionName(name);
|
|
1881
|
+
let readBack = session.sessionManager?.getSessionName() ?? name;
|
|
1882
|
+
await ctx.reply(`Session titled "${readBack}".`);
|
|
1883
|
+
}
|
|
1884
|
+
}, contextCommand = {
|
|
1885
|
+
name: "context",
|
|
1886
|
+
description: "estimated context window breakdown",
|
|
1887
|
+
bypassBusy: !0,
|
|
1888
|
+
needsSession: !0,
|
|
1889
|
+
async handler(ctx) {
|
|
1890
|
+
let session = ctx.session, model = session.model;
|
|
1891
|
+
if (!model) {
|
|
1892
|
+
await ctx.reply("No model selected.");
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
let contextWindow = model.contextWindow ?? 0;
|
|
1896
|
+
if (contextWindow <= 0) {
|
|
1897
|
+
await ctx.reply("Context window unknown for this model.");
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
let tools = session.getActiveToolNames().map((name) => session.getToolDefinition(name)).filter((d) => d !== void 0), breakdown = computeContextBreakdown({
|
|
1901
|
+
systemPrompt: session.systemPrompt,
|
|
1902
|
+
tools,
|
|
1903
|
+
messages: session.messages,
|
|
1904
|
+
contextWindow
|
|
1905
|
+
}), usage = session.getContextUsage(), tokens = usage?.tokens ?? breakdown.total, percent = usage?.percent ?? (contextWindow > 0 ? tokens / contextWindow * 100 : 0), messageTokens = breakdown.user + breakdown.assistantText + breakdown.thinking + breakdown.toolCalls + breakdown.toolResults + breakdown.summaries, stats = session.getSessionStats();
|
|
1906
|
+
await ctx.reply([
|
|
1907
|
+
`Context: ${tokens.toLocaleString("en-US")} / ${contextWindow.toLocaleString("en-US")} tokens (${Math.round(percent)}%)`,
|
|
1908
|
+
` system ${breakdown.systemPrompt.toLocaleString("en-US")} \xB7 tools ${breakdown.toolDefinitions.toLocaleString("en-US")} \xB7 messages ${messageTokens.toLocaleString("en-US")}`,
|
|
1909
|
+
`Session: ${stats.totalMessages} messages \xB7 ${stats.userMessages} turns \xB7 model ${model.provider}/${model.id}`
|
|
1910
|
+
].join(`
|
|
1911
|
+
`));
|
|
1912
|
+
}
|
|
1913
|
+
}, compactCommand = {
|
|
1914
|
+
name: "compact",
|
|
1915
|
+
description: "summarize and compress session context",
|
|
1916
|
+
bypassBusy: !0,
|
|
1917
|
+
needsSession: !0,
|
|
1918
|
+
async handler(ctx) {
|
|
1919
|
+
let session = ctx.session, instructions = ctx.args.trim() || void 0;
|
|
1920
|
+
try {
|
|
1921
|
+
let result = await session.compact(instructions), before = result.tokensBefore.toLocaleString("en-US"), after = result.estimatedTokensAfter !== void 0 ? `~${result.estimatedTokensAfter.toLocaleString("en-US")}` : "~?";
|
|
1922
|
+
await ctx.reply(`Compacted: ${before} \u2192 ${after} tokens.`);
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1925
|
+
if (message.includes("Nothing to compact") || message.includes("Already compacted") || message.includes("No model selected")) {
|
|
1926
|
+
await ctx.reply(message);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}, thinkingCommand = {
|
|
1933
|
+
name: "thinking",
|
|
1934
|
+
aliases: ["effort", "reasoning"],
|
|
1935
|
+
description: "get or set the thinking level",
|
|
1936
|
+
bypassBusy: !1,
|
|
1937
|
+
needsSession: !0,
|
|
1938
|
+
async handler(ctx) {
|
|
1939
|
+
let session = ctx.session, levels = session.getAvailableThinkingLevels();
|
|
1940
|
+
if (!session.supportsThinking()) {
|
|
1941
|
+
await ctx.reply(`Model ${session.model?.provider ?? "none"}/${session.model?.id ?? "none"} doesn't support thinking.`);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
let arg = ctx.args.trim().toLowerCase();
|
|
1945
|
+
if (!arg) {
|
|
1946
|
+
let current = session.thinkingLevel, items = levels.map((level) => ({
|
|
1947
|
+
label: `${level === current ? "\u2713 " : ""}${level}`,
|
|
1948
|
+
value: level
|
|
1949
|
+
}));
|
|
1950
|
+
(await createPicker(ctx.adapter, ctx.event.source, {
|
|
1951
|
+
kind: "thinking",
|
|
1952
|
+
sessionKey: ctx.key,
|
|
1953
|
+
invokerId: ctx.event.source.userId,
|
|
1954
|
+
items,
|
|
1955
|
+
perPage: levels.length,
|
|
1956
|
+
title: "Pick a thinking level",
|
|
1957
|
+
async resolve(item) {
|
|
1958
|
+
let level = item.value;
|
|
1959
|
+
return session.setThinkingLevel(level), { done: !0, text: `\u263E Thinking \u2192 ${level}` };
|
|
1960
|
+
}
|
|
1961
|
+
}, { replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId })).success || await ctx.reply(`Level: ${current} \u2014 available: ${levels.join(", ")}`);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (!levels.includes(arg)) {
|
|
1965
|
+
await ctx.reply(`Invalid level. Available: ${levels.join(", ")}`);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
session.setThinkingLevel(arg), await ctx.reply(`Thinking \u2192 ${arg}`);
|
|
1969
|
+
}
|
|
1970
|
+
}, CHAT_COMMANDS = [
|
|
1971
|
+
newCommand,
|
|
1972
|
+
undoCommand,
|
|
1973
|
+
redoCommand,
|
|
1974
|
+
stopCommand,
|
|
1975
|
+
statusCommand,
|
|
1976
|
+
whoamiCommand,
|
|
1977
|
+
helpCommand,
|
|
1978
|
+
modelCommand,
|
|
1979
|
+
sessionsCommand,
|
|
1980
|
+
titleCommand,
|
|
1981
|
+
contextCommand,
|
|
1982
|
+
compactCommand,
|
|
1983
|
+
thinkingCommand
|
|
1984
|
+
];
|
|
1985
|
+
function botCommandSpecs() {
|
|
1986
|
+
return CHAT_COMMANDS.map((c) => ({ name: c.name, description: c.description }));
|
|
1987
|
+
}
|
|
1988
|
+
function formatHelpText() {
|
|
1989
|
+
let lines = ["lunR gateway commands:"];
|
|
1990
|
+
for (let cmd of CHAT_COMMANDS) {
|
|
1991
|
+
let names = [cmd.name, ...cmd.aliases ?? []].map((a) => `/${a}`).join(" | ");
|
|
1992
|
+
lines.push(`${names} \u2014 ${cmd.description}`);
|
|
1993
|
+
}
|
|
1994
|
+
return lines.push(""), lines.push("Tap to pick: /model, /thinking, /sessions (run without args to see buttons)."), lines.join(`
|
|
1995
|
+
`);
|
|
1996
|
+
}
|
|
1997
|
+
async function runChatCommand(cmd, ctx) {
|
|
1998
|
+
if (!cmd.bypassBusy && ctx.bridge.getStatus(ctx.key).busy)
|
|
1999
|
+
return await ctx.reply("\u23F3 busy \u2014 /stop first or wait"), !0;
|
|
2000
|
+
if (cmd.needsSession) {
|
|
2001
|
+
let session = await ctx.bridge.getSession(ctx.key);
|
|
2002
|
+
if (!session)
|
|
2003
|
+
return await ctx.reply("No session yet \u2014 send a message first."), !0;
|
|
2004
|
+
ctx.session = session;
|
|
2005
|
+
}
|
|
2006
|
+
try {
|
|
2007
|
+
return await cmd.handler(ctx) ?? !0;
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
2010
|
+
return await ctx.adapter.send(ctx.event.source.chatId, `\u26A0 ${oneLine(message)}`, {
|
|
2011
|
+
replyTo: ctx.event.messageId,
|
|
2012
|
+
threadId: ctx.event.source.threadId
|
|
2013
|
+
}), !0;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async function sendCommandReply(adapter, event, text) {
|
|
2017
|
+
await adapter.send(event.source.chatId, prefixLines(text, "\u263E "), {
|
|
2018
|
+
replyTo: event.messageId,
|
|
2019
|
+
threadId: event.source.threadId
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// packages/coding-agent/dist/gateway/pairing.js
|
|
2024
|
+
import { randomInt } from "node:crypto";
|
|
2025
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2026
|
+
import { join as join2 } from "node:path";
|
|
2027
|
+
var CODE_ALPHABET3 = "ABCDEFGHJKMNPQRSTUVWXYZ23456789", CODE_LENGTH2 = 8, DEFAULT_TTL_MS = 3600 * 1e3, DEFAULT_RATE_LIMIT_MS = 600 * 1e3, DEFAULT_MAX_PENDING = 3, DEFAULT_MAX_FAILS = 5, DEFAULT_LOCKOUT_MS = 3600 * 1e3;
|
|
2028
|
+
function normalizeCode(code) {
|
|
2029
|
+
return code.replace(/-/g, "").trim().toUpperCase();
|
|
2030
|
+
}
|
|
2031
|
+
function generateCode() {
|
|
2032
|
+
let code = "";
|
|
2033
|
+
for (let i = 0; i < CODE_LENGTH2; i++)
|
|
2034
|
+
code += CODE_ALPHABET3[randomInt(0, CODE_ALPHABET3.length)];
|
|
2035
|
+
return code;
|
|
2036
|
+
}
|
|
2037
|
+
function createPairingStore(options = {}) {
|
|
2038
|
+
let dir = options.dir ?? join2(getAgentDir(), "pairing"), ttlMs = options.ttlMs ?? DEFAULT_TTL_MS, rateLimitMs = options.rateLimitMs ?? DEFAULT_RATE_LIMIT_MS, maxPending = options.maxPending ?? DEFAULT_MAX_PENDING, maxFails = options.maxFails ?? DEFAULT_MAX_FAILS, lockoutMs = options.lockoutMs ?? DEFAULT_LOCKOUT_MS, now = options.now ?? (() => Date.now());
|
|
2039
|
+
function storeFile() {
|
|
2040
|
+
return join2(dir, "store.json");
|
|
2041
|
+
}
|
|
2042
|
+
function read() {
|
|
2043
|
+
let path = storeFile();
|
|
2044
|
+
if (!existsSync3(path))
|
|
2045
|
+
return { pending: [], approved: [] };
|
|
2046
|
+
try {
|
|
2047
|
+
let parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
2048
|
+
if (parsed == null || typeof parsed != "object" || Array.isArray(parsed))
|
|
2049
|
+
return { pending: [], approved: [] };
|
|
2050
|
+
let p = parsed, pending2 = Array.isArray(p.pending) ? p.pending : [], approved = Array.isArray(p.approved) ? p.approved : [];
|
|
2051
|
+
return {
|
|
2052
|
+
pending: pending2.filter((c) => typeof c?.code == "string" && typeof c?.platform == "string" && typeof c?.userId == "string"),
|
|
2053
|
+
approved: approved.filter((u) => typeof u?.platform == "string" && typeof u?.userId == "string"),
|
|
2054
|
+
failedAttempts: typeof p.failedAttempts == "object" && p.failedAttempts !== null && !Array.isArray(p.failedAttempts) ? p.failedAttempts : void 0,
|
|
2055
|
+
lockedOutUntil: typeof p.lockedOutUntil == "object" && p.lockedOutUntil !== null && !Array.isArray(p.lockedOutUntil) ? p.lockedOutUntil : void 0
|
|
2056
|
+
};
|
|
2057
|
+
} catch {
|
|
2058
|
+
return { pending: [], approved: [] };
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function write(data) {
|
|
2062
|
+
existsSync3(dir) || mkdirSync2(dir, { recursive: !0 });
|
|
2063
|
+
let path = storeFile(), tmp = `${path}.tmp-${process.pid}`;
|
|
2064
|
+
writeFileSync2(tmp, `${JSON.stringify(data, null, 2)}
|
|
2065
|
+
`, "utf-8"), renameSync2(tmp, path);
|
|
2066
|
+
try {
|
|
2067
|
+
chmodSync(path, 384);
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
function isLockedOut(platform) {
|
|
2072
|
+
let until = read().lockedOutUntil?.[platform] ?? 0;
|
|
2073
|
+
return now() < until;
|
|
2074
|
+
}
|
|
2075
|
+
function recordFailedAttempt(platform) {
|
|
2076
|
+
let data = read(), failed = data.failedAttempts ?? {};
|
|
2077
|
+
if (failed[platform] = (failed[platform] ?? 0) + 1, failed[platform] >= maxFails) {
|
|
2078
|
+
let locked = data.lockedOutUntil ?? {};
|
|
2079
|
+
locked[platform] = now() + lockoutMs, failed[platform] = 0, data.lockedOutUntil = locked;
|
|
2080
|
+
}
|
|
2081
|
+
data.failedAttempts = failed, write(data);
|
|
2082
|
+
}
|
|
2083
|
+
function livePending(data) {
|
|
2084
|
+
let cutoff = now() - ttlMs;
|
|
2085
|
+
return data.pending.filter((c) => c.issuedAt >= cutoff);
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
issueCode(platform, userId) {
|
|
2089
|
+
if (isLockedOut(platform))
|
|
2090
|
+
return null;
|
|
2091
|
+
let data = read(), lastIssued = data.pending.filter((c) => c.platform === platform && c.userId === userId).reduce((max, c) => Math.max(max, c.issuedAt), 0);
|
|
2092
|
+
if (lastIssued > 0 && now() - lastIssued < rateLimitMs)
|
|
2093
|
+
return null;
|
|
2094
|
+
let live = livePending(data);
|
|
2095
|
+
if (live.filter((c) => c.platform === platform).length >= maxPending)
|
|
2096
|
+
return null;
|
|
2097
|
+
let entry = {
|
|
2098
|
+
code: generateCode(),
|
|
2099
|
+
platform,
|
|
2100
|
+
userId,
|
|
2101
|
+
issuedAt: now()
|
|
2102
|
+
};
|
|
2103
|
+
return data.pending = [...live, entry], write(data), entry.code;
|
|
2104
|
+
},
|
|
2105
|
+
approve(platform, code) {
|
|
2106
|
+
if (isLockedOut(platform))
|
|
2107
|
+
return null;
|
|
2108
|
+
let data = read(), live = livePending(data), normalized = normalizeCode(code), match = live.find((c) => c.platform === platform && c.code === normalized);
|
|
2109
|
+
return match ? (data.pending = live.filter((c) => c !== match), data.approved = [
|
|
2110
|
+
...data.approved.filter((u) => !(u.platform === platform && u.userId === match.userId)),
|
|
2111
|
+
{ platform, userId: match.userId, approvedAt: now() }
|
|
2112
|
+
], write(data), match.userId) : (recordFailedAttempt(platform), null);
|
|
2113
|
+
},
|
|
2114
|
+
isPaired(platform, userId) {
|
|
2115
|
+
return read().approved.some((u) => u.platform === platform && u.userId === userId);
|
|
2116
|
+
},
|
|
2117
|
+
listPending() {
|
|
2118
|
+
let data = read();
|
|
2119
|
+
return livePending(data).map((c) => ({
|
|
2120
|
+
code: c.code,
|
|
2121
|
+
platform: c.platform,
|
|
2122
|
+
userId: c.userId,
|
|
2123
|
+
expiresAt: c.issuedAt + ttlMs
|
|
2124
|
+
}));
|
|
2125
|
+
},
|
|
2126
|
+
listApproved() {
|
|
2127
|
+
return read().approved.map((u) => ({ platform: u.platform, userId: u.userId, approvedAt: u.approvedAt }));
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// packages/coding-agent/dist/gateway/router.js
|
|
2133
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
2134
|
+
|
|
2135
|
+
// packages/coding-agent/dist/gateway/session-keys.js
|
|
2136
|
+
var SESSION_KEY_PREFIX = "agent:main";
|
|
2137
|
+
function sanitize(component) {
|
|
2138
|
+
return component.replace(/[\s:]+/g, "_");
|
|
2139
|
+
}
|
|
2140
|
+
function buildSessionKey(source, options) {
|
|
2141
|
+
let parts = [source.platform, source.chatType, source.chatId].map(sanitize);
|
|
2142
|
+
return source.threadId ? parts.push(sanitize(source.threadId)) : (source.chatType === "group" || source.chatType === "channel") && options.groupSessionsPerUser && parts.push(sanitize(source.userId)), [SESSION_KEY_PREFIX, ...parts].join(":");
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// packages/coding-agent/dist/gateway/stream.js
|
|
2146
|
+
var SILENCE_MARKERS = /* @__PURE__ */ new Set(["[SILENT]", "NO_REPLY"]);
|
|
2147
|
+
function isMarkerLine(line) {
|
|
2148
|
+
return SILENCE_MARKERS.has(line.trim());
|
|
2149
|
+
}
|
|
2150
|
+
function applySilenceFilter(text) {
|
|
2151
|
+
let trimmed = text.trim();
|
|
2152
|
+
if (trimmed.length === 0 || isMarkerLine(trimmed))
|
|
2153
|
+
return null;
|
|
2154
|
+
let lines = trimmed.split(`
|
|
2155
|
+
`);
|
|
2156
|
+
lines.length > 0 && isMarkerLine(lines[0]) && lines.shift(), lines.length > 0 && isMarkerLine(lines[lines.length - 1]) && lines.pop();
|
|
2157
|
+
let result = lines.join(`
|
|
2158
|
+
`).trim();
|
|
2159
|
+
return result.length > 0 ? result : null;
|
|
2160
|
+
}
|
|
2161
|
+
var StreamConsumer = class {
|
|
2162
|
+
options;
|
|
2163
|
+
buffer = "";
|
|
2164
|
+
flushedLen = 0;
|
|
2165
|
+
messageId = null;
|
|
2166
|
+
lastFlushAt = 0;
|
|
2167
|
+
chain = Promise.resolve();
|
|
2168
|
+
maxPreview;
|
|
2169
|
+
now;
|
|
2170
|
+
constructor(options) {
|
|
2171
|
+
this.options = options, this.maxPreview = options.maxPreview ?? 4096, this.now = options.now ?? (() => Date.now());
|
|
2172
|
+
}
|
|
2173
|
+
/** Accumulate a delta; schedule a flush when both gates (chars, interval) pass. */
|
|
2174
|
+
push(delta) {
|
|
2175
|
+
this.buffer += delta, this.buffer.length - this.flushedLen >= this.options.threshold && this.now() - this.lastFlushAt >= this.options.intervalMs && this.schedule();
|
|
2176
|
+
}
|
|
2177
|
+
/** Force a final flush; resolves with the FULL text (caller splits for delivery). */
|
|
2178
|
+
async finalize() {
|
|
2179
|
+
return this.schedule(!0), await this.chain, this.buffer;
|
|
2180
|
+
}
|
|
2181
|
+
/** The platform message id of the preview, when one was successfully sent. */
|
|
2182
|
+
get sentMessageId() {
|
|
2183
|
+
return this.messageId;
|
|
2184
|
+
}
|
|
2185
|
+
/** True when the preview had to truncate the full text (buffer > maxPreview). */
|
|
2186
|
+
get truncated() {
|
|
2187
|
+
return this.buffer.length > this.maxPreview;
|
|
2188
|
+
}
|
|
2189
|
+
/** The streaming preview: full text truncated to maxPreview-3 + "…". */
|
|
2190
|
+
preview() {
|
|
2191
|
+
if (this.buffer.length <= this.maxPreview)
|
|
2192
|
+
return this.buffer;
|
|
2193
|
+
let cut = this.maxPreview - 3, code = this.buffer.charCodeAt(cut - 1);
|
|
2194
|
+
return code >= 55296 && code <= 56319 && (cut -= 1), `${this.buffer.slice(0, cut)}\u2026`;
|
|
2195
|
+
}
|
|
2196
|
+
schedule(force = !1) {
|
|
2197
|
+
this.chain = this.chain.then(() => this.flush(force)).catch(() => {
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
async flush(force) {
|
|
2201
|
+
if (!(this.buffer.length > this.flushedLen) || !force && this.messageId !== null && this.now() - this.lastFlushAt < this.options.intervalMs)
|
|
2202
|
+
return;
|
|
2203
|
+
let text = this.preview();
|
|
2204
|
+
if (this.messageId === null) {
|
|
2205
|
+
if (this.messageId = await this.options.sendInitial(text), this.messageId === null)
|
|
2206
|
+
return;
|
|
2207
|
+
} else
|
|
2208
|
+
await this.options.edit(this.messageId, text);
|
|
2209
|
+
this.flushedLen = this.buffer.length, this.lastFlushAt = this.now();
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
// packages/coding-agent/dist/gateway/router.js
|
|
2214
|
+
function oneLine2(text) {
|
|
2215
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2216
|
+
}
|
|
2217
|
+
function formatPairingCode(code) {
|
|
2218
|
+
return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
|
|
2219
|
+
}
|
|
2220
|
+
function mentionedBot(event) {
|
|
2221
|
+
return event.metadata?.mentionedBot === !0;
|
|
2222
|
+
}
|
|
2223
|
+
function createRouter(deps) {
|
|
2224
|
+
let { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;
|
|
2225
|
+
registerGatewayApprovalHandler();
|
|
2226
|
+
function freshCfg() {
|
|
2227
|
+
if (!reloadConfig)
|
|
2228
|
+
return initialCfg;
|
|
2229
|
+
try {
|
|
2230
|
+
if (existsSync4(gatewayConfigPath()))
|
|
2231
|
+
return loadGatewayConfig();
|
|
2232
|
+
} catch {
|
|
2233
|
+
}
|
|
2234
|
+
return initialCfg;
|
|
2235
|
+
}
|
|
2236
|
+
async function sendError(adapter, event, message) {
|
|
2237
|
+
await adapter.send(event.source.chatId, `\u26A0 ${oneLine2(message)}`, {
|
|
2238
|
+
replyTo: event.messageId,
|
|
2239
|
+
threadId: event.source.threadId
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
function isGroupGated(event, cfg) {
|
|
2243
|
+
let { source } = event;
|
|
2244
|
+
if (source.chatType === "dm")
|
|
2245
|
+
return !1;
|
|
2246
|
+
let platformCfg = platformConfigFor(cfg, source.platform);
|
|
2247
|
+
if (!platformCfg)
|
|
2248
|
+
return !0;
|
|
2249
|
+
let freeResponse = platformCfg.freeResponseChats.includes(source.chatId);
|
|
2250
|
+
return !!(platformCfg.allowedChats.length > 0 && !platformCfg.allowedChats.includes(source.chatId) && !freeResponse && !mentionedBot(event) || platformCfg.requireMention && !freeResponse && !mentionedBot(event));
|
|
2251
|
+
}
|
|
2252
|
+
async function isDenied(adapter, event, cfg) {
|
|
2253
|
+
if (isAuthorized(event.source, cfg, pairing))
|
|
2254
|
+
return !1;
|
|
2255
|
+
let { source } = event;
|
|
2256
|
+
if (source.chatType !== "dm" || cfg.unauthorizedDmBehavior !== "pair")
|
|
2257
|
+
return !0;
|
|
2258
|
+
let code = pairing.issueCode(source.platform, source.userId);
|
|
2259
|
+
return code === null || await adapter.send(source.chatId, `Your lunR pairing code: ${formatPairingCode(code)} \u2014 ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`, { replyTo: event.messageId, threadId: source.threadId }), !0;
|
|
2260
|
+
}
|
|
2261
|
+
async function handleSlash(adapter, event, key) {
|
|
2262
|
+
let text = event.text.trim();
|
|
2263
|
+
if (!text.startsWith("/"))
|
|
2264
|
+
return !1;
|
|
2265
|
+
let firstToken = text.split(/\s+/, 1)[0].toLowerCase(), commandWord = firstToken.split("@")[0].slice(1), args = text.slice(firstToken.length).trim(), cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));
|
|
2266
|
+
return cmd ? runChatCommand(cmd, {
|
|
2267
|
+
event,
|
|
2268
|
+
key,
|
|
2269
|
+
adapter,
|
|
2270
|
+
bridge,
|
|
2271
|
+
args,
|
|
2272
|
+
reply: (message) => sendCommandReply(adapter, event, message)
|
|
2273
|
+
}) : event.source.chatType !== "dm";
|
|
2274
|
+
}
|
|
2275
|
+
async function deliver(adapter, event, text, opts) {
|
|
2276
|
+
let filtered = applySilenceFilter(text);
|
|
2277
|
+
if (filtered === null)
|
|
2278
|
+
return;
|
|
2279
|
+
let chunks = splitMessage(filtered, adapter.maxMessageLength), startIndex = 0;
|
|
2280
|
+
if (opts.editMessageId) {
|
|
2281
|
+
if (!opts.previewTruncated) {
|
|
2282
|
+
await adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
await adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]), startIndex = 1;
|
|
2286
|
+
}
|
|
2287
|
+
for (let index = startIndex; index < chunks.length; index++) {
|
|
2288
|
+
let result = await adapter.send(event.source.chatId, chunks[index], {
|
|
2289
|
+
replyTo: opts.reply && index === 0 ? event.messageId : void 0,
|
|
2290
|
+
threadId: event.source.threadId
|
|
2291
|
+
});
|
|
2292
|
+
if (!result.success) {
|
|
2293
|
+
console.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? "unknown error"}`);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
async function runTurn(adapter, event, key, cfg) {
|
|
2299
|
+
let streaming = cfg.streaming.enabled && typeof adapter.editMessage == "function", consumer;
|
|
2300
|
+
streaming && (consumer = new StreamConsumer({
|
|
2301
|
+
sendInitial: async (text) => {
|
|
2302
|
+
let result2 = await adapter.send(event.source.chatId, text, {
|
|
2303
|
+
replyTo: event.messageId,
|
|
2304
|
+
threadId: event.source.threadId
|
|
2305
|
+
});
|
|
2306
|
+
return result2.success ? result2.messageId ?? null : null;
|
|
2307
|
+
},
|
|
2308
|
+
edit: async (messageId, text) => {
|
|
2309
|
+
await adapter.editMessage(event.source.chatId, messageId, text);
|
|
2310
|
+
},
|
|
2311
|
+
intervalMs: cfg.streaming.editIntervalMs,
|
|
2312
|
+
threshold: cfg.streaming.bufferThreshold,
|
|
2313
|
+
maxPreview: adapter.maxMessageLength
|
|
2314
|
+
}));
|
|
2315
|
+
let callbacks = {
|
|
2316
|
+
onDelta: consumer ? (delta) => consumer.push(delta) : void 0,
|
|
2317
|
+
onFollowUpResult: (text) => {
|
|
2318
|
+
deliver(adapter, event, text, { reply: !1 });
|
|
2319
|
+
},
|
|
2320
|
+
onError: (message) => {
|
|
2321
|
+
sendError(adapter, event, message);
|
|
2322
|
+
}
|
|
2323
|
+
}, result = await runWithApprovalContext({ key, adapter, source: event.source }, () => bridge.runTurn(key, event, callbacks));
|
|
2324
|
+
result !== QUEUED && (consumer && await consumer.finalize(), await deliver(adapter, event, result, {
|
|
2325
|
+
reply: !consumer,
|
|
2326
|
+
editMessageId: consumer?.sentMessageId ?? void 0,
|
|
2327
|
+
previewTruncated: consumer?.truncated ?? !1
|
|
2328
|
+
}));
|
|
2329
|
+
}
|
|
2330
|
+
return {
|
|
2331
|
+
async handleEvent(event) {
|
|
2332
|
+
let adapter = adapters.get(event.source.platform);
|
|
2333
|
+
if (!adapter)
|
|
2334
|
+
return;
|
|
2335
|
+
let cfg = freshCfg();
|
|
2336
|
+
try {
|
|
2337
|
+
if (isGroupGated(event, cfg) || await isDenied(adapter, event, cfg))
|
|
2338
|
+
return;
|
|
2339
|
+
let key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });
|
|
2340
|
+
if (await handleSlash(adapter, event, key))
|
|
2341
|
+
return;
|
|
2342
|
+
await adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {
|
|
2343
|
+
}), await runTurn(adapter, event, key, cfg);
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
await sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => {
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// packages/coding-agent/dist/gateway/index.js
|
|
2353
|
+
var ADAPTER_FACTORIES = {
|
|
2354
|
+
telegram: (cfg) => new TelegramAdapter(cfg),
|
|
2355
|
+
discord: (cfg) => new DiscordAdapter(cfg)
|
|
2356
|
+
}, KNOWN_PLATFORMS = ["telegram", "discord"], SETUP_INSTRUCTIONS = {
|
|
2357
|
+
telegram: [
|
|
2358
|
+
"Telegram: talk to @BotFather \u2192 /newbot \u2192 copy the token into",
|
|
2359
|
+
" <agentDir>/gateway.json (telegram.token) or export LUNR_TELEGRAM_BOT_TOKEN,",
|
|
2360
|
+
" then set telegram.enabled = true."
|
|
2361
|
+
],
|
|
2362
|
+
discord: [
|
|
2363
|
+
"Discord: discord.com/developers \u2192 New Application \u2192 Bot \u2192 copy the token into",
|
|
2364
|
+
" <agentDir>/gateway.json (discord.token) or export LUNR_DISCORD_BOT_TOKEN,",
|
|
2365
|
+
" enable the MESSAGE CONTENT intent, then set discord.enabled = true."
|
|
2366
|
+
]
|
|
2367
|
+
};
|
|
2368
|
+
function printSetupInstructions(enabledButUnrunnable) {
|
|
2369
|
+
console.log("lunR gateway: no runnable platforms configured.");
|
|
2370
|
+
for (let platform of enabledButUnrunnable)
|
|
2371
|
+
console.log(` (platform "${platform}" is enabled but has no registered adapter or resolvable token)`);
|
|
2372
|
+
for (let platform of KNOWN_PLATFORMS)
|
|
2373
|
+
for (let line of SETUP_INSTRUCTIONS[platform])
|
|
2374
|
+
console.log(line);
|
|
2375
|
+
}
|
|
2376
|
+
async function runPairApprove(platform, code) {
|
|
2377
|
+
if (!code)
|
|
2378
|
+
return console.error("Usage: lunr gateway pair approve <platform> <code>"), 1;
|
|
2379
|
+
let userId = createPairingStore().approve(platform, code);
|
|
2380
|
+
if (userId === null)
|
|
2381
|
+
return console.error(`Pairing failed: no valid pending code "${code}" for ${platform} (wrong, expired, or locked out).`), 1;
|
|
2382
|
+
try {
|
|
2383
|
+
addAllowedUser(platform, userId);
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
return console.error(`Paired ${platform} user ${userId}, but could not persist allowedUsers: ${err}`), 1;
|
|
2386
|
+
}
|
|
2387
|
+
return console.log(`Approved ${platform} user ${userId} (added to ${platform}.allowedUsers in gateway.json).`), 0;
|
|
2388
|
+
}
|
|
2389
|
+
function runPairList() {
|
|
2390
|
+
let pairing = createPairingStore(), pending2 = pairing.listPending(), approved = pairing.listApproved();
|
|
2391
|
+
if (console.log("Pending pairing codes:"), pending2.length === 0)
|
|
2392
|
+
console.log(" (none)");
|
|
2393
|
+
else
|
|
2394
|
+
for (let p of pending2)
|
|
2395
|
+
console.log(` ${p.platform} ${p.code} user=${p.userId} expires ${new Date(p.expiresAt).toISOString()}`);
|
|
2396
|
+
if (console.log("Paired users:"), approved.length === 0)
|
|
2397
|
+
console.log(" (none)");
|
|
2398
|
+
else
|
|
2399
|
+
for (let u of approved)
|
|
2400
|
+
console.log(` ${u.platform} ${u.userId} approved ${new Date(u.approvedAt).toISOString()}`);
|
|
2401
|
+
return 0;
|
|
2402
|
+
}
|
|
2403
|
+
function runStatus() {
|
|
2404
|
+
let cfg = loadGatewayConfig();
|
|
2405
|
+
console.log(`chat-platforms: ${isFeatureEnabled("chat-platforms") ? "enabled" : "disabled"} (install-features.json)`), console.log("lunR gateway status:");
|
|
2406
|
+
for (let platform of KNOWN_PLATFORMS) {
|
|
2407
|
+
let platformCfg = platformConfigFor(cfg, platform);
|
|
2408
|
+
if (!platformCfg)
|
|
2409
|
+
continue;
|
|
2410
|
+
let token = resolvePlatformToken(platform, platformCfg), adapter = ADAPTER_FACTORIES[platform] === void 0 ? "no adapter" : "adapter registered";
|
|
2411
|
+
console.log(` ${platform}: ${platformCfg.enabled ? "enabled" : "disabled"} \xB7 token ${token ? "resolved" : "missing"} \xB7 ${adapter}`);
|
|
2412
|
+
}
|
|
2413
|
+
let sessions = listSessions();
|
|
2414
|
+
return console.log(` sessions: ${Object.keys(sessions).length} stored`), 0;
|
|
2415
|
+
}
|
|
2416
|
+
async function runDaemon() {
|
|
2417
|
+
if (!isFeatureEnabled("chat-platforms"))
|
|
2418
|
+
return console.error("lunR gateway: chat platforms are not enabled."), console.error("Enable with: lunr features enable chat-platforms"), 1;
|
|
2419
|
+
let cfg = loadGatewayConfig(), adapters = /* @__PURE__ */ new Map(), skipped = [];
|
|
2420
|
+
for (let platform of KNOWN_PLATFORMS) {
|
|
2421
|
+
let platformCfg = platformConfigFor(cfg, platform);
|
|
2422
|
+
if (!platformCfg?.enabled)
|
|
2423
|
+
continue;
|
|
2424
|
+
let factory = ADAPTER_FACTORIES[platform];
|
|
2425
|
+
if (factory === void 0) {
|
|
2426
|
+
skipped.push(platform);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
let token = resolvePlatformToken(platform, platformCfg);
|
|
2430
|
+
if (!token) {
|
|
2431
|
+
skipped.push(platform);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
adapters.set(platform, factory({ ...platformCfg, token }));
|
|
2435
|
+
}
|
|
2436
|
+
if (adapters.size === 0)
|
|
2437
|
+
return printSetupInstructions(skipped), 1;
|
|
2438
|
+
let pairing = createPairingStore(), bridge = new AgentBridge(), router = createRouter({ adapters, cfg, pairing, bridge, reloadConfig: !0 }), connected = [];
|
|
2439
|
+
startButtonSweeper();
|
|
2440
|
+
for (let [platform, adapter] of adapters)
|
|
2441
|
+
try {
|
|
2442
|
+
if (!await adapter.connect()) {
|
|
2443
|
+
console.error(`[gateway] ${platform}: connect() reported failure; skipping`);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
adapter.onMessage((event) => {
|
|
2447
|
+
router.handleEvent(event);
|
|
2448
|
+
}), adapter.onCallback(async (event) => {
|
|
2449
|
+
await handleApprovalCallback(event, adapter) || handleCallback(event, { adapters, cfg, pairing, bridge, adapter });
|
|
2450
|
+
});
|
|
2451
|
+
try {
|
|
2452
|
+
await adapter.registerCommands?.(botCommandSpecs());
|
|
2453
|
+
} catch (err) {
|
|
2454
|
+
console.error(`[gateway] ${platform}: command menu registration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2455
|
+
}
|
|
2456
|
+
connected.push(adapter), console.log(`lunR gateway: ${platform} connected`);
|
|
2457
|
+
} catch (err) {
|
|
2458
|
+
console.error(`[gateway] ${platform}: connect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2459
|
+
}
|
|
2460
|
+
if (connected.length === 0)
|
|
2461
|
+
return console.error("lunR gateway: every configured platform failed to connect."), stopButtonSweeper(), 1;
|
|
2462
|
+
let cron = startGatewayCron({ adapters, cfg });
|
|
2463
|
+
console.log(`lunR gateway: cron scheduler started (${cron.intervalMs / 1e3}s interval)`);
|
|
2464
|
+
let shutdown = () => {
|
|
2465
|
+
cron.stop(), stopButtonSweeper(), Promise.all(connected.map((adapter) => adapter.disconnect().catch(() => {
|
|
2466
|
+
}))).finally(() => {
|
|
2467
|
+
process.exit(0);
|
|
2468
|
+
});
|
|
2469
|
+
};
|
|
2470
|
+
return process.on("SIGINT", shutdown), process.on("SIGTERM", shutdown), await new Promise(() => {
|
|
2471
|
+
}), 0;
|
|
2472
|
+
}
|
|
2473
|
+
async function runGateway(args) {
|
|
2474
|
+
let [sub, ...rest] = args;
|
|
2475
|
+
if (sub === "pair") {
|
|
2476
|
+
let [action, platform, code] = rest;
|
|
2477
|
+
return action === "approve" && platform ? runPairApprove(platform, code) : action === "list" ? runPairList() : (console.error("Usage: lunr gateway pair approve <platform> <code> | lunr gateway pair list"), 1);
|
|
2478
|
+
}
|
|
2479
|
+
return sub === "status" ? runStatus() : sub === void 0 ? runDaemon() : (console.error(`Unknown gateway sub-command "${sub}". Expected: pair approve | pair list | status`), 1);
|
|
2480
|
+
}
|
|
2481
|
+
export {
|
|
2482
|
+
ADAPTER_FACTORIES,
|
|
2483
|
+
runGateway
|
|
2484
|
+
};
|