@ashx-j/lunr 0.2.25 → 0.2.26
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 +16 -1
- package/dist/builtin-extensions/index.d.ts.map +1 -1
- package/dist/builtin-extensions/index.js +1 -0
- package/dist/builtin-extensions/index.js.map +1 -1
- package/dist/builtin-extensions/lunr-computer-use.d.ts +3 -0
- package/dist/builtin-extensions/lunr-computer-use.d.ts.map +1 -0
- package/dist/builtin-extensions/lunr-computer-use.js +179 -0
- package/dist/builtin-extensions/lunr-computer-use.js.map +1 -0
- package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +4 -4
- package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +8 -5
- package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts +1 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js +11 -2
- package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-runner.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js +3 -3
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +15 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.d.ts +1 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.js +5 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/chain-outputs.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.d.ts +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js +2 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/communication.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/communication.js +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/communication.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +1 -0
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js +5 -3
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
- package/dist/core/computer-use/adapter.d.ts +28 -0
- package/dist/core/computer-use/adapter.d.ts.map +1 -0
- package/dist/core/computer-use/adapter.js +133 -0
- package/dist/core/computer-use/adapter.js.map +1 -0
- package/dist/core/computer-use/image.d.ts +27 -0
- package/dist/core/computer-use/image.d.ts.map +1 -0
- package/dist/core/computer-use/image.js +72 -0
- package/dist/core/computer-use/image.js.map +1 -0
- package/dist/core/computer-use/lease.d.ts +18 -0
- package/dist/core/computer-use/lease.d.ts.map +1 -0
- package/dist/core/computer-use/lease.js +129 -0
- package/dist/core/computer-use/lease.js.map +1 -0
- package/dist/core/computer-use/macos-runtime.d.ts +14 -0
- package/dist/core/computer-use/macos-runtime.d.ts.map +1 -0
- package/dist/core/computer-use/macos-runtime.js +148 -0
- package/dist/core/computer-use/macos-runtime.js.map +1 -0
- package/dist/core/computer-use/policy.d.ts +19 -0
- package/dist/core/computer-use/policy.d.ts.map +1 -0
- package/dist/core/computer-use/policy.js +67 -0
- package/dist/core/computer-use/policy.js.map +1 -0
- package/dist/core/computer-use/release.generated.d.ts +31 -0
- package/dist/core/computer-use/release.generated.d.ts.map +1 -0
- package/dist/core/computer-use/release.generated.js +36 -0
- package/dist/core/computer-use/release.generated.js.map +1 -0
- package/dist/core/computer-use/runtime.d.ts +13 -0
- package/dist/core/computer-use/runtime.d.ts.map +1 -0
- package/dist/core/computer-use/runtime.js +158 -0
- package/dist/core/computer-use/runtime.js.map +1 -0
- package/dist/core/computer-use/schemas.d.ts +100 -0
- package/dist/core/computer-use/schemas.d.ts.map +1 -0
- package/dist/core/computer-use/schemas.js +79 -0
- package/dist/core/computer-use/schemas.js.map +1 -0
- package/dist/core/computer-use/windows-desktop.d.ts +3 -0
- package/dist/core/computer-use/windows-desktop.d.ts.map +1 -0
- package/dist/core/computer-use/windows-desktop.js +48 -0
- package/dist/core/computer-use/windows-desktop.js.map +1 -0
- package/dist/core/computer-use/workflow.d.ts +38 -0
- package/dist/core/computer-use/workflow.d.ts.map +1 -0
- package/dist/core/computer-use/workflow.js +560 -0
- package/dist/core/computer-use/workflow.js.map +1 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +4 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/permissions.d.ts +1 -1
- package/dist/core/permissions.d.ts.map +1 -1
- package/dist/core/permissions.js +14 -3
- package/dist/core/permissions.js.map +1 -1
- package/dist/core/plan-mode.d.ts.map +1 -1
- package/dist/core/plan-mode.js +9 -0
- package/dist/core/plan-mode.js.map +1 -1
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +19 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-event-transport.d.ts +3 -0
- package/dist/core/subagent-event-transport.d.ts.map +1 -0
- package/dist/core/subagent-event-transport.js +61 -0
- package/dist/core/subagent-event-transport.js.map +1 -0
- package/dist/gateway/adapters/discord.d.ts.map +1 -1
- package/dist/gateway/adapters/discord.js +2 -2
- package/dist/gateway/adapters/discord.js.map +1 -1
- package/dist/gateway/approval.d.ts.map +1 -1
- package/dist/gateway/approval.js +3 -3
- package/dist/gateway/approval.js.map +1 -1
- package/dist/gateway/authz.d.ts +4 -4
- package/dist/gateway/authz.d.ts.map +1 -1
- package/dist/gateway/authz.js +8 -12
- package/dist/gateway/authz.js.map +1 -1
- package/dist/gateway/commands.d.ts.map +1 -1
- package/dist/gateway/commands.js +2 -6
- package/dist/gateway/commands.js.map +1 -1
- package/dist/gateway/cron.d.ts.map +1 -1
- package/dist/gateway/cron.js +20 -6
- package/dist/gateway/cron.js.map +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +3 -3
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/mobile-commands.d.ts.map +1 -1
- package/dist/gateway/mobile-commands.js +8 -8
- package/dist/gateway/mobile-commands.js.map +1 -1
- package/dist/gateway/presenter.d.ts.map +1 -1
- package/dist/gateway/presenter.js +3 -3
- package/dist/gateway/presenter.js.map +1 -1
- package/dist/gateway/router.d.ts.map +1 -1
- package/dist/gateway/router.js +5 -7
- package/dist/gateway/router.js.map +1 -1
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +6 -4
- package/dist/gateway/setup.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +20 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +8 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +9 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/node-runtime/adapter-VFG7WNPX.js +17 -0
- package/dist/node-runtime/{agent-session-services-YXXFCP5J.js → agent-session-services-5YXS2PLF.js} +10 -8
- package/dist/node-runtime/{builtin-extensions-ZLAX7TUH.js → builtin-extensions-I2HIFEMY.js} +3 -2
- package/dist/node-runtime/{chunk-3XPLBUEM.js → chunk-2CFDTH5B.js} +7 -3
- package/dist/node-runtime/chunk-2IDTZAG5.js +1415 -0
- package/dist/node-runtime/{chunk-QTU6AKNJ.js → chunk-3MISGG4S.js} +11 -15
- package/dist/node-runtime/chunk-4SBQAI4T.js +2185 -0
- package/dist/node-runtime/{chunk-TORR5XBV.js → chunk-5B5QN3ZQ.js} +12 -12
- package/dist/node-runtime/chunk-5B6VKFEV.js +180 -0
- package/dist/node-runtime/{chunk-AD6VXAO7.js → chunk-5Q4A37RB.js} +15 -104
- package/dist/node-runtime/{chunk-4TLO4PO2.js → chunk-7IQGBKBJ.js} +7 -1401
- package/dist/node-runtime/{chunk-OCVIXVTZ.js → chunk-D5UCKPBQ.js} +12 -1
- package/dist/node-runtime/{chunk-7S3T6YEA.js → chunk-DVQSHCVP.js} +1 -1
- package/dist/node-runtime/{chunk-HFVZUPFJ.js → chunk-E75WFZ33.js} +2 -2
- package/dist/node-runtime/chunk-FY4A63RK.js +73 -0
- package/dist/node-runtime/{chunk-6XL67VVY.js → chunk-I74LN5JG.js} +15 -0
- package/dist/node-runtime/chunk-IKU6Y7DL.js +287 -0
- package/dist/node-runtime/{chunk-WF7PGWRZ.js → chunk-L24TPBS4.js} +5 -4
- package/dist/node-runtime/{chunk-VQFSUJJG.js → chunk-LJTIVM7I.js} +7 -7
- package/dist/node-runtime/{chunk-DSIKTUPW.js → chunk-LWJXTA6Z.js} +274 -2433
- package/dist/node-runtime/{chunk-E4D3IGP2.js → chunk-P7XBVXGT.js} +6 -7
- package/dist/node-runtime/{chunk-OVX6X377.js → chunk-PTFBPO3C.js} +1 -1
- package/dist/node-runtime/{chunk-RC7DMV45.js → chunk-RCR3XAQY.js} +34 -6
- package/dist/node-runtime/chunk-RGQAEOM5.js +81 -0
- package/dist/node-runtime/{chunk-YCUZEGJG.js → chunk-SN2KSMPQ.js} +6 -3
- package/dist/node-runtime/{chunk-NRKGMJYU.js → chunk-SPVDBUOS.js} +1 -1
- package/dist/node-runtime/{chunk-IJIFHRTB.js → chunk-T4DA4XE7.js} +19 -19
- package/dist/node-runtime/chunk-U5F4IC7R.js +53 -0
- package/dist/node-runtime/{chunk-KWIYUUZK.js → chunk-UVWCDQYF.js} +27 -18
- package/dist/node-runtime/{chunk-ZXSGTGJ6.js → chunk-WIF24I7G.js} +21 -5
- package/dist/node-runtime/{chunk-2HHLJWGD.js → chunk-X3PGBLQ7.js} +2 -2
- package/dist/node-runtime/chunk-XHIRAZ26.js +108 -0
- package/dist/node-runtime/{chunk-YDO2F2IA.js → chunk-YWOJIGFY.js} +1 -1
- package/dist/node-runtime/{command-6WDOYKS6.js → command-FSBU2EVZ.js} +3 -3
- package/dist/node-runtime/{commands-NZLSOHPE.js → commands-EMBQQVVT.js} +4 -2
- package/dist/node-runtime/{commands-QC6VUZJQ.js → commands-NAAL7KNT.js} +5 -4
- package/dist/node-runtime/{direct-tool-executor-77GW4F5P.js → direct-tool-executor-4D7UXYU4.js} +7 -4
- package/dist/node-runtime/{gateway-INAYBF4Q.js → gateway-DQM333YV.js} +18 -19
- package/dist/node-runtime/index.js +23 -19
- package/dist/node-runtime/{init-HS6NQXSK.js → init-P5AUV77I.js} +4 -2
- package/dist/node-runtime/{install-cli-KL4UDDAD.js → install-cli-NN4YAU44.js} +1 -1
- package/dist/node-runtime/{interactive-mode-Z5M7WYYZ.js → interactive-mode-YOHZVMKX.js} +11 -9
- package/dist/node-runtime/lunr-computer-use-Y23CCDIJ.js +158 -0
- package/dist/node-runtime/{lunr-cron-LWIZJM2N.js → lunr-cron-USHC4LXH.js} +6 -4
- package/dist/node-runtime/main.js +16 -14
- package/dist/node-runtime/{mcp-auth-flow-QIXHW6QN.js → mcp-auth-flow-QVLFVLEO.js} +2 -1
- package/dist/node-runtime/{mobile-commands-J7OJEKFO.js → mobile-commands-O5LCHL32.js} +4 -3
- package/dist/node-runtime/{package-manager-cli-VYZM5WHN.js → package-manager-cli-Q7BL6KZO.js} +7 -6
- package/dist/node-runtime/{pi-mcp-adapter-5UKBZP44.js → pi-mcp-adapter-3PHF4KZ4.js} +5 -5
- package/dist/node-runtime/{pi-subagents-DNY7EAIG.js → pi-subagents-7IBSSGEW.js} +24 -18
- package/dist/node-runtime/{print-mode-XSFM26EL.js → print-mode-XQ76CYMT.js} +2 -1
- package/dist/node-runtime/{proxy-modes-LGEUYD7R.js → proxy-modes-5FAPSZEZ.js} +7 -4
- package/dist/node-runtime/rpc-entry.js +16 -14
- package/dist/node-runtime/runtime-EFPPD2UW.js +21 -0
- package/dist/node-runtime/{runtime-bridges-VRDDR73P.js → runtime-bridges-7WVCLI76.js} +3 -2
- package/dist/node-runtime/{settings-manager-J4SPQPDJ.js → settings-manager-DJDM5AHM.js} +2 -1
- package/dist/node-runtime/{setup-CQ2NLVK5.js → setup-PFBWRNUI.js} +10 -9
- package/dist/node-runtime/{subagent-executor-KZZQGAKV.js → subagent-executor-LPZ4BHWE.js} +29 -11
- package/dist/node-runtime/{update-cli-GTFJC7XK.js → update-cli-HNMOTNGX.js} +2 -1
- package/dist/node-runtime/workflow-OHPBEFO4.js +637 -0
- package/dist/utils/image-resize-core.d.ts +2 -0
- package/dist/utils/image-resize-core.d.ts.map +1 -1
- package/dist/utils/image-resize-core.js +10 -0
- package/dist/utils/image-resize-core.js.map +1 -1
- package/docs/computer-use.md +362 -0
- package/docs/features.md +8 -6
- 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/install-lock/package-lock.json +3841 -0
- package/install-lock/package.json +18 -0
- package/native/computer-use/LICENSE.md +21 -0
- package/npm-shrinkwrap.json +3822 -0
- package/package.json +15 -9
- package/dist/node-runtime/{chunk-LQP3RB67.js → chunk-RAK2QA2B.js} +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../../src/gateway/presenter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAIhG,OAAO,EAAE,KAAK,aAAa,EAAwC,MAAM,aAAa,CAAC;AAKvF,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAoC7E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI,CAIrF;AAUD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;CAAE,CAM5G;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAGzF;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CA2BnF;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAO3C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,QAAQ,GAAG,QAAmB,EACpC,KAAK,SAAoB,GACvB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,gBAAgB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,EAC9B,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9G,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAElD;AAsED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU1D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAM5E;AAqCD,wBAAgB,aAAa,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,CAAC,EAAE,wBAAwB,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB7B;AAED,wBAAgB,YAAY,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,wBAAwB,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAa7B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CA4B/D","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../config.ts\";\nimport { noOpUIContext } from \"../core/extensions/runner.ts\";\nimport type { ExtensionUIContext, ExtensionUIDialogOptions } from \"../core/extensions/types.ts\";\nimport { runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, isGatewayOwner } from \"./authz.ts\";\nimport { createPicker } from \"./buttons.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { atomicJson } from \"./service.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter, SendOptions } from \"./types.ts\";\n\nconst epochs = new Map<string, number>();\nconst inputs = new Map<string, { userId: string; resolve: (value: string | undefined) => void }>();\nconst dialogs = new Map<string, Set<() => void>>();\nlet adapters = new Map<string, PlatformAdapter>();\nconst deliveryConfigs = new Map<string, GatewayConfig>();\nconst roleGrants = new Map<string, { userId: string; chatId: string; threadId?: string }>();\nlet timer: ReturnType<typeof setInterval> | undefined;\nconst activeDeliveries = new Map<string, Promise<void>>();\ninterface Outbound {\n\tid: string;\n\tkey: string;\n\tepoch: number;\n\ttext: string;\n\tsource?: MessageEvent[\"source\"];\n\treplyTo?: string;\n\tkind?: \"result\" | \"notice\";\n\tattempts?: number;\n\tnextAttempt?: number;\n\tfailed?: boolean;\n\tbatchId?: string;\n}\nconst MAX_ATTEMPTS = 5;\nconst path = () => join(getAgentDir(), \"gateway-outbox.json\");\nlet outbox: Outbound[] = [];\n\nfunction sourceWithCurrentRole(key: string, source: MessageEvent[\"source\"]): MessageEvent[\"source\"] {\n\tconst grant = roleGrants.get(key);\n\treturn {\n\t\t...source,\n\t\troleAuthorized:\n\t\t\tgrant?.userId === source.userId && grant.chatId === source.chatId && grant.threadId === source.threadId,\n\t};\n}\n\nexport function rememberGatewayRole(key: string, source: MessageEvent[\"source\"]): void {\n\tif (source.roleAuthorized === true)\n\t\troleGrants.set(key, { userId: source.userId, chatId: source.chatId, threadId: source.threadId });\n\telse roleGrants.delete(key);\n}\n\nfunction canDeliver(key: string): boolean {\n\tconst binding = conversationBinding(key);\n\tif (!binding) return false;\n\tconst cfg = loadGatewayConfig();\n\tif (binding.owner && (binding.owner !== binding.source.userId || !isGatewayOwner(binding.source, cfg))) return false;\n\treturn isAuthorized(sourceWithCurrentRole(key, binding.source), cfg, createPairingStore());\n}\n\nexport function gatewayDestination(key: string): { adapter: PlatformAdapter; source: MessageEvent[\"source\"] } {\n\tconst binding = conversationBinding(key);\n\tconst adapter = binding && adapters.get(binding.source.platform);\n\tif (!binding || !adapter || !canDeliver(key))\n\t\tthrow new Error(\"This chat is no longer authorized or its platform is disconnected.\");\n\treturn { adapter, source: binding.source };\n}\n\nexport function withGatewayPresentation<T>(key: string, run: () => Promise<T>): Promise<T> {\n\tconst { adapter, source } = gatewayDestination(key);\n\treturn runWithApprovalContext({ adapter, source, key }, run);\n}\n\nexport function startGatewayPresenter(connected: Map<string, PlatformAdapter>): void {\n\tadapters = connected;\n\tif (existsSync(path())) {\n\t\ttry {\n\t\t\tconst raw: unknown = JSON.parse(readFileSync(path(), \"utf8\"));\n\t\t\tif (\n\t\t\t\t!Array.isArray(raw) ||\n\t\t\t\t!raw.every(\n\t\t\t\t\t(item) =>\n\t\t\t\t\t\titem && typeof item.id === \"string\" && typeof item.key === \"string\" && typeof item.text === \"string\",\n\t\t\t\t)\n\t\t\t)\n\t\t\t\tthrow new Error(\"Invalid outbox\");\n\t\t\toutbox = raw as Outbound[];\n\t\t} catch {\n\t\t\tthrow new Error(\"Gateway outbox is invalid. Restore it before starting.\");\n\t\t}\n\t} else outbox = [];\n\tfor (const item of outbox) {\n\t\tif (item.kind === \"notice\") epochs.set(item.key, Math.max(epochs.get(item.key) ?? 0, item.epoch ?? 0));\n\t}\n\tif (timer) clearInterval(timer);\n\ttimer = setInterval(() => {\n\t\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n\t}, 3000);\n\ttimer.unref();\n\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n}\n\nexport function stopGatewayPresenter(): void {\n\tif (timer) clearInterval(timer);\n\ttimer = undefined;\n\tfor (const key of dialogs.keys()) invalidateGatewayDialogs(key);\n\tadapters = new Map();\n\tdeliveryConfigs.clear();\n\troleGrants.clear();\n}\n\nexport function gatewayEpoch(key: string): number {\n\treturn epochs.get(key) ?? 0;\n}\n\nexport async function sendGatewayNotice(\n\tkey: string,\n\ttext: string,\n\tkind: \"notice\" | \"result\" = \"notice\",\n\tepoch = gatewayEpoch(key),\n): Promise<void> {\n\tconst clean = text.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").trim();\n\tif (!clean || !canDeliver(key)) return;\n\tconst { adapter, source } = gatewayDestination(key);\n\tawait queueGatewayText(key, source, adapter, clean, { kind, epoch });\n}\n\nexport async function queueGatewayText(\n\tkey: string,\n\tsource: MessageEvent[\"source\"],\n\tadapter: PlatformAdapter,\n\ttext: string,\n\toptions: { kind: \"result\" | \"notice\"; replyTo?: string; cfg?: GatewayConfig; chunks?: string[]; epoch?: number },\n): Promise<void> {\n\tif (!text || (options.kind === \"notice\" && options.epoch !== undefined && options.epoch !== gatewayEpoch(key)))\n\t\treturn;\n\tadapters.set(source.platform, adapter);\n\tif (options.cfg) deliveryConfigs.set(key, options.cfg);\n\tconst chunks = options.chunks ?? splitMessage(text, adapter.maxMessageLength);\n\tconst batchId = randomUUID();\n\tfor (const [index, chunk] of chunks.entries())\n\t\toutbox.push({\n\t\t\tid: randomUUID(),\n\t\t\tbatchId,\n\t\t\tkey,\n\t\t\tsource: { ...source, roleAuthorized: undefined },\n\t\t\tepoch: options.epoch ?? gatewayEpoch(key),\n\t\t\ttext: chunk,\n\t\t\tkind: options.kind,\n\t\t\treplyTo: index === 0 ? options.replyTo : undefined,\n\t\t});\n\tatomicJson(path(), outbox);\n\tif (activeDeliveries.has(key)) await activeDeliveries.get(key);\n\tawait flushKey(key);\n}\n\nexport function flushGatewayOutbox(): Promise<void> {\n\treturn flushOutbox();\n}\n\nfunction flushOutbox(): Promise<void> {\n\treturn Promise.all([...new Set(outbox.map((item) => item.key))].map(flushKey)).then(() => {});\n}\n\nfunction flushKey(key: string): Promise<void> {\n\tconst current = activeDeliveries.get(key);\n\tif (current) return current;\n\tconst running = drainKey(key).finally(() => {\n\t\tactiveDeliveries.delete(key);\n\t});\n\tactiveDeliveries.set(key, running);\n\treturn running;\n}\n\nasync function drainKey(key: string): Promise<void> {\n\tconst failedBatches = new Set(outbox.filter((item) => item.failed && item.batchId).map((item) => item.batchId));\n\tfor (const item of [...outbox].filter((item) => item.key === key)) {\n\t\tif (item.failed || (item.batchId && failedBatches.has(item.batchId))) continue;\n\t\tif (item.kind === \"notice\" && item.epoch !== (epochs.get(item.key) ?? 0)) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst binding = conversationBinding(item.key);\n\t\tconst source = item.source ?? binding?.source;\n\t\tif (!source) break;\n\t\tconst cfg = existsSync(gatewayConfigPath())\n\t\t\t? loadGatewayConfig()\n\t\t\t: (deliveryConfigs.get(item.key) ?? loadGatewayConfig());\n\t\tif (\n\t\t\t(binding &&\n\t\t\t\t(binding.source.platform !== source.platform ||\n\t\t\t\t\tbinding.source.chatId !== source.chatId ||\n\t\t\t\t\tbinding.source.userId !== source.userId ||\n\t\t\t\t\tbinding.source.threadId !== source.threadId)) ||\n\t\t\t!isAuthorized(sourceWithCurrentRole(key, source), cfg, createPairingStore()) ||\n\t\t\t(binding?.owner && (binding.owner !== source.userId || !isGatewayOwner(source, cfg)))\n\t\t) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst adapter = adapters.get(source.platform);\n\t\tif (!adapter || (item.nextAttempt ?? 0) > Date.now()) break;\n\t\tconst opts: SendOptions = { threadId: source.threadId, replyTo: item.replyTo };\n\t\tconst result = await adapter\n\t\t\t.send(source.chatId, item.text, opts)\n\t\t\t.catch((error: unknown) => ({ success: false as const, error: String(error), retryable: true }));\n\t\tif (result.success) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t} else {\n\t\t\titem.attempts = (item.attempts ?? 0) + 1;\n\t\t\titem.failed = result.retryable === false || item.attempts >= MAX_ATTEMPTS;\n\t\t\titem.nextAttempt = Date.now() + Math.min(60_000, 3000 * 2 ** (item.attempts - 1));\n\t\t\tif (item.failed && item.batchId) {\n\t\t\t\tfailedBatches.add(item.batchId);\n\t\t\t\tfor (const pending of outbox) if (pending.batchId === item.batchId) pending.failed = true;\n\t\t\t}\n\t\t\tatomicJson(path(), outbox);\n\t\t\tconsole.error(\n\t\t\t\t`[gateway] delivery ${item.failed ? \"failed permanently\" : \"retry scheduled\"} for ${source.platform}: ${result.error ?? \"unknown error\"}`,\n\t\t\t);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nexport function invalidateGatewayDialogs(key: string): void {\n\tepochs.set(key, (epochs.get(key) ?? 0) + 1);\n\tconst remaining = outbox.filter((item) => item.key !== key || item.kind !== \"notice\");\n\tif (remaining.length !== outbox.length) {\n\t\toutbox = remaining;\n\t\tatomicJson(path(), outbox);\n\t}\n\tfor (const cancel of dialogs.get(key) ?? []) cancel();\n\tdialogs.delete(key);\n\tinputs.delete(key);\n}\n\nexport function acceptGatewayInput(key: string, event: MessageEvent): boolean {\n\tconst input = inputs.get(key);\n\tif (!input || input.userId !== event.source.userId || !canDeliver(key)) return false;\n\tif (event.text.startsWith(\"/\") && event.text !== \"/cancel\") return false;\n\tinput.resolve(event.text === \"/cancel\" ? undefined : event.text);\n\treturn true;\n}\n\nfunction dialog(\n\tkey: string,\n\topts: ExtensionUIDialogOptions | undefined,\n\tshow: (done: (value: string | undefined) => void, valid: () => boolean) => Promise<void>,\n): Promise<string | undefined> {\n\tconst epoch = epochs.get(key) ?? 0;\n\treturn new Promise((resolve, reject) => {\n\t\tlet settled = false;\n\t\tconst finish = (value: string | undefined, error?: unknown) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timeout);\n\t\t\topts?.signal?.removeEventListener(\"abort\", cancel);\n\t\t\tinputs.delete(key);\n\t\t\tdialogs.get(key)?.delete(cancel);\n\t\t\tif (error !== undefined) reject(error);\n\t\t\telse resolve(value);\n\t\t};\n\t\tconst done = (value: string | undefined) => finish(value);\n\t\tconst cancel = () => done(undefined);\n\t\tconst timeout = setTimeout(cancel, opts?.timeout ?? 300_000);\n\t\tconst set = dialogs.get(key) ?? new Set();\n\t\tset.add(cancel);\n\t\tdialogs.set(key, set);\n\t\topts?.signal?.addEventListener(\"abort\", cancel, { once: true });\n\t\tif (opts?.signal?.aborted) {\n\t\t\tcancel();\n\t\t\treturn;\n\t\t}\n\t\tvoid show(done, () => !settled && (epochs.get(key) ?? 0) === epoch && canDeliver(key)).catch((error) => {\n\t\t\tfinish(undefined, error);\n\t\t});\n\t});\n}\n\nexport function gatewaySelect(\n\tkey: string,\n\ttitle: string,\n\toptions: string[],\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\treturn dialog(key, opts, async (done, valid) => {\n\t\tconst { adapter, source } = gatewayDestination(key);\n\t\tconst result = await createPicker(\n\t\t\tadapter,\n\t\t\tsource,\n\t\t\t{\n\t\t\t\tkind: \"dialog\",\n\t\t\t\tsessionKey: key,\n\t\t\t\tinvokerId: source.userId,\n\t\t\t\ttitle,\n\t\t\t\titems: options.map((value) => ({ label: value, value })),\n\t\t\t\tvalidate: valid,\n\t\t\t\tonCancel: () => done(undefined),\n\t\t\t\tresolve: async (item) => {\n\t\t\t\t\tdone(item.value);\n\t\t\t\t\treturn { done: true, text: `${title}\\n${item.value}` };\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ threadId: source.threadId },\n\t\t);\n\t\tif (!result.success) throw new Error(result.error ?? \"Could not send picker.\");\n\t});\n}\n\nexport function gatewayInput(\n\tkey: string,\n\ttitle: string,\n\tplaceholder?: string,\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\tif (inputs.has(key)) return Promise.reject(new Error(\"Answer or cancel the existing question first.\"));\n\tconst epoch = gatewayEpoch(key);\n\treturn dialog(key, opts, async (done) => {\n\t\tconst { source } = gatewayDestination(key);\n\t\tinputs.set(key, { userId: source.userId, resolve: done });\n\t\tawait sendGatewayNotice(\n\t\t\tkey,\n\t\t\t`${title}${placeholder ? `\\n${placeholder}` : \"\"}\\nReply with text, or /cancel.`,\n\t\t\t\"notice\",\n\t\t\tepoch,\n\t\t);\n\t});\n}\n\nexport function createGatewayUI(key: string): ExtensionUIContext {\n\tconst epoch = gatewayEpoch(key);\n\treturn {\n\t\t...noOpUIContext,\n\t\tselect: (title, options, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewaySelect(key, title, options, opts) : Promise.resolve(undefined),\n\t\tconfirm: async (title, message, opts) =>\n\t\t\tgatewayEpoch(key) === epoch &&\n\t\t\t(await gatewaySelect(key, `${title}\\n${message}`, [\"Approve\", \"Decline\"], opts)) === \"Approve\",\n\t\tinput: (title, placeholder, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, placeholder, opts) : Promise.resolve(undefined),\n\t\teditor: (title, prefill) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, prefill) : Promise.resolve(undefined),\n\t\tnotify: (message) => {\n\t\t\tvoid sendGatewayNotice(key, message, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tcustom: async () => {\n\t\t\tthrow new Error(\n\t\t\t\t\"This command requires the terminal interface. Use the gateway's text commands or button pickers.\",\n\t\t\t);\n\t\t},\n\t\tpasteToEditor: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tsetEditorText: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../../src/gateway/presenter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAIhG,OAAO,EAAE,KAAK,aAAa,EAAwC,MAAM,aAAa,CAAC;AAKvF,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAe,MAAM,YAAY,CAAC;AAoC7E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI,CAIrF;AAUD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;CAAE,CAM5G;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAGzF;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CA2BnF;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAO3C;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,QAAQ,GAAG,QAAmB,EACpC,KAAK,SAAoB,GACvB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,gBAAgB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,EAC9B,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9G,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAElD;AAsED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU1D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAM5E;AAqCD,wBAAgB,aAAa,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,CAAC,EAAE,wBAAwB,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB7B;AAED,wBAAgB,YAAY,CAC3B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,wBAAwB,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAa7B;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CA4B/D","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../config.ts\";\nimport { noOpUIContext } from \"../core/extensions/runner.ts\";\nimport type { ExtensionUIContext, ExtensionUIDialogOptions } from \"../core/extensions/types.ts\";\nimport { runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized } from \"./authz.ts\";\nimport { createPicker } from \"./buttons.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { atomicJson } from \"./service.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter, SendOptions } from \"./types.ts\";\n\nconst epochs = new Map<string, number>();\nconst inputs = new Map<string, { userId: string; resolve: (value: string | undefined) => void }>();\nconst dialogs = new Map<string, Set<() => void>>();\nlet adapters = new Map<string, PlatformAdapter>();\nconst deliveryConfigs = new Map<string, GatewayConfig>();\nconst roleGrants = new Map<string, { userId: string; chatId: string; threadId?: string }>();\nlet timer: ReturnType<typeof setInterval> | undefined;\nconst activeDeliveries = new Map<string, Promise<void>>();\ninterface Outbound {\n\tid: string;\n\tkey: string;\n\tepoch: number;\n\ttext: string;\n\tsource?: MessageEvent[\"source\"];\n\treplyTo?: string;\n\tkind?: \"result\" | \"notice\";\n\tattempts?: number;\n\tnextAttempt?: number;\n\tfailed?: boolean;\n\tbatchId?: string;\n}\nconst MAX_ATTEMPTS = 5;\nconst path = () => join(getAgentDir(), \"gateway-outbox.json\");\nlet outbox: Outbound[] = [];\n\nfunction sourceWithCurrentRole(key: string, source: MessageEvent[\"source\"]): MessageEvent[\"source\"] {\n\tconst grant = roleGrants.get(key);\n\treturn {\n\t\t...source,\n\t\troleAuthorized:\n\t\t\tgrant?.userId === source.userId && grant.chatId === source.chatId && grant.threadId === source.threadId,\n\t};\n}\n\nexport function rememberGatewayRole(key: string, source: MessageEvent[\"source\"]): void {\n\tif (source.roleAuthorized === true)\n\t\troleGrants.set(key, { userId: source.userId, chatId: source.chatId, threadId: source.threadId });\n\telse roleGrants.delete(key);\n}\n\nfunction canDeliver(key: string): boolean {\n\tconst binding = conversationBinding(key);\n\tif (!binding) return false;\n\tconst cfg = loadGatewayConfig();\n\tif (binding.owner && binding.owner !== binding.source.userId) return false;\n\treturn isAuthorized(sourceWithCurrentRole(key, binding.source), cfg, createPairingStore());\n}\n\nexport function gatewayDestination(key: string): { adapter: PlatformAdapter; source: MessageEvent[\"source\"] } {\n\tconst binding = conversationBinding(key);\n\tconst adapter = binding && adapters.get(binding.source.platform);\n\tif (!binding || !adapter || !canDeliver(key))\n\t\tthrow new Error(\"This chat is no longer authorized or its platform is disconnected.\");\n\treturn { adapter, source: binding.source };\n}\n\nexport function withGatewayPresentation<T>(key: string, run: () => Promise<T>): Promise<T> {\n\tconst { adapter, source } = gatewayDestination(key);\n\treturn runWithApprovalContext({ adapter, source, key }, run);\n}\n\nexport function startGatewayPresenter(connected: Map<string, PlatformAdapter>): void {\n\tadapters = connected;\n\tif (existsSync(path())) {\n\t\ttry {\n\t\t\tconst raw: unknown = JSON.parse(readFileSync(path(), \"utf8\"));\n\t\t\tif (\n\t\t\t\t!Array.isArray(raw) ||\n\t\t\t\t!raw.every(\n\t\t\t\t\t(item) =>\n\t\t\t\t\t\titem && typeof item.id === \"string\" && typeof item.key === \"string\" && typeof item.text === \"string\",\n\t\t\t\t)\n\t\t\t)\n\t\t\t\tthrow new Error(\"Invalid outbox\");\n\t\t\toutbox = raw as Outbound[];\n\t\t} catch {\n\t\t\tthrow new Error(\"Gateway outbox is invalid. Restore it before starting.\");\n\t\t}\n\t} else outbox = [];\n\tfor (const item of outbox) {\n\t\tif (item.kind === \"notice\") epochs.set(item.key, Math.max(epochs.get(item.key) ?? 0, item.epoch ?? 0));\n\t}\n\tif (timer) clearInterval(timer);\n\ttimer = setInterval(() => {\n\t\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n\t}, 3000);\n\ttimer.unref();\n\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n}\n\nexport function stopGatewayPresenter(): void {\n\tif (timer) clearInterval(timer);\n\ttimer = undefined;\n\tfor (const key of dialogs.keys()) invalidateGatewayDialogs(key);\n\tadapters = new Map();\n\tdeliveryConfigs.clear();\n\troleGrants.clear();\n}\n\nexport function gatewayEpoch(key: string): number {\n\treturn epochs.get(key) ?? 0;\n}\n\nexport async function sendGatewayNotice(\n\tkey: string,\n\ttext: string,\n\tkind: \"notice\" | \"result\" = \"notice\",\n\tepoch = gatewayEpoch(key),\n): Promise<void> {\n\tconst clean = text.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").trim();\n\tif (!clean || !canDeliver(key)) return;\n\tconst { adapter, source } = gatewayDestination(key);\n\tawait queueGatewayText(key, source, adapter, clean, { kind, epoch });\n}\n\nexport async function queueGatewayText(\n\tkey: string,\n\tsource: MessageEvent[\"source\"],\n\tadapter: PlatformAdapter,\n\ttext: string,\n\toptions: { kind: \"result\" | \"notice\"; replyTo?: string; cfg?: GatewayConfig; chunks?: string[]; epoch?: number },\n): Promise<void> {\n\tif (!text || (options.kind === \"notice\" && options.epoch !== undefined && options.epoch !== gatewayEpoch(key)))\n\t\treturn;\n\tadapters.set(source.platform, adapter);\n\tif (options.cfg) deliveryConfigs.set(key, options.cfg);\n\tconst chunks = options.chunks ?? splitMessage(text, adapter.maxMessageLength);\n\tconst batchId = randomUUID();\n\tfor (const [index, chunk] of chunks.entries())\n\t\toutbox.push({\n\t\t\tid: randomUUID(),\n\t\t\tbatchId,\n\t\t\tkey,\n\t\t\tsource: { ...source, roleAuthorized: undefined },\n\t\t\tepoch: options.epoch ?? gatewayEpoch(key),\n\t\t\ttext: chunk,\n\t\t\tkind: options.kind,\n\t\t\treplyTo: index === 0 ? options.replyTo : undefined,\n\t\t});\n\tatomicJson(path(), outbox);\n\tif (activeDeliveries.has(key)) await activeDeliveries.get(key);\n\tawait flushKey(key);\n}\n\nexport function flushGatewayOutbox(): Promise<void> {\n\treturn flushOutbox();\n}\n\nfunction flushOutbox(): Promise<void> {\n\treturn Promise.all([...new Set(outbox.map((item) => item.key))].map(flushKey)).then(() => {});\n}\n\nfunction flushKey(key: string): Promise<void> {\n\tconst current = activeDeliveries.get(key);\n\tif (current) return current;\n\tconst running = drainKey(key).finally(() => {\n\t\tactiveDeliveries.delete(key);\n\t});\n\tactiveDeliveries.set(key, running);\n\treturn running;\n}\n\nasync function drainKey(key: string): Promise<void> {\n\tconst failedBatches = new Set(outbox.filter((item) => item.failed && item.batchId).map((item) => item.batchId));\n\tfor (const item of [...outbox].filter((item) => item.key === key)) {\n\t\tif (item.failed || (item.batchId && failedBatches.has(item.batchId))) continue;\n\t\tif (item.kind === \"notice\" && item.epoch !== (epochs.get(item.key) ?? 0)) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst binding = conversationBinding(item.key);\n\t\tconst source = item.source ?? binding?.source;\n\t\tif (!source) break;\n\t\tconst cfg = existsSync(gatewayConfigPath())\n\t\t\t? loadGatewayConfig()\n\t\t\t: (deliveryConfigs.get(item.key) ?? loadGatewayConfig());\n\t\tif (\n\t\t\t(binding &&\n\t\t\t\t(binding.source.platform !== source.platform ||\n\t\t\t\t\tbinding.source.chatId !== source.chatId ||\n\t\t\t\t\tbinding.source.userId !== source.userId ||\n\t\t\t\t\tbinding.source.threadId !== source.threadId)) ||\n\t\t\t!isAuthorized(sourceWithCurrentRole(key, source), cfg, createPairingStore()) ||\n\t\t\t(binding?.owner && binding.owner !== source.userId)\n\t\t) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst adapter = adapters.get(source.platform);\n\t\tif (!adapter || (item.nextAttempt ?? 0) > Date.now()) break;\n\t\tconst opts: SendOptions = { threadId: source.threadId, replyTo: item.replyTo };\n\t\tconst result = await adapter\n\t\t\t.send(source.chatId, item.text, opts)\n\t\t\t.catch((error: unknown) => ({ success: false as const, error: String(error), retryable: true }));\n\t\tif (result.success) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t} else {\n\t\t\titem.attempts = (item.attempts ?? 0) + 1;\n\t\t\titem.failed = result.retryable === false || item.attempts >= MAX_ATTEMPTS;\n\t\t\titem.nextAttempt = Date.now() + Math.min(60_000, 3000 * 2 ** (item.attempts - 1));\n\t\t\tif (item.failed && item.batchId) {\n\t\t\t\tfailedBatches.add(item.batchId);\n\t\t\t\tfor (const pending of outbox) if (pending.batchId === item.batchId) pending.failed = true;\n\t\t\t}\n\t\t\tatomicJson(path(), outbox);\n\t\t\tconsole.error(\n\t\t\t\t`[gateway] delivery ${item.failed ? \"failed permanently\" : \"retry scheduled\"} for ${source.platform}: ${result.error ?? \"unknown error\"}`,\n\t\t\t);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nexport function invalidateGatewayDialogs(key: string): void {\n\tepochs.set(key, (epochs.get(key) ?? 0) + 1);\n\tconst remaining = outbox.filter((item) => item.key !== key || item.kind !== \"notice\");\n\tif (remaining.length !== outbox.length) {\n\t\toutbox = remaining;\n\t\tatomicJson(path(), outbox);\n\t}\n\tfor (const cancel of dialogs.get(key) ?? []) cancel();\n\tdialogs.delete(key);\n\tinputs.delete(key);\n}\n\nexport function acceptGatewayInput(key: string, event: MessageEvent): boolean {\n\tconst input = inputs.get(key);\n\tif (!input || input.userId !== event.source.userId || !canDeliver(key)) return false;\n\tif (event.text.startsWith(\"/\") && event.text !== \"/cancel\") return false;\n\tinput.resolve(event.text === \"/cancel\" ? undefined : event.text);\n\treturn true;\n}\n\nfunction dialog(\n\tkey: string,\n\topts: ExtensionUIDialogOptions | undefined,\n\tshow: (done: (value: string | undefined) => void, valid: () => boolean) => Promise<void>,\n): Promise<string | undefined> {\n\tconst epoch = epochs.get(key) ?? 0;\n\treturn new Promise((resolve, reject) => {\n\t\tlet settled = false;\n\t\tconst finish = (value: string | undefined, error?: unknown) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timeout);\n\t\t\topts?.signal?.removeEventListener(\"abort\", cancel);\n\t\t\tinputs.delete(key);\n\t\t\tdialogs.get(key)?.delete(cancel);\n\t\t\tif (error !== undefined) reject(error);\n\t\t\telse resolve(value);\n\t\t};\n\t\tconst done = (value: string | undefined) => finish(value);\n\t\tconst cancel = () => done(undefined);\n\t\tconst timeout = setTimeout(cancel, opts?.timeout ?? 300_000);\n\t\tconst set = dialogs.get(key) ?? new Set();\n\t\tset.add(cancel);\n\t\tdialogs.set(key, set);\n\t\topts?.signal?.addEventListener(\"abort\", cancel, { once: true });\n\t\tif (opts?.signal?.aborted) {\n\t\t\tcancel();\n\t\t\treturn;\n\t\t}\n\t\tvoid show(done, () => !settled && (epochs.get(key) ?? 0) === epoch && canDeliver(key)).catch((error) => {\n\t\t\tfinish(undefined, error);\n\t\t});\n\t});\n}\n\nexport function gatewaySelect(\n\tkey: string,\n\ttitle: string,\n\toptions: string[],\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\treturn dialog(key, opts, async (done, valid) => {\n\t\tconst { adapter, source } = gatewayDestination(key);\n\t\tconst result = await createPicker(\n\t\t\tadapter,\n\t\t\tsource,\n\t\t\t{\n\t\t\t\tkind: \"dialog\",\n\t\t\t\tsessionKey: key,\n\t\t\t\tinvokerId: source.userId,\n\t\t\t\ttitle,\n\t\t\t\titems: options.map((value) => ({ label: value, value })),\n\t\t\t\tvalidate: valid,\n\t\t\t\tonCancel: () => done(undefined),\n\t\t\t\tresolve: async (item) => {\n\t\t\t\t\tdone(item.value);\n\t\t\t\t\treturn { done: true, text: `${title}\\n${item.value}` };\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ threadId: source.threadId },\n\t\t);\n\t\tif (!result.success) throw new Error(result.error ?? \"Could not send picker.\");\n\t});\n}\n\nexport function gatewayInput(\n\tkey: string,\n\ttitle: string,\n\tplaceholder?: string,\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\tif (inputs.has(key)) return Promise.reject(new Error(\"Answer or cancel the existing question first.\"));\n\tconst epoch = gatewayEpoch(key);\n\treturn dialog(key, opts, async (done) => {\n\t\tconst { source } = gatewayDestination(key);\n\t\tinputs.set(key, { userId: source.userId, resolve: done });\n\t\tawait sendGatewayNotice(\n\t\t\tkey,\n\t\t\t`${title}${placeholder ? `\\n${placeholder}` : \"\"}\\nReply with text, or /cancel.`,\n\t\t\t\"notice\",\n\t\t\tepoch,\n\t\t);\n\t});\n}\n\nexport function createGatewayUI(key: string): ExtensionUIContext {\n\tconst epoch = gatewayEpoch(key);\n\treturn {\n\t\t...noOpUIContext,\n\t\tselect: (title, options, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewaySelect(key, title, options, opts) : Promise.resolve(undefined),\n\t\tconfirm: async (title, message, opts) =>\n\t\t\tgatewayEpoch(key) === epoch &&\n\t\t\t(await gatewaySelect(key, `${title}\\n${message}`, [\"Approve\", \"Decline\"], opts)) === \"Approve\",\n\t\tinput: (title, placeholder, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, placeholder, opts) : Promise.resolve(undefined),\n\t\teditor: (title, prefill) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, prefill) : Promise.resolve(undefined),\n\t\tnotify: (message) => {\n\t\t\tvoid sendGatewayNotice(key, message, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tcustom: async () => {\n\t\t\tthrow new Error(\n\t\t\t\t\"This command requires the terminal interface. Use the gateway's text commands or button pickers.\",\n\t\t\t);\n\t\t},\n\t\tpasteToEditor: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tsetEditorText: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t};\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { getAgentDir } from "../config.js";
|
|
5
5
|
import { noOpUIContext } from "../core/extensions/runner.js";
|
|
6
6
|
import { runWithApprovalContext } from "./approval.js";
|
|
7
|
-
import { isAuthorized
|
|
7
|
+
import { isAuthorized } from "./authz.js";
|
|
8
8
|
import { createPicker } from "./buttons.js";
|
|
9
9
|
import { gatewayConfigPath, loadGatewayConfig } from "./config.js";
|
|
10
10
|
import { conversationBinding } from "./conversations.js";
|
|
@@ -40,7 +40,7 @@ function canDeliver(key) {
|
|
|
40
40
|
if (!binding)
|
|
41
41
|
return false;
|
|
42
42
|
const cfg = loadGatewayConfig();
|
|
43
|
-
if (binding.owner &&
|
|
43
|
+
if (binding.owner && binding.owner !== binding.source.userId)
|
|
44
44
|
return false;
|
|
45
45
|
return isAuthorized(sourceWithCurrentRole(key, binding.source), cfg, createPairingStore());
|
|
46
46
|
}
|
|
@@ -166,7 +166,7 @@ async function drainKey(key) {
|
|
|
166
166
|
binding.source.userId !== source.userId ||
|
|
167
167
|
binding.source.threadId !== source.threadId)) ||
|
|
168
168
|
!isAuthorized(sourceWithCurrentRole(key, source), cfg, createPairingStore()) ||
|
|
169
|
-
(binding?.owner &&
|
|
169
|
+
(binding?.owner && binding.owner !== source.userId)) {
|
|
170
170
|
outbox = outbox.filter((v) => v.id !== item.id);
|
|
171
171
|
atomicJson(path(), outbox);
|
|
172
172
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presenter.js","sourceRoot":"","sources":["../../src/gateway/presenter.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../config.ts\";\nimport { noOpUIContext } from \"../core/extensions/runner.ts\";\nimport type { ExtensionUIContext, ExtensionUIDialogOptions } from \"../core/extensions/types.ts\";\nimport { runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, isGatewayOwner } from \"./authz.ts\";\nimport { createPicker } from \"./buttons.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { atomicJson } from \"./service.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter, SendOptions } from \"./types.ts\";\n\nconst epochs = new Map<string, number>();\nconst inputs = new Map<string, { userId: string; resolve: (value: string | undefined) => void }>();\nconst dialogs = new Map<string, Set<() => void>>();\nlet adapters = new Map<string, PlatformAdapter>();\nconst deliveryConfigs = new Map<string, GatewayConfig>();\nconst roleGrants = new Map<string, { userId: string; chatId: string; threadId?: string }>();\nlet timer: ReturnType<typeof setInterval> | undefined;\nconst activeDeliveries = new Map<string, Promise<void>>();\ninterface Outbound {\n\tid: string;\n\tkey: string;\n\tepoch: number;\n\ttext: string;\n\tsource?: MessageEvent[\"source\"];\n\treplyTo?: string;\n\tkind?: \"result\" | \"notice\";\n\tattempts?: number;\n\tnextAttempt?: number;\n\tfailed?: boolean;\n\tbatchId?: string;\n}\nconst MAX_ATTEMPTS = 5;\nconst path = () => join(getAgentDir(), \"gateway-outbox.json\");\nlet outbox: Outbound[] = [];\n\nfunction sourceWithCurrentRole(key: string, source: MessageEvent[\"source\"]): MessageEvent[\"source\"] {\n\tconst grant = roleGrants.get(key);\n\treturn {\n\t\t...source,\n\t\troleAuthorized:\n\t\t\tgrant?.userId === source.userId && grant.chatId === source.chatId && grant.threadId === source.threadId,\n\t};\n}\n\nexport function rememberGatewayRole(key: string, source: MessageEvent[\"source\"]): void {\n\tif (source.roleAuthorized === true)\n\t\troleGrants.set(key, { userId: source.userId, chatId: source.chatId, threadId: source.threadId });\n\telse roleGrants.delete(key);\n}\n\nfunction canDeliver(key: string): boolean {\n\tconst binding = conversationBinding(key);\n\tif (!binding) return false;\n\tconst cfg = loadGatewayConfig();\n\tif (binding.owner && (binding.owner !== binding.source.userId || !isGatewayOwner(binding.source, cfg))) return false;\n\treturn isAuthorized(sourceWithCurrentRole(key, binding.source), cfg, createPairingStore());\n}\n\nexport function gatewayDestination(key: string): { adapter: PlatformAdapter; source: MessageEvent[\"source\"] } {\n\tconst binding = conversationBinding(key);\n\tconst adapter = binding && adapters.get(binding.source.platform);\n\tif (!binding || !adapter || !canDeliver(key))\n\t\tthrow new Error(\"This chat is no longer authorized or its platform is disconnected.\");\n\treturn { adapter, source: binding.source };\n}\n\nexport function withGatewayPresentation<T>(key: string, run: () => Promise<T>): Promise<T> {\n\tconst { adapter, source } = gatewayDestination(key);\n\treturn runWithApprovalContext({ adapter, source, key }, run);\n}\n\nexport function startGatewayPresenter(connected: Map<string, PlatformAdapter>): void {\n\tadapters = connected;\n\tif (existsSync(path())) {\n\t\ttry {\n\t\t\tconst raw: unknown = JSON.parse(readFileSync(path(), \"utf8\"));\n\t\t\tif (\n\t\t\t\t!Array.isArray(raw) ||\n\t\t\t\t!raw.every(\n\t\t\t\t\t(item) =>\n\t\t\t\t\t\titem && typeof item.id === \"string\" && typeof item.key === \"string\" && typeof item.text === \"string\",\n\t\t\t\t)\n\t\t\t)\n\t\t\t\tthrow new Error(\"Invalid outbox\");\n\t\t\toutbox = raw as Outbound[];\n\t\t} catch {\n\t\t\tthrow new Error(\"Gateway outbox is invalid. Restore it before starting.\");\n\t\t}\n\t} else outbox = [];\n\tfor (const item of outbox) {\n\t\tif (item.kind === \"notice\") epochs.set(item.key, Math.max(epochs.get(item.key) ?? 0, item.epoch ?? 0));\n\t}\n\tif (timer) clearInterval(timer);\n\ttimer = setInterval(() => {\n\t\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n\t}, 3000);\n\ttimer.unref();\n\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n}\n\nexport function stopGatewayPresenter(): void {\n\tif (timer) clearInterval(timer);\n\ttimer = undefined;\n\tfor (const key of dialogs.keys()) invalidateGatewayDialogs(key);\n\tadapters = new Map();\n\tdeliveryConfigs.clear();\n\troleGrants.clear();\n}\n\nexport function gatewayEpoch(key: string): number {\n\treturn epochs.get(key) ?? 0;\n}\n\nexport async function sendGatewayNotice(\n\tkey: string,\n\ttext: string,\n\tkind: \"notice\" | \"result\" = \"notice\",\n\tepoch = gatewayEpoch(key),\n): Promise<void> {\n\tconst clean = text.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").trim();\n\tif (!clean || !canDeliver(key)) return;\n\tconst { adapter, source } = gatewayDestination(key);\n\tawait queueGatewayText(key, source, adapter, clean, { kind, epoch });\n}\n\nexport async function queueGatewayText(\n\tkey: string,\n\tsource: MessageEvent[\"source\"],\n\tadapter: PlatformAdapter,\n\ttext: string,\n\toptions: { kind: \"result\" | \"notice\"; replyTo?: string; cfg?: GatewayConfig; chunks?: string[]; epoch?: number },\n): Promise<void> {\n\tif (!text || (options.kind === \"notice\" && options.epoch !== undefined && options.epoch !== gatewayEpoch(key)))\n\t\treturn;\n\tadapters.set(source.platform, adapter);\n\tif (options.cfg) deliveryConfigs.set(key, options.cfg);\n\tconst chunks = options.chunks ?? splitMessage(text, adapter.maxMessageLength);\n\tconst batchId = randomUUID();\n\tfor (const [index, chunk] of chunks.entries())\n\t\toutbox.push({\n\t\t\tid: randomUUID(),\n\t\t\tbatchId,\n\t\t\tkey,\n\t\t\tsource: { ...source, roleAuthorized: undefined },\n\t\t\tepoch: options.epoch ?? gatewayEpoch(key),\n\t\t\ttext: chunk,\n\t\t\tkind: options.kind,\n\t\t\treplyTo: index === 0 ? options.replyTo : undefined,\n\t\t});\n\tatomicJson(path(), outbox);\n\tif (activeDeliveries.has(key)) await activeDeliveries.get(key);\n\tawait flushKey(key);\n}\n\nexport function flushGatewayOutbox(): Promise<void> {\n\treturn flushOutbox();\n}\n\nfunction flushOutbox(): Promise<void> {\n\treturn Promise.all([...new Set(outbox.map((item) => item.key))].map(flushKey)).then(() => {});\n}\n\nfunction flushKey(key: string): Promise<void> {\n\tconst current = activeDeliveries.get(key);\n\tif (current) return current;\n\tconst running = drainKey(key).finally(() => {\n\t\tactiveDeliveries.delete(key);\n\t});\n\tactiveDeliveries.set(key, running);\n\treturn running;\n}\n\nasync function drainKey(key: string): Promise<void> {\n\tconst failedBatches = new Set(outbox.filter((item) => item.failed && item.batchId).map((item) => item.batchId));\n\tfor (const item of [...outbox].filter((item) => item.key === key)) {\n\t\tif (item.failed || (item.batchId && failedBatches.has(item.batchId))) continue;\n\t\tif (item.kind === \"notice\" && item.epoch !== (epochs.get(item.key) ?? 0)) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst binding = conversationBinding(item.key);\n\t\tconst source = item.source ?? binding?.source;\n\t\tif (!source) break;\n\t\tconst cfg = existsSync(gatewayConfigPath())\n\t\t\t? loadGatewayConfig()\n\t\t\t: (deliveryConfigs.get(item.key) ?? loadGatewayConfig());\n\t\tif (\n\t\t\t(binding &&\n\t\t\t\t(binding.source.platform !== source.platform ||\n\t\t\t\t\tbinding.source.chatId !== source.chatId ||\n\t\t\t\t\tbinding.source.userId !== source.userId ||\n\t\t\t\t\tbinding.source.threadId !== source.threadId)) ||\n\t\t\t!isAuthorized(sourceWithCurrentRole(key, source), cfg, createPairingStore()) ||\n\t\t\t(binding?.owner && (binding.owner !== source.userId || !isGatewayOwner(source, cfg)))\n\t\t) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst adapter = adapters.get(source.platform);\n\t\tif (!adapter || (item.nextAttempt ?? 0) > Date.now()) break;\n\t\tconst opts: SendOptions = { threadId: source.threadId, replyTo: item.replyTo };\n\t\tconst result = await adapter\n\t\t\t.send(source.chatId, item.text, opts)\n\t\t\t.catch((error: unknown) => ({ success: false as const, error: String(error), retryable: true }));\n\t\tif (result.success) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t} else {\n\t\t\titem.attempts = (item.attempts ?? 0) + 1;\n\t\t\titem.failed = result.retryable === false || item.attempts >= MAX_ATTEMPTS;\n\t\t\titem.nextAttempt = Date.now() + Math.min(60_000, 3000 * 2 ** (item.attempts - 1));\n\t\t\tif (item.failed && item.batchId) {\n\t\t\t\tfailedBatches.add(item.batchId);\n\t\t\t\tfor (const pending of outbox) if (pending.batchId === item.batchId) pending.failed = true;\n\t\t\t}\n\t\t\tatomicJson(path(), outbox);\n\t\t\tconsole.error(\n\t\t\t\t`[gateway] delivery ${item.failed ? \"failed permanently\" : \"retry scheduled\"} for ${source.platform}: ${result.error ?? \"unknown error\"}`,\n\t\t\t);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nexport function invalidateGatewayDialogs(key: string): void {\n\tepochs.set(key, (epochs.get(key) ?? 0) + 1);\n\tconst remaining = outbox.filter((item) => item.key !== key || item.kind !== \"notice\");\n\tif (remaining.length !== outbox.length) {\n\t\toutbox = remaining;\n\t\tatomicJson(path(), outbox);\n\t}\n\tfor (const cancel of dialogs.get(key) ?? []) cancel();\n\tdialogs.delete(key);\n\tinputs.delete(key);\n}\n\nexport function acceptGatewayInput(key: string, event: MessageEvent): boolean {\n\tconst input = inputs.get(key);\n\tif (!input || input.userId !== event.source.userId || !canDeliver(key)) return false;\n\tif (event.text.startsWith(\"/\") && event.text !== \"/cancel\") return false;\n\tinput.resolve(event.text === \"/cancel\" ? undefined : event.text);\n\treturn true;\n}\n\nfunction dialog(\n\tkey: string,\n\topts: ExtensionUIDialogOptions | undefined,\n\tshow: (done: (value: string | undefined) => void, valid: () => boolean) => Promise<void>,\n): Promise<string | undefined> {\n\tconst epoch = epochs.get(key) ?? 0;\n\treturn new Promise((resolve, reject) => {\n\t\tlet settled = false;\n\t\tconst finish = (value: string | undefined, error?: unknown) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timeout);\n\t\t\topts?.signal?.removeEventListener(\"abort\", cancel);\n\t\t\tinputs.delete(key);\n\t\t\tdialogs.get(key)?.delete(cancel);\n\t\t\tif (error !== undefined) reject(error);\n\t\t\telse resolve(value);\n\t\t};\n\t\tconst done = (value: string | undefined) => finish(value);\n\t\tconst cancel = () => done(undefined);\n\t\tconst timeout = setTimeout(cancel, opts?.timeout ?? 300_000);\n\t\tconst set = dialogs.get(key) ?? new Set();\n\t\tset.add(cancel);\n\t\tdialogs.set(key, set);\n\t\topts?.signal?.addEventListener(\"abort\", cancel, { once: true });\n\t\tif (opts?.signal?.aborted) {\n\t\t\tcancel();\n\t\t\treturn;\n\t\t}\n\t\tvoid show(done, () => !settled && (epochs.get(key) ?? 0) === epoch && canDeliver(key)).catch((error) => {\n\t\t\tfinish(undefined, error);\n\t\t});\n\t});\n}\n\nexport function gatewaySelect(\n\tkey: string,\n\ttitle: string,\n\toptions: string[],\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\treturn dialog(key, opts, async (done, valid) => {\n\t\tconst { adapter, source } = gatewayDestination(key);\n\t\tconst result = await createPicker(\n\t\t\tadapter,\n\t\t\tsource,\n\t\t\t{\n\t\t\t\tkind: \"dialog\",\n\t\t\t\tsessionKey: key,\n\t\t\t\tinvokerId: source.userId,\n\t\t\t\ttitle,\n\t\t\t\titems: options.map((value) => ({ label: value, value })),\n\t\t\t\tvalidate: valid,\n\t\t\t\tonCancel: () => done(undefined),\n\t\t\t\tresolve: async (item) => {\n\t\t\t\t\tdone(item.value);\n\t\t\t\t\treturn { done: true, text: `${title}\\n${item.value}` };\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ threadId: source.threadId },\n\t\t);\n\t\tif (!result.success) throw new Error(result.error ?? \"Could not send picker.\");\n\t});\n}\n\nexport function gatewayInput(\n\tkey: string,\n\ttitle: string,\n\tplaceholder?: string,\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\tif (inputs.has(key)) return Promise.reject(new Error(\"Answer or cancel the existing question first.\"));\n\tconst epoch = gatewayEpoch(key);\n\treturn dialog(key, opts, async (done) => {\n\t\tconst { source } = gatewayDestination(key);\n\t\tinputs.set(key, { userId: source.userId, resolve: done });\n\t\tawait sendGatewayNotice(\n\t\t\tkey,\n\t\t\t`${title}${placeholder ? `\\n${placeholder}` : \"\"}\\nReply with text, or /cancel.`,\n\t\t\t\"notice\",\n\t\t\tepoch,\n\t\t);\n\t});\n}\n\nexport function createGatewayUI(key: string): ExtensionUIContext {\n\tconst epoch = gatewayEpoch(key);\n\treturn {\n\t\t...noOpUIContext,\n\t\tselect: (title, options, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewaySelect(key, title, options, opts) : Promise.resolve(undefined),\n\t\tconfirm: async (title, message, opts) =>\n\t\t\tgatewayEpoch(key) === epoch &&\n\t\t\t(await gatewaySelect(key, `${title}\\n${message}`, [\"Approve\", \"Decline\"], opts)) === \"Approve\",\n\t\tinput: (title, placeholder, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, placeholder, opts) : Promise.resolve(undefined),\n\t\teditor: (title, prefill) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, prefill) : Promise.resolve(undefined),\n\t\tnotify: (message) => {\n\t\t\tvoid sendGatewayNotice(key, message, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tcustom: async () => {\n\t\t\tthrow new Error(\n\t\t\t\t\"This command requires the terminal interface. Use the gateway's text commands or button pickers.\",\n\t\t\t);\n\t\t},\n\t\tpasteToEditor: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tsetEditorText: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"presenter.js","sourceRoot":"","sources":["../../src/gateway/presenter.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../config.ts\";\nimport { noOpUIContext } from \"../core/extensions/runner.ts\";\nimport type { ExtensionUIContext, ExtensionUIDialogOptions } from \"../core/extensions/types.ts\";\nimport { runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized } from \"./authz.ts\";\nimport { createPicker } from \"./buttons.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { atomicJson } from \"./service.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter, SendOptions } from \"./types.ts\";\n\nconst epochs = new Map<string, number>();\nconst inputs = new Map<string, { userId: string; resolve: (value: string | undefined) => void }>();\nconst dialogs = new Map<string, Set<() => void>>();\nlet adapters = new Map<string, PlatformAdapter>();\nconst deliveryConfigs = new Map<string, GatewayConfig>();\nconst roleGrants = new Map<string, { userId: string; chatId: string; threadId?: string }>();\nlet timer: ReturnType<typeof setInterval> | undefined;\nconst activeDeliveries = new Map<string, Promise<void>>();\ninterface Outbound {\n\tid: string;\n\tkey: string;\n\tepoch: number;\n\ttext: string;\n\tsource?: MessageEvent[\"source\"];\n\treplyTo?: string;\n\tkind?: \"result\" | \"notice\";\n\tattempts?: number;\n\tnextAttempt?: number;\n\tfailed?: boolean;\n\tbatchId?: string;\n}\nconst MAX_ATTEMPTS = 5;\nconst path = () => join(getAgentDir(), \"gateway-outbox.json\");\nlet outbox: Outbound[] = [];\n\nfunction sourceWithCurrentRole(key: string, source: MessageEvent[\"source\"]): MessageEvent[\"source\"] {\n\tconst grant = roleGrants.get(key);\n\treturn {\n\t\t...source,\n\t\troleAuthorized:\n\t\t\tgrant?.userId === source.userId && grant.chatId === source.chatId && grant.threadId === source.threadId,\n\t};\n}\n\nexport function rememberGatewayRole(key: string, source: MessageEvent[\"source\"]): void {\n\tif (source.roleAuthorized === true)\n\t\troleGrants.set(key, { userId: source.userId, chatId: source.chatId, threadId: source.threadId });\n\telse roleGrants.delete(key);\n}\n\nfunction canDeliver(key: string): boolean {\n\tconst binding = conversationBinding(key);\n\tif (!binding) return false;\n\tconst cfg = loadGatewayConfig();\n\tif (binding.owner && binding.owner !== binding.source.userId) return false;\n\treturn isAuthorized(sourceWithCurrentRole(key, binding.source), cfg, createPairingStore());\n}\n\nexport function gatewayDestination(key: string): { adapter: PlatformAdapter; source: MessageEvent[\"source\"] } {\n\tconst binding = conversationBinding(key);\n\tconst adapter = binding && adapters.get(binding.source.platform);\n\tif (!binding || !adapter || !canDeliver(key))\n\t\tthrow new Error(\"This chat is no longer authorized or its platform is disconnected.\");\n\treturn { adapter, source: binding.source };\n}\n\nexport function withGatewayPresentation<T>(key: string, run: () => Promise<T>): Promise<T> {\n\tconst { adapter, source } = gatewayDestination(key);\n\treturn runWithApprovalContext({ adapter, source, key }, run);\n}\n\nexport function startGatewayPresenter(connected: Map<string, PlatformAdapter>): void {\n\tadapters = connected;\n\tif (existsSync(path())) {\n\t\ttry {\n\t\t\tconst raw: unknown = JSON.parse(readFileSync(path(), \"utf8\"));\n\t\t\tif (\n\t\t\t\t!Array.isArray(raw) ||\n\t\t\t\t!raw.every(\n\t\t\t\t\t(item) =>\n\t\t\t\t\t\titem && typeof item.id === \"string\" && typeof item.key === \"string\" && typeof item.text === \"string\",\n\t\t\t\t)\n\t\t\t)\n\t\t\t\tthrow new Error(\"Invalid outbox\");\n\t\t\toutbox = raw as Outbound[];\n\t\t} catch {\n\t\t\tthrow new Error(\"Gateway outbox is invalid. Restore it before starting.\");\n\t\t}\n\t} else outbox = [];\n\tfor (const item of outbox) {\n\t\tif (item.kind === \"notice\") epochs.set(item.key, Math.max(epochs.get(item.key) ?? 0, item.epoch ?? 0));\n\t}\n\tif (timer) clearInterval(timer);\n\ttimer = setInterval(() => {\n\t\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n\t}, 3000);\n\ttimer.unref();\n\tvoid flushOutbox().catch((error) => console.error(\"[gateway] outbox flush failed:\", error));\n}\n\nexport function stopGatewayPresenter(): void {\n\tif (timer) clearInterval(timer);\n\ttimer = undefined;\n\tfor (const key of dialogs.keys()) invalidateGatewayDialogs(key);\n\tadapters = new Map();\n\tdeliveryConfigs.clear();\n\troleGrants.clear();\n}\n\nexport function gatewayEpoch(key: string): number {\n\treturn epochs.get(key) ?? 0;\n}\n\nexport async function sendGatewayNotice(\n\tkey: string,\n\ttext: string,\n\tkind: \"notice\" | \"result\" = \"notice\",\n\tepoch = gatewayEpoch(key),\n): Promise<void> {\n\tconst clean = text.replace(/\\x1b\\[[0-9;]*[a-zA-Z]/g, \"\").trim();\n\tif (!clean || !canDeliver(key)) return;\n\tconst { adapter, source } = gatewayDestination(key);\n\tawait queueGatewayText(key, source, adapter, clean, { kind, epoch });\n}\n\nexport async function queueGatewayText(\n\tkey: string,\n\tsource: MessageEvent[\"source\"],\n\tadapter: PlatformAdapter,\n\ttext: string,\n\toptions: { kind: \"result\" | \"notice\"; replyTo?: string; cfg?: GatewayConfig; chunks?: string[]; epoch?: number },\n): Promise<void> {\n\tif (!text || (options.kind === \"notice\" && options.epoch !== undefined && options.epoch !== gatewayEpoch(key)))\n\t\treturn;\n\tadapters.set(source.platform, adapter);\n\tif (options.cfg) deliveryConfigs.set(key, options.cfg);\n\tconst chunks = options.chunks ?? splitMessage(text, adapter.maxMessageLength);\n\tconst batchId = randomUUID();\n\tfor (const [index, chunk] of chunks.entries())\n\t\toutbox.push({\n\t\t\tid: randomUUID(),\n\t\t\tbatchId,\n\t\t\tkey,\n\t\t\tsource: { ...source, roleAuthorized: undefined },\n\t\t\tepoch: options.epoch ?? gatewayEpoch(key),\n\t\t\ttext: chunk,\n\t\t\tkind: options.kind,\n\t\t\treplyTo: index === 0 ? options.replyTo : undefined,\n\t\t});\n\tatomicJson(path(), outbox);\n\tif (activeDeliveries.has(key)) await activeDeliveries.get(key);\n\tawait flushKey(key);\n}\n\nexport function flushGatewayOutbox(): Promise<void> {\n\treturn flushOutbox();\n}\n\nfunction flushOutbox(): Promise<void> {\n\treturn Promise.all([...new Set(outbox.map((item) => item.key))].map(flushKey)).then(() => {});\n}\n\nfunction flushKey(key: string): Promise<void> {\n\tconst current = activeDeliveries.get(key);\n\tif (current) return current;\n\tconst running = drainKey(key).finally(() => {\n\t\tactiveDeliveries.delete(key);\n\t});\n\tactiveDeliveries.set(key, running);\n\treturn running;\n}\n\nasync function drainKey(key: string): Promise<void> {\n\tconst failedBatches = new Set(outbox.filter((item) => item.failed && item.batchId).map((item) => item.batchId));\n\tfor (const item of [...outbox].filter((item) => item.key === key)) {\n\t\tif (item.failed || (item.batchId && failedBatches.has(item.batchId))) continue;\n\t\tif (item.kind === \"notice\" && item.epoch !== (epochs.get(item.key) ?? 0)) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst binding = conversationBinding(item.key);\n\t\tconst source = item.source ?? binding?.source;\n\t\tif (!source) break;\n\t\tconst cfg = existsSync(gatewayConfigPath())\n\t\t\t? loadGatewayConfig()\n\t\t\t: (deliveryConfigs.get(item.key) ?? loadGatewayConfig());\n\t\tif (\n\t\t\t(binding &&\n\t\t\t\t(binding.source.platform !== source.platform ||\n\t\t\t\t\tbinding.source.chatId !== source.chatId ||\n\t\t\t\t\tbinding.source.userId !== source.userId ||\n\t\t\t\t\tbinding.source.threadId !== source.threadId)) ||\n\t\t\t!isAuthorized(sourceWithCurrentRole(key, source), cfg, createPairingStore()) ||\n\t\t\t(binding?.owner && binding.owner !== source.userId)\n\t\t) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t\tcontinue;\n\t\t}\n\t\tconst adapter = adapters.get(source.platform);\n\t\tif (!adapter || (item.nextAttempt ?? 0) > Date.now()) break;\n\t\tconst opts: SendOptions = { threadId: source.threadId, replyTo: item.replyTo };\n\t\tconst result = await adapter\n\t\t\t.send(source.chatId, item.text, opts)\n\t\t\t.catch((error: unknown) => ({ success: false as const, error: String(error), retryable: true }));\n\t\tif (result.success) {\n\t\t\toutbox = outbox.filter((v) => v.id !== item.id);\n\t\t\tatomicJson(path(), outbox);\n\t\t} else {\n\t\t\titem.attempts = (item.attempts ?? 0) + 1;\n\t\t\titem.failed = result.retryable === false || item.attempts >= MAX_ATTEMPTS;\n\t\t\titem.nextAttempt = Date.now() + Math.min(60_000, 3000 * 2 ** (item.attempts - 1));\n\t\t\tif (item.failed && item.batchId) {\n\t\t\t\tfailedBatches.add(item.batchId);\n\t\t\t\tfor (const pending of outbox) if (pending.batchId === item.batchId) pending.failed = true;\n\t\t\t}\n\t\t\tatomicJson(path(), outbox);\n\t\t\tconsole.error(\n\t\t\t\t`[gateway] delivery ${item.failed ? \"failed permanently\" : \"retry scheduled\"} for ${source.platform}: ${result.error ?? \"unknown error\"}`,\n\t\t\t);\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\nexport function invalidateGatewayDialogs(key: string): void {\n\tepochs.set(key, (epochs.get(key) ?? 0) + 1);\n\tconst remaining = outbox.filter((item) => item.key !== key || item.kind !== \"notice\");\n\tif (remaining.length !== outbox.length) {\n\t\toutbox = remaining;\n\t\tatomicJson(path(), outbox);\n\t}\n\tfor (const cancel of dialogs.get(key) ?? []) cancel();\n\tdialogs.delete(key);\n\tinputs.delete(key);\n}\n\nexport function acceptGatewayInput(key: string, event: MessageEvent): boolean {\n\tconst input = inputs.get(key);\n\tif (!input || input.userId !== event.source.userId || !canDeliver(key)) return false;\n\tif (event.text.startsWith(\"/\") && event.text !== \"/cancel\") return false;\n\tinput.resolve(event.text === \"/cancel\" ? undefined : event.text);\n\treturn true;\n}\n\nfunction dialog(\n\tkey: string,\n\topts: ExtensionUIDialogOptions | undefined,\n\tshow: (done: (value: string | undefined) => void, valid: () => boolean) => Promise<void>,\n): Promise<string | undefined> {\n\tconst epoch = epochs.get(key) ?? 0;\n\treturn new Promise((resolve, reject) => {\n\t\tlet settled = false;\n\t\tconst finish = (value: string | undefined, error?: unknown) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timeout);\n\t\t\topts?.signal?.removeEventListener(\"abort\", cancel);\n\t\t\tinputs.delete(key);\n\t\t\tdialogs.get(key)?.delete(cancel);\n\t\t\tif (error !== undefined) reject(error);\n\t\t\telse resolve(value);\n\t\t};\n\t\tconst done = (value: string | undefined) => finish(value);\n\t\tconst cancel = () => done(undefined);\n\t\tconst timeout = setTimeout(cancel, opts?.timeout ?? 300_000);\n\t\tconst set = dialogs.get(key) ?? new Set();\n\t\tset.add(cancel);\n\t\tdialogs.set(key, set);\n\t\topts?.signal?.addEventListener(\"abort\", cancel, { once: true });\n\t\tif (opts?.signal?.aborted) {\n\t\t\tcancel();\n\t\t\treturn;\n\t\t}\n\t\tvoid show(done, () => !settled && (epochs.get(key) ?? 0) === epoch && canDeliver(key)).catch((error) => {\n\t\t\tfinish(undefined, error);\n\t\t});\n\t});\n}\n\nexport function gatewaySelect(\n\tkey: string,\n\ttitle: string,\n\toptions: string[],\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\treturn dialog(key, opts, async (done, valid) => {\n\t\tconst { adapter, source } = gatewayDestination(key);\n\t\tconst result = await createPicker(\n\t\t\tadapter,\n\t\t\tsource,\n\t\t\t{\n\t\t\t\tkind: \"dialog\",\n\t\t\t\tsessionKey: key,\n\t\t\t\tinvokerId: source.userId,\n\t\t\t\ttitle,\n\t\t\t\titems: options.map((value) => ({ label: value, value })),\n\t\t\t\tvalidate: valid,\n\t\t\t\tonCancel: () => done(undefined),\n\t\t\t\tresolve: async (item) => {\n\t\t\t\t\tdone(item.value);\n\t\t\t\t\treturn { done: true, text: `${title}\\n${item.value}` };\n\t\t\t\t},\n\t\t\t},\n\t\t\t{ threadId: source.threadId },\n\t\t);\n\t\tif (!result.success) throw new Error(result.error ?? \"Could not send picker.\");\n\t});\n}\n\nexport function gatewayInput(\n\tkey: string,\n\ttitle: string,\n\tplaceholder?: string,\n\topts?: ExtensionUIDialogOptions,\n): Promise<string | undefined> {\n\tif (inputs.has(key)) return Promise.reject(new Error(\"Answer or cancel the existing question first.\"));\n\tconst epoch = gatewayEpoch(key);\n\treturn dialog(key, opts, async (done) => {\n\t\tconst { source } = gatewayDestination(key);\n\t\tinputs.set(key, { userId: source.userId, resolve: done });\n\t\tawait sendGatewayNotice(\n\t\t\tkey,\n\t\t\t`${title}${placeholder ? `\\n${placeholder}` : \"\"}\\nReply with text, or /cancel.`,\n\t\t\t\"notice\",\n\t\t\tepoch,\n\t\t);\n\t});\n}\n\nexport function createGatewayUI(key: string): ExtensionUIContext {\n\tconst epoch = gatewayEpoch(key);\n\treturn {\n\t\t...noOpUIContext,\n\t\tselect: (title, options, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewaySelect(key, title, options, opts) : Promise.resolve(undefined),\n\t\tconfirm: async (title, message, opts) =>\n\t\t\tgatewayEpoch(key) === epoch &&\n\t\t\t(await gatewaySelect(key, `${title}\\n${message}`, [\"Approve\", \"Decline\"], opts)) === \"Approve\",\n\t\tinput: (title, placeholder, opts) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, placeholder, opts) : Promise.resolve(undefined),\n\t\teditor: (title, prefill) =>\n\t\t\tgatewayEpoch(key) === epoch ? gatewayInput(key, title, prefill) : Promise.resolve(undefined),\n\t\tnotify: (message) => {\n\t\t\tvoid sendGatewayNotice(key, message, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tcustom: async () => {\n\t\t\tthrow new Error(\n\t\t\t\t\"This command requires the terminal interface. Use the gateway's text commands or button pickers.\",\n\t\t\t);\n\t\t},\n\t\tpasteToEditor: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t\tsetEditorText: (text) => {\n\t\t\tvoid sendGatewayNotice(key, text, \"notice\", epoch).catch(() => {});\n\t\t},\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAU,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI7G,OAAO,EAAE,KAAK,aAAa,EAA2D,MAAM,aAAa,CAAC;AAG1G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAYjD,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,yEAAyE;AACzE,MAAM,WAAW,UAAU;IAC1B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC5C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACzE,WAAW,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAC;IACrD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,GAAG,EAAE,aAAa,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACtB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAcD,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAmSrD","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded into a successful streaming preview edit.\n * Failed final edits and ordinary answers enter the durable outbox, which\n * acknowledges chunks in order and retries by destination.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, isGatewayOwner, requireGatewayOwner } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { bindConversation, conversationBinding } from \"./conversations.ts\";\nimport { resolveWithinRoots } from \"./mobile-commands.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport {\n\tacceptGatewayInput,\n\tgatewayEpoch,\n\tinvalidateGatewayDialogs,\n\tqueueGatewayText,\n\trememberGatewayRole,\n} from \"./presenter.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { getSession as getStoredSession } from \"./store.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string, create?: boolean): Promise<BridgeSession | null>;\n\tpeekSession?(key: string): BridgeSession | undefined;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n\tremoteControls?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// Headless gateway sessions still prompt for large subagent launches in yolo mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\tmessage: string,\n\t\tepoch: number,\n\t): Promise<void> {\n\t\tawait queueGatewayText(key, event.source, adapter, `⚠ ${oneLine(message)}`, {\n\t\t\tkind: \"notice\",\n\t\t\treplyTo: event.messageId,\n\t\t\tcfg,\n\t\t\tepoch,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)}. Ask the computer's owner to run lunr gateway pair approve ${source.platform} ${formatPairingCode(code)} --owner if this is their account. Approval without --owner grants chat access only.`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\tfunction workspaceReadiness(event: MessageEvent, key: string, cfg: GatewayConfig): string | undefined {\n\t\tif (getStoredSession(key) || conversationBinding(key)?.cwd) return undefined;\n\t\tif (!isGatewayOwner(event.source, cfg))\n\t\t\treturn \"This chat is paired but has no project. Ask the computer's gateway owner to configure your access locally. /project and /continue require owner access.\";\n\t\tconst root = cfg.defaultProject;\n\t\tif (!root) return \"No default project selected. Use /project in this private chat to choose an approved folder.\";\n\t\ttry {\n\t\t\tconst cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);\n\t\t\tbindConversation(key, event.source, { cwd, owner: event.source.userId });\n\t\t\treturn undefined;\n\t\t} catch {\n\t\t\treturn \"Default project is no longer an approved folder. Use /project to choose an approved folder.\";\n\t\t}\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tif (deps.remoteControls) {\n\t\t\tconst { handleMobileCommand, cancelMobileTransfer } = await import(\"./mobile-commands.ts\");\n\t\t\tif ([\"stop\", \"stopall\", \"new\", \"cancel\"].includes(commandWord)) cancelMobileTransfer(key);\n\t\t\tif (commandWord === \"cancel\") {\n\t\t\t\tinvalidateGatewayDialogs(key);\n\t\t\t\tawait adapter.send(event.source.chatId, \"Cancelled the pending selection or transfer.\", {\n\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst consumed = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\t\thandleMobileCommand({ key, event, adapter, bridge, cfg: freshCfg() }, commandWord, args),\n\t\t\t);\n\t\t\tif (consumed) return true;\n\t\t\tif (!event.text.startsWith(\"/\")) return false;\n\t\t}\n\t\tif (commandWord === \"start\") {\n\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, freshCfg()) : undefined;\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst session = await bridge.getSession(key, true);\n\t\t\t\tconst models = await session?.modelRuntime.getAvailable();\n\t\t\t\tif (!models?.length)\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"No authenticated model available. Use /login on your computer, then try /start again.\",\n\t\t\t\t\t);\n\t\t\t\telse if (\n\t\t\t\t\t!session?.model ||\n\t\t\t\t\t!models.some((model) => model.id === session.model?.id && model.provider === session.model.provider)\n\t\t\t\t)\n\t\t\t\t\tawait sendCommandReply(adapter, event, \"No available model selected. Use /model here to choose one.\");\n\t\t\t\telse\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"Ready. Send a message to begin, or use /model to choose a model.\",\n\t\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\tawait sendCommandReply(\n\t\t\t\t\tadapter,\n\t\t\t\t\tevent,\n\t\t\t\t\t\"Session setup failed. Run lunr gateway doctor on your computer, then try /start again.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (deps.remoteControls && commandWord === \"model\") {\n\t\t\tconst issue = workspaceReadiness(event, key, freshCfg());\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\tcfg: freshCfg(),\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));\n\t}\n\n\t/** A successful preview is finalized by edit; failed edits fall back to a durable full reply. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tif (opts.editMessageId) {\n\t\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\t\tconst edit = await adapter\n\t\t\t\t.editMessage(event.source.chatId, opts.editMessageId, opts.previewTruncated ? chunks[0] : filtered)\n\t\t\t\t.catch(() => ({ success: false }));\n\t\t\tif (edit.success) {\n\t\t\t\tif (!opts.previewTruncated) return;\n\t\t\t\tawait queueGatewayText(key, event.source, adapter, chunks.slice(1).join(\"\"), {\n\t\t\t\t\tkind: \"result\",\n\t\t\t\t\tcfg,\n\t\t\t\t\tchunks: chunks.slice(1),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tawait queueGatewayText(key, event.source, adapter, filtered, {\n\t\t\tkind: \"result\",\n\t\t\treplyTo: opts.reply ? event.messageId : undefined,\n\t\t\tcfg,\n\t\t});\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst epoch = gatewayEpoch(key);\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tconst result = await adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t\tif (!result.success) throw new Error(result.error ?? \"Preview edit failed\");\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, key, cfg, { reply: false }).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] follow-up delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, key, cfg, message, epoch).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] error delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, key, cfg, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\tconst epoch = gatewayEpoch(key);\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\trememberGatewayRole(key, event.source);\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tif (deps.remoteControls) {\n\t\t\t\t\tconst binding = conversationBinding(key);\n\t\t\t\t\tif (binding?.owner) {\n\t\t\t\t\t\trequireGatewayOwner(event.source, cfg);\n\t\t\t\t\t\tif (binding.owner !== event.source.userId)\n\t\t\t\t\t\t\tthrow new Error(\"This session belongs to a different owner.\");\n\t\t\t\t\t}\n\t\t\t\t\tbindConversation(key, event.source);\n\t\t\t\t\tif (acceptGatewayInput(key, event)) return;\n\t\t\t\t}\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, cfg) : undefined;\n\t\t\t\tif (issue) {\n\t\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, key, cfg, err instanceof Error ? err.message : String(err), epoch).catch(\n\t\t\t\t\t() => {},\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAU,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI7G,OAAO,EAAE,KAAK,aAAa,EAA2D,MAAM,aAAa,CAAC;AAG1G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAYjD,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,yEAAyE;AACzE,MAAM,WAAW,UAAU;IAC1B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC5C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACzE,WAAW,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAC;IACrD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,GAAG,EAAE,aAAa,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACtB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAcD,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiSrD","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded into a successful streaming preview edit.\n * Failed final edits and ordinary answers enter the durable outbox, which\n * acknowledges chunks in order and retries by destination.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, requireAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { bindConversation, conversationBinding } from \"./conversations.ts\";\nimport { resolveWithinRoots } from \"./mobile-commands.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport {\n\tacceptGatewayInput,\n\tgatewayEpoch,\n\tinvalidateGatewayDialogs,\n\tqueueGatewayText,\n\trememberGatewayRole,\n} from \"./presenter.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { getSession as getStoredSession } from \"./store.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string, create?: boolean): Promise<BridgeSession | null>;\n\tpeekSession?(key: string): BridgeSession | undefined;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n\tremoteControls?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// Headless gateway sessions still prompt for large subagent launches in yolo mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\tmessage: string,\n\t\tepoch: number,\n\t): Promise<void> {\n\t\tawait queueGatewayText(key, event.source, adapter, `⚠ ${oneLine(message)}`, {\n\t\t\tkind: \"notice\",\n\t\t\treplyTo: event.messageId,\n\t\t\tcfg,\n\t\t\tepoch,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)}. On the computer running lunr, approve access with: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}. Approval grants full gateway access, including local projects and saved sessions.`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\tfunction workspaceReadiness(event: MessageEvent, key: string, cfg: GatewayConfig): string | undefined {\n\t\tif (getStoredSession(key) || conversationBinding(key)?.cwd) return undefined;\n\t\tconst root = cfg.defaultProject;\n\t\tif (!root) return \"No default project selected. Use /project to choose an approved folder.\";\n\t\ttry {\n\t\t\tconst cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);\n\t\t\tbindConversation(key, event.source, { cwd, owner: event.source.userId });\n\t\t\treturn undefined;\n\t\t} catch {\n\t\t\treturn \"Default project is no longer an approved folder. Use /project to choose an approved folder.\";\n\t\t}\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tif (deps.remoteControls) {\n\t\t\tconst { handleMobileCommand, cancelMobileTransfer } = await import(\"./mobile-commands.ts\");\n\t\t\tif ([\"stop\", \"stopall\", \"new\", \"cancel\"].includes(commandWord)) cancelMobileTransfer(key);\n\t\t\tif (commandWord === \"cancel\") {\n\t\t\t\tinvalidateGatewayDialogs(key);\n\t\t\t\tawait adapter.send(event.source.chatId, \"Cancelled the pending selection or transfer.\", {\n\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst consumed = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\t\thandleMobileCommand({ key, event, adapter, bridge, cfg: freshCfg() }, commandWord, args),\n\t\t\t);\n\t\t\tif (consumed) return true;\n\t\t\tif (!event.text.startsWith(\"/\")) return false;\n\t\t}\n\t\tif (commandWord === \"start\") {\n\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, freshCfg()) : undefined;\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst session = await bridge.getSession(key, true);\n\t\t\t\tconst models = await session?.modelRuntime.getAvailable();\n\t\t\t\tif (!models?.length)\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"No authenticated model available. Use /login on your computer, then try /start again.\",\n\t\t\t\t\t);\n\t\t\t\telse if (\n\t\t\t\t\t!session?.model ||\n\t\t\t\t\t!models.some((model) => model.id === session.model?.id && model.provider === session.model.provider)\n\t\t\t\t)\n\t\t\t\t\tawait sendCommandReply(adapter, event, \"No available model selected. Use /model here to choose one.\");\n\t\t\t\telse\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"Ready. Send a message to begin, or use /model to choose a model.\",\n\t\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\tawait sendCommandReply(\n\t\t\t\t\tadapter,\n\t\t\t\t\tevent,\n\t\t\t\t\t\"Session setup failed. Run lunr gateway doctor on your computer, then try /start again.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (deps.remoteControls && commandWord === \"model\") {\n\t\t\tconst issue = workspaceReadiness(event, key, freshCfg());\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\tcfg: freshCfg(),\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));\n\t}\n\n\t/** A successful preview is finalized by edit; failed edits fall back to a durable full reply. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tif (opts.editMessageId) {\n\t\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\t\tconst edit = await adapter\n\t\t\t\t.editMessage(event.source.chatId, opts.editMessageId, opts.previewTruncated ? chunks[0] : filtered)\n\t\t\t\t.catch(() => ({ success: false }));\n\t\t\tif (edit.success) {\n\t\t\t\tif (!opts.previewTruncated) return;\n\t\t\t\tawait queueGatewayText(key, event.source, adapter, chunks.slice(1).join(\"\"), {\n\t\t\t\t\tkind: \"result\",\n\t\t\t\t\tcfg,\n\t\t\t\t\tchunks: chunks.slice(1),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tawait queueGatewayText(key, event.source, adapter, filtered, {\n\t\t\tkind: \"result\",\n\t\t\treplyTo: opts.reply ? event.messageId : undefined,\n\t\t\tcfg,\n\t\t});\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst epoch = gatewayEpoch(key);\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tconst result = await adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t\tif (!result.success) throw new Error(result.error ?? \"Preview edit failed\");\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, key, cfg, { reply: false }).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] follow-up delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, key, cfg, message, epoch).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] error delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, key, cfg, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\tconst epoch = gatewayEpoch(key);\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\trememberGatewayRole(key, event.source);\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tif (deps.remoteControls) {\n\t\t\t\t\tconst binding = conversationBinding(key);\n\t\t\t\t\tif (binding?.owner) {\n\t\t\t\t\t\trequireAuthorized(event.source, cfg);\n\t\t\t\t\t\tif (binding.owner !== event.source.userId)\n\t\t\t\t\t\t\tthrow new Error(\"This session belongs to a different user.\");\n\t\t\t\t\t}\n\t\t\t\t\tbindConversation(key, event.source);\n\t\t\t\t\tif (acceptGatewayInput(key, event)) return;\n\t\t\t\t}\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, cfg) : undefined;\n\t\t\t\tif (issue) {\n\t\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, key, cfg, err instanceof Error ? err.message : String(err), epoch).catch(\n\t\t\t\t\t() => {},\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
package/dist/gateway/router.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
19
|
import { QUEUED } from "./agent-bridge.js";
|
|
20
20
|
import { registerGatewayApprovalHandler, runWithApprovalContext } from "./approval.js";
|
|
21
|
-
import { isAuthorized,
|
|
21
|
+
import { isAuthorized, requireAuthorized } from "./authz.js";
|
|
22
22
|
import { CHAT_COMMANDS, runChatCommand, sendCommandReply } from "./commands.js";
|
|
23
23
|
import { gatewayConfigPath, loadGatewayConfig, platformConfigFor } from "./config.js";
|
|
24
24
|
import { bindConversation, conversationBinding } from "./conversations.js";
|
|
@@ -95,17 +95,15 @@ export function createRouter(deps) {
|
|
|
95
95
|
const code = pairing.issueCode(source.platform, source.userId);
|
|
96
96
|
if (code === null)
|
|
97
97
|
return true; // rate-limited or pending list full: stay silent
|
|
98
|
-
await adapter.send(source.chatId, `Your lunR pairing code: ${formatPairingCode(code)}.
|
|
98
|
+
await adapter.send(source.chatId, `Your lunR pairing code: ${formatPairingCode(code)}. On the computer running lunr, approve access with: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}. Approval grants full gateway access, including local projects and saved sessions.`, { replyTo: event.messageId, threadId: source.threadId });
|
|
99
99
|
return true;
|
|
100
100
|
}
|
|
101
101
|
function workspaceReadiness(event, key, cfg) {
|
|
102
102
|
if (getStoredSession(key) || conversationBinding(key)?.cwd)
|
|
103
103
|
return undefined;
|
|
104
|
-
if (!isGatewayOwner(event.source, cfg))
|
|
105
|
-
return "This chat is paired but has no project. Ask the computer's gateway owner to configure your access locally. /project and /continue require owner access.";
|
|
106
104
|
const root = cfg.defaultProject;
|
|
107
105
|
if (!root)
|
|
108
|
-
return "No default project selected. Use /project
|
|
106
|
+
return "No default project selected. Use /project to choose an approved folder.";
|
|
109
107
|
try {
|
|
110
108
|
const cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);
|
|
111
109
|
bindConversation(key, event.source, { cwd, owner: event.source.userId });
|
|
@@ -271,9 +269,9 @@ export function createRouter(deps) {
|
|
|
271
269
|
if (deps.remoteControls) {
|
|
272
270
|
const binding = conversationBinding(key);
|
|
273
271
|
if (binding?.owner) {
|
|
274
|
-
|
|
272
|
+
requireAuthorized(event.source, cfg);
|
|
275
273
|
if (binding.owner !== event.source.userId)
|
|
276
|
-
throw new Error("This session belongs to a different
|
|
274
|
+
throw new Error("This session belongs to a different user.");
|
|
277
275
|
}
|
|
278
276
|
bindConversation(key, event.source);
|
|
279
277
|
if (acceptGatewayInput(key, event))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAgD,MAAM,EAAsB,MAAM,mBAAmB,CAAC;AAC7G,OAAO,EAAE,8BAA8B,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAsB,iBAAiB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC1G,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EACN,kBAAkB,EAClB,YAAY,EACZ,wBAAwB,EACxB,gBAAgB,EAChB,mBAAmB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AA8BzC,SAAS,OAAO,CAAC,IAAY,EAAU;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAAA,CACtD;AAED,SAAS,iBAAiB,CAAC,IAAY,EAAU;IAChD,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAAA,CACzE;AAED,SAAS,YAAY,CAAC,KAAmB,EAAW;IACnD,OAAO,KAAK,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAAA,CAC7C;AAED,MAAM,UAAU,YAAY,CAAC,IAAgB,EAAU;IACtD,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAE1E,mFAAmF;IACnF,8BAA8B,EAAE,CAAC;IAEjC,2GAA2G;IAC3G,SAAS,QAAQ,GAAkB;QAClC,IAAI,CAAC,YAAY;YAAE,OAAO,UAAU,CAAC;QACrC,IAAI,CAAC;YACJ,IAAI,UAAU,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC;gBACrC,OAAO,iBAAiB,EAAE,CAAC;YAC5B,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,iCAAiC;QAClC,CAAC;QACD,OAAO,UAAU,CAAC;IAAA,CAClB;IAED,KAAK,UAAU,SAAS,CACvB,OAAwB,EACxB,KAAmB,EACnB,GAAW,EACX,GAAkB,EAClB,OAAe,EACf,KAAa,EACG;QAChB,MAAM,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,OAAK,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE;YAC3E,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,KAAK,CAAC,SAAS;YACxB,GAAG;YACH,KAAK;SACL,CAAC,CAAC;IAAA,CACH;IAED,oFAAoF;IACpF,SAAS,YAAY,CAAC,KAAmB,EAAE,GAAkB,EAAW;QACvE,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;QACzB,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC3C,MAAM,WAAW,GAAG,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5D,IAAI,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,YAAY,GAAG,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3E,IACC,WAAW,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;YACnC,CAAC,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;YACjD,CAAC,YAAY;YACb,CAAC,YAAY,CAAC,KAAK,CAAC,EACnB,CAAC;YACF,OAAO,IAAI,CAAC;QACb,CAAC;QACD,IAAI,WAAW,CAAC,cAAc,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;YACzE,OAAO,IAAI,CAAC;QACb,CAAC;QACD,OAAO,KAAK,CAAC;IAAA,CACb;IAED,2FAA2F;IAC3F,KAAK,UAAU,QAAQ,CAAC,OAAwB,EAAE,KAAmB,EAAE,GAAkB,EAAoB;QAC5G,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QAC3D,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;QACzB,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,uBAAuB;QAClE,IAAI,GAAG,CAAC,sBAAsB,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QACvD,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,iDAAiD;QACjF,MAAM,OAAO,CAAC,IAAI,CACjB,MAAM,CAAC,MAAM,EACb,2BAA2B,iBAAiB,CAAC,IAAI,CAAC,+DAA+D,MAAM,CAAC,QAAQ,IAAI,iBAAiB,CAAC,IAAI,CAAC,sFAAsF,EACjP,EAAE,OAAO,EAAE,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CACvD,CAAC;QACF,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,SAAS,kBAAkB,CAAC,KAAmB,EAAE,GAAW,EAAE,GAAkB,EAAsB;QACrG,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,GAAG,CAAC,EAAE,GAAG;YAAE,OAAO,SAAS,CAAC;QAC7E,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC;YACrC,OAAO,yJAAyJ,CAAC;QAClK,MAAM,IAAI,GAAG,GAAG,CAAC,cAAc,CAAC;QAChC,IAAI,CAAC,IAAI;YAAE,OAAO,8FAA8F,CAAC;QACjH,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;YAC7D,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;YACzE,OAAO,SAAS,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,6FAA6F,CAAC;QACtG,CAAC;IAAA,CACD;IAED,0EAA0E;IAC1E,KAAK,UAAU,WAAW,CAAC,OAAwB,EAAE,KAAmB,EAAE,GAAW,EAAoB;QACxG,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACzD,MAAM,WAAW,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,MAAM,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;YAC3F,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAAE,oBAAoB,CAAC,GAAG,CAAC,CAAC;YAC1F,IAAI,WAAW,KAAK,QAAQ,EAAE,CAAC;gBAC9B,wBAAwB,CAAC,GAAG,CAAC,CAAC;gBAC9B,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,8CAA8C,EAAE;oBACvF,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;iBAC/B,CAAC,CAAC;gBACH,OAAO,IAAI,CAAC;YACb,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,sBAAsB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAC1F,mBAAmB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CACxF,CAAC;YACF,IAAI,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,OAAO,KAAK,CAAC;QAC/C,CAAC;QACD,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC3F,IAAI,KAAK,EAAE,CAAC;gBACX,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;gBAC9C,OAAO,IAAI,CAAC;YACb,CAAC;YACD,IAAI,CAAC;gBACJ,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBACnD,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,YAAY,CAAC,YAAY,EAAE,CAAC;gBAC1D,IAAI,CAAC,MAAM,EAAE,MAAM;oBAClB,MAAM,gBAAgB,CACrB,OAAO,EACP,KAAK,EACL,uFAAuF,CACvF,CAAC;qBACE,IACJ,CAAC,OAAO,EAAE,KAAK;oBACf,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,KAAK,EAAE,EAAE,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;oBAEpG,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,6DAA6D,CAAC,CAAC;;oBAEtG,MAAM,gBAAgB,CACrB,OAAO,EACP,KAAK,EACL,kEAAkE,CAClE,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACR,MAAM,gBAAgB,CACrB,OAAO,EACP,KAAK,EACL,wFAAwF,CACxF,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACb,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,IAAI,WAAW,KAAK,OAAO,EAAE,CAAC;YACpD,MAAM,KAAK,GAAG,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;YACzD,IAAI,KAAK,EAAE,CAAC;gBACX,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;gBAC9C,OAAO,IAAI,CAAC;YACb,CAAC;QACF,CAAC;QACD,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;QAClG,IAAI,CAAC,GAAG,EAAE,CAAC;YACV,OAAO,KAAK,CAAC,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC;QACvC,CAAC;QACD,MAAM,GAAG,GAAG;YACX,KAAK;YACL,GAAG;YACH,OAAO;YACP,MAAM;YACN,GAAG,EAAE,QAAQ,EAAE;YACf,IAAI;YACJ,KAAK,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;SACrE,CAAC;QACF,OAAO,sBAAsB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAAA,CACtG;IAED,iGAAiG;IACjG,KAAK,UAAU,OAAO,CACrB,OAAwB,EACxB,KAAmB,EACnB,IAAY,EACZ,GAAW,EACX,GAAkB,EAClB,IAA4E,EAC5D;QAChB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO;QAC9B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;YAChE,MAAM,IAAI,GAAG,MAAM,OAAO;iBACxB,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;iBAClG,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;YACpC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClB,IAAI,CAAC,IAAI,CAAC,gBAAgB;oBAAE,OAAO;gBACnC,MAAM,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;oBAC5E,IAAI,EAAE,QAAQ;oBACd,GAAG;oBACH,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;iBACvB,CAAC,CAAC;gBACH,OAAO;YACR,CAAC;QACF,CAAC;QACD,MAAM,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE;YAC5D,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;YACjD,GAAG;SACH,CAAC,CAAC;IAAA,CACH;IAED,KAAK,UAAU,OAAO,CACrB,OAAwB,EACxB,KAAmB,EACnB,GAAW,EACX,GAAkB,EACF;QAChB,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,WAAW,KAAK,UAAU,CAAC;QACrF,IAAI,QAAoC,CAAC;QACzC,IAAI,SAAS,EAAE,CAAC;YACf,QAAQ,GAAG,IAAI,cAAc,CAAC;gBAC7B,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;oBAC5B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE;wBAC5D,OAAO,EAAE,KAAK,CAAC,SAAS;wBACxB,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;qBAC/B,CAAC,CAAC;oBACH,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAAA,CAC1D;gBACD,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;oBAChC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;oBAC/E,IAAI,CAAC,MAAM,CAAC,OAAO;wBAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,IAAI,qBAAqB,CAAC,CAAC;gBAAA,CAC5E;gBACD,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,cAAc;gBACxC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,eAAe;gBACxC,UAAU,EAAE,OAAO,CAAC,gBAAgB;aACpC,CAAC,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAkB;YAChC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;YAC/D,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC3B,KAAK,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAC9E,OAAO,CAAC,KAAK,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAC5D,CAAC;YAAA,CACF;YACD,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBACrB,KAAK,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CACxE,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CACxD,CAAC;YAAA,CACF;SACD,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,CACxF,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,CACrC,CAAC;QACF,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,CAAC,yCAAyC;QACxE,IAAI,QAAQ;YAAE,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACxC,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE;YAC/C,KAAK,EAAE,CAAC,QAAQ;YAChB,aAAa,EAAE,QAAQ,EAAE,aAAa,IAAI,SAAS;YACnD,gBAAgB,EAAE,QAAQ,EAAE,SAAS,IAAI,KAAK;SAC9C,CAAC,CAAC;IAAA,CACH;IAED,OAAO;QACN,KAAK,CAAC,WAAW,CAAC,KAAmB,EAAiB;YACrD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACpD,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;YACvB,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,oBAAoB,EAAE,GAAG,CAAC,oBAAoB,EAAE,CAAC,CAAC;YAC9F,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,CAAC;gBACJ,IAAI,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC;oBAAE,OAAO;gBACrC,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACvC,IAAI,MAAM,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC;oBAAE,OAAO;gBAChD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;oBACzB,MAAM,OAAO,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;oBACzC,IAAI,OAAO,EAAE,KAAK,EAAE,CAAC;wBACpB,mBAAmB,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;wBACvC,IAAI,OAAO,CAAC,KAAK,KAAK,KAAK,CAAC,MAAM,CAAC,MAAM;4BACxC,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;oBAChE,CAAC;oBACD,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;oBACpC,IAAI,kBAAkB,CAAC,GAAG,EAAE,KAAK,CAAC;wBAAE,OAAO;gBAC5C,CAAC;gBACD,IAAI,MAAM,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC;oBAAE,OAAO;gBACnD,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACpF,IAAI,KAAK,EAAE,CAAC;oBACX,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;oBAC9C,OAAO;gBACR,CAAC;gBACD,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;gBACrF,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,MAAM,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,CACvG,GAAG,EAAE,CAAC,EAAC,CAAC,CACR,CAAC;YACH,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded into a successful streaming preview edit.\n * Failed final edits and ordinary answers enter the durable outbox, which\n * acknowledges chunks in order and retries by destination.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, isGatewayOwner, requireGatewayOwner } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { bindConversation, conversationBinding } from \"./conversations.ts\";\nimport { resolveWithinRoots } from \"./mobile-commands.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport {\n\tacceptGatewayInput,\n\tgatewayEpoch,\n\tinvalidateGatewayDialogs,\n\tqueueGatewayText,\n\trememberGatewayRole,\n} from \"./presenter.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { getSession as getStoredSession } from \"./store.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string, create?: boolean): Promise<BridgeSession | null>;\n\tpeekSession?(key: string): BridgeSession | undefined;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n\tremoteControls?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// Headless gateway sessions still prompt for large subagent launches in yolo mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\tmessage: string,\n\t\tepoch: number,\n\t): Promise<void> {\n\t\tawait queueGatewayText(key, event.source, adapter, `⚠ ${oneLine(message)}`, {\n\t\t\tkind: \"notice\",\n\t\t\treplyTo: event.messageId,\n\t\t\tcfg,\n\t\t\tepoch,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)}. Ask the computer's owner to run lunr gateway pair approve ${source.platform} ${formatPairingCode(code)} --owner if this is their account. Approval without --owner grants chat access only.`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\tfunction workspaceReadiness(event: MessageEvent, key: string, cfg: GatewayConfig): string | undefined {\n\t\tif (getStoredSession(key) || conversationBinding(key)?.cwd) return undefined;\n\t\tif (!isGatewayOwner(event.source, cfg))\n\t\t\treturn \"This chat is paired but has no project. Ask the computer's gateway owner to configure your access locally. /project and /continue require owner access.\";\n\t\tconst root = cfg.defaultProject;\n\t\tif (!root) return \"No default project selected. Use /project in this private chat to choose an approved folder.\";\n\t\ttry {\n\t\t\tconst cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);\n\t\t\tbindConversation(key, event.source, { cwd, owner: event.source.userId });\n\t\t\treturn undefined;\n\t\t} catch {\n\t\t\treturn \"Default project is no longer an approved folder. Use /project to choose an approved folder.\";\n\t\t}\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tif (deps.remoteControls) {\n\t\t\tconst { handleMobileCommand, cancelMobileTransfer } = await import(\"./mobile-commands.ts\");\n\t\t\tif ([\"stop\", \"stopall\", \"new\", \"cancel\"].includes(commandWord)) cancelMobileTransfer(key);\n\t\t\tif (commandWord === \"cancel\") {\n\t\t\t\tinvalidateGatewayDialogs(key);\n\t\t\t\tawait adapter.send(event.source.chatId, \"Cancelled the pending selection or transfer.\", {\n\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst consumed = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\t\thandleMobileCommand({ key, event, adapter, bridge, cfg: freshCfg() }, commandWord, args),\n\t\t\t);\n\t\t\tif (consumed) return true;\n\t\t\tif (!event.text.startsWith(\"/\")) return false;\n\t\t}\n\t\tif (commandWord === \"start\") {\n\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, freshCfg()) : undefined;\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst session = await bridge.getSession(key, true);\n\t\t\t\tconst models = await session?.modelRuntime.getAvailable();\n\t\t\t\tif (!models?.length)\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"No authenticated model available. Use /login on your computer, then try /start again.\",\n\t\t\t\t\t);\n\t\t\t\telse if (\n\t\t\t\t\t!session?.model ||\n\t\t\t\t\t!models.some((model) => model.id === session.model?.id && model.provider === session.model.provider)\n\t\t\t\t)\n\t\t\t\t\tawait sendCommandReply(adapter, event, \"No available model selected. Use /model here to choose one.\");\n\t\t\t\telse\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"Ready. Send a message to begin, or use /model to choose a model.\",\n\t\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\tawait sendCommandReply(\n\t\t\t\t\tadapter,\n\t\t\t\t\tevent,\n\t\t\t\t\t\"Session setup failed. Run lunr gateway doctor on your computer, then try /start again.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (deps.remoteControls && commandWord === \"model\") {\n\t\t\tconst issue = workspaceReadiness(event, key, freshCfg());\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\tcfg: freshCfg(),\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));\n\t}\n\n\t/** A successful preview is finalized by edit; failed edits fall back to a durable full reply. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tif (opts.editMessageId) {\n\t\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\t\tconst edit = await adapter\n\t\t\t\t.editMessage(event.source.chatId, opts.editMessageId, opts.previewTruncated ? chunks[0] : filtered)\n\t\t\t\t.catch(() => ({ success: false }));\n\t\t\tif (edit.success) {\n\t\t\t\tif (!opts.previewTruncated) return;\n\t\t\t\tawait queueGatewayText(key, event.source, adapter, chunks.slice(1).join(\"\"), {\n\t\t\t\t\tkind: \"result\",\n\t\t\t\t\tcfg,\n\t\t\t\t\tchunks: chunks.slice(1),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tawait queueGatewayText(key, event.source, adapter, filtered, {\n\t\t\tkind: \"result\",\n\t\t\treplyTo: opts.reply ? event.messageId : undefined,\n\t\t\tcfg,\n\t\t});\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst epoch = gatewayEpoch(key);\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tconst result = await adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t\tif (!result.success) throw new Error(result.error ?? \"Preview edit failed\");\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, key, cfg, { reply: false }).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] follow-up delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, key, cfg, message, epoch).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] error delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, key, cfg, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\tconst epoch = gatewayEpoch(key);\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\trememberGatewayRole(key, event.source);\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tif (deps.remoteControls) {\n\t\t\t\t\tconst binding = conversationBinding(key);\n\t\t\t\t\tif (binding?.owner) {\n\t\t\t\t\t\trequireGatewayOwner(event.source, cfg);\n\t\t\t\t\t\tif (binding.owner !== event.source.userId)\n\t\t\t\t\t\t\tthrow new Error(\"This session belongs to a different owner.\");\n\t\t\t\t\t}\n\t\t\t\t\tbindConversation(key, event.source);\n\t\t\t\t\tif (acceptGatewayInput(key, event)) return;\n\t\t\t\t}\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, cfg) : undefined;\n\t\t\t\tif (issue) {\n\t\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, key, cfg, err instanceof Error ? err.message : String(err), epoch).catch(\n\t\t\t\t\t() => {},\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded into a successful streaming preview edit.\n * Failed final edits and ordinary answers enter the durable outbox, which\n * acknowledges chunks in order and retries by destination.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized, requireAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { bindConversation, conversationBinding } from \"./conversations.ts\";\nimport { resolveWithinRoots } from \"./mobile-commands.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport {\n\tacceptGatewayInput,\n\tgatewayEpoch,\n\tinvalidateGatewayDialogs,\n\tqueueGatewayText,\n\trememberGatewayRole,\n} from \"./presenter.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { getSession as getStoredSession } from \"./store.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string, create?: boolean): Promise<BridgeSession | null>;\n\tpeekSession?(key: string): BridgeSession | undefined;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n\tremoteControls?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// Headless gateway sessions still prompt for large subagent launches in yolo mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\tmessage: string,\n\t\tepoch: number,\n\t): Promise<void> {\n\t\tawait queueGatewayText(key, event.source, adapter, `⚠ ${oneLine(message)}`, {\n\t\t\tkind: \"notice\",\n\t\t\treplyTo: event.messageId,\n\t\t\tcfg,\n\t\t\tepoch,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)}. On the computer running lunr, approve access with: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}. Approval grants full gateway access, including local projects and saved sessions.`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\tfunction workspaceReadiness(event: MessageEvent, key: string, cfg: GatewayConfig): string | undefined {\n\t\tif (getStoredSession(key) || conversationBinding(key)?.cwd) return undefined;\n\t\tconst root = cfg.defaultProject;\n\t\tif (!root) return \"No default project selected. Use /project to choose an approved folder.\";\n\t\ttry {\n\t\t\tconst cwd = resolveWithinRoots(root, cfg.projectRoots ?? []);\n\t\t\tbindConversation(key, event.source, { cwd, owner: event.source.userId });\n\t\t\treturn undefined;\n\t\t} catch {\n\t\t\treturn \"Default project is no longer an approved folder. Use /project to choose an approved folder.\";\n\t\t}\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tif (deps.remoteControls) {\n\t\t\tconst { handleMobileCommand, cancelMobileTransfer } = await import(\"./mobile-commands.ts\");\n\t\t\tif ([\"stop\", \"stopall\", \"new\", \"cancel\"].includes(commandWord)) cancelMobileTransfer(key);\n\t\t\tif (commandWord === \"cancel\") {\n\t\t\t\tinvalidateGatewayDialogs(key);\n\t\t\t\tawait adapter.send(event.source.chatId, \"Cancelled the pending selection or transfer.\", {\n\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t});\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst consumed = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\t\thandleMobileCommand({ key, event, adapter, bridge, cfg: freshCfg() }, commandWord, args),\n\t\t\t);\n\t\t\tif (consumed) return true;\n\t\t\tif (!event.text.startsWith(\"/\")) return false;\n\t\t}\n\t\tif (commandWord === \"start\") {\n\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, freshCfg()) : undefined;\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst session = await bridge.getSession(key, true);\n\t\t\t\tconst models = await session?.modelRuntime.getAvailable();\n\t\t\t\tif (!models?.length)\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"No authenticated model available. Use /login on your computer, then try /start again.\",\n\t\t\t\t\t);\n\t\t\t\telse if (\n\t\t\t\t\t!session?.model ||\n\t\t\t\t\t!models.some((model) => model.id === session.model?.id && model.provider === session.model.provider)\n\t\t\t\t)\n\t\t\t\t\tawait sendCommandReply(adapter, event, \"No available model selected. Use /model here to choose one.\");\n\t\t\t\telse\n\t\t\t\t\tawait sendCommandReply(\n\t\t\t\t\t\tadapter,\n\t\t\t\t\t\tevent,\n\t\t\t\t\t\t\"Ready. Send a message to begin, or use /model to choose a model.\",\n\t\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\tawait sendCommandReply(\n\t\t\t\t\tadapter,\n\t\t\t\t\tevent,\n\t\t\t\t\t\"Session setup failed. Run lunr gateway doctor on your computer, then try /start again.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (deps.remoteControls && commandWord === \"model\") {\n\t\t\tconst issue = workspaceReadiness(event, key, freshCfg());\n\t\t\tif (issue) {\n\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\tcfg: freshCfg(),\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runWithApprovalContext({ key, adapter, source: event.source }, () => runChatCommand(cmd, ctx));\n\t}\n\n\t/** A successful preview is finalized by edit; failed edits fall back to a durable full reply. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tif (opts.editMessageId) {\n\t\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\t\tconst edit = await adapter\n\t\t\t\t.editMessage(event.source.chatId, opts.editMessageId, opts.previewTruncated ? chunks[0] : filtered)\n\t\t\t\t.catch(() => ({ success: false }));\n\t\t\tif (edit.success) {\n\t\t\t\tif (!opts.previewTruncated) return;\n\t\t\t\tawait queueGatewayText(key, event.source, adapter, chunks.slice(1).join(\"\"), {\n\t\t\t\t\tkind: \"result\",\n\t\t\t\t\tcfg,\n\t\t\t\t\tchunks: chunks.slice(1),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tawait queueGatewayText(key, event.source, adapter, filtered, {\n\t\t\tkind: \"result\",\n\t\t\treplyTo: opts.reply ? event.messageId : undefined,\n\t\t\tcfg,\n\t\t});\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst epoch = gatewayEpoch(key);\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tconst result = await adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t\tif (!result.success) throw new Error(result.error ?? \"Preview edit failed\");\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, key, cfg, { reply: false }).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] follow-up delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, key, cfg, message, epoch).catch((error) =>\n\t\t\t\t\tconsole.error(\"[gateway] error delivery failed:\", error),\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, key, cfg, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\tconst epoch = gatewayEpoch(key);\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\trememberGatewayRole(key, event.source);\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tif (deps.remoteControls) {\n\t\t\t\t\tconst binding = conversationBinding(key);\n\t\t\t\t\tif (binding?.owner) {\n\t\t\t\t\t\trequireAuthorized(event.source, cfg);\n\t\t\t\t\t\tif (binding.owner !== event.source.userId)\n\t\t\t\t\t\t\tthrow new Error(\"This session belongs to a different user.\");\n\t\t\t\t\t}\n\t\t\t\t\tbindConversation(key, event.source);\n\t\t\t\t\tif (acceptGatewayInput(key, event)) return;\n\t\t\t\t}\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tconst issue = deps.remoteControls ? workspaceReadiness(event, key, cfg) : undefined;\n\t\t\t\tif (issue) {\n\t\t\t\t\tawait sendCommandReply(adapter, event, issue);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, key, cfg, err instanceof Error ? err.message : String(err), epoch).catch(\n\t\t\t\t\t() => {},\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/gateway/setup.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAM9D,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBvG;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACxC,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC,EAC3C,OAAO,CAAC,EAAE,CAAC,GACT,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAgCxB;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CA2JlD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAW1E","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { Container, SelectList, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport { readSecret } from \"../cli/read-secret.ts\";\nimport { createStartupTui, showStartupInput, startStartupTui } from \"../cli/startup-ui.ts\";\nimport { loadInstallFeatures, saveInstallFeatures } from \"../core/install-features.ts\";\nimport { SettingsManager } from \"../core/settings-manager.ts\";\nimport { getSelectListTheme, theme } from \"../modes/interactive/theme/theme.ts\";\nimport { loadGatewayConfig, saveGatewayConfig } from \"./config.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { configureStartup, loadServiceSettings, type StartupMode, startGatewayService } from \"./service.ts\";\n\nexport async function validateBotToken(platform: \"telegram\" | \"discord\", token: string): Promise<string> {\n\tconst url =\n\t\tplatform === \"telegram\" ? `https://api.telegram.org/bot${token}/getMe` : \"https://discord.com/api/v10/users/@me\";\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\theaders: platform === \"discord\" ? { Authorization: `Bot ${token}` } : {},\n\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t});\n\t} catch {\n\t\tthrow new Error(`${platform}: could not reach the bot API. Check your connection and try again.`);\n\t}\n\tif (!response.ok)\n\t\tthrow new Error(\n\t\t\t`${platform}: token check failed with HTTP ${response.status}. Check the token and bot permissions.`,\n\t\t);\n\tconst value: unknown = await response.json();\n\tif (!value || typeof value !== \"object\") throw new Error(`${platform}: unexpected bot API response.`);\n\tconst user = platform === \"telegram\" && \"result\" in value ? value.result : value;\n\tif (!user || typeof user !== \"object\" || !(\"id\" in user)) throw new Error(`${platform}: bot identity was missing.`);\n\treturn \"username\" in user && typeof user.username === \"string\" ? user.username : String(user.id);\n}\n\nexport async function selectSetupOption<T>(\n\tsettings: SettingsManager,\n\ttitle: string,\n\toptions: Array<{ label: string; value: T }>,\n\tinitial?: T,\n): Promise<T | undefined> {\n\tconst ui = await createStartupTui(settings);\n\ttry {\n\t\treturn await new Promise<T | undefined>((resolve) => {\n\t\t\tconst list = new SelectList(\n\t\t\t\toptions.map((option, index) => ({ label: option.label, value: String(index) })),\n\t\t\t\t8,\n\t\t\t\tgetSelectListTheme(),\n\t\t\t);\n\t\t\tlist.setSelectedIndex(\n\t\t\t\tMath.max(\n\t\t\t\t\t0,\n\t\t\t\t\toptions.findIndex((option) => option.value === initial),\n\t\t\t\t),\n\t\t\t);\n\t\t\tlist.onSelect = (item) => resolve(options[Number(item.value)].value);\n\t\t\tlist.onCancel = () => resolve(undefined);\n\t\t\tconst panel = new Container();\n\t\t\tpanel.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\t\tpanel.addChild(new Spacer(1));\n\t\t\tpanel.addChild(list);\n\t\t\tpanel.addChild(new Text(\"Up/Down to navigate, Enter to select, Esc to cancel\", 1, 1));\n\t\t\tui.addChild(panel);\n\t\t\tui.setFocus(list);\n\t\t\tstartStartupTui(ui, settings);\n\t\t});\n\t} finally {\n\t\tui.clear();\n\t\tui.requestRender();\n\t\tawait new Promise((resolve) => setTimeout(resolve, 25));\n\t\tui.stop();\n\t}\n}\n\nexport async function setupGateway(): Promise<void> {\n\tif (!process.stdin.isTTY || !process.stdout.isTTY)\n\t\tthrow new Error(\n\t\t\t\"Run lunr gateway setup in a terminal. Bot tokens are entered privately, not as command arguments.\",\n\t\t);\n\tconst uiSettings = SettingsManager.inMemory({ theme: \"moon\" });\n\tconst cancelled = new Error(\"Setup cancelled.\");\n\tlet saved = false;\n\tconst choose = async <T>(title: string, options: Array<{ label: string; value: T }>, initial?: T) => {\n\t\tconst value = await selectSetupOption(uiSettings, title, options, initial);\n\t\tif (value === undefined) throw cancelled;\n\t\treturn value;\n\t};\n\tconst ask = async (title: string) => {\n\t\tconst value = await showStartupInput(uiSettings, title);\n\t\tif (value === undefined) throw cancelled;\n\t\treturn value.trim();\n\t};\n\tconst cfg = loadGatewayConfig();\n\ttry {\n\t\tconsole.log(\"Set up lunR on your phone\\nYour computer must stay awake and connected to the internet.\\n\");\n\t\tconst choice = await choose(\n\t\t\t\"Connect a chat platform\",\n\t\t\t[\n\t\t\t\t{ label: \"Telegram\", value: \"telegram\" },\n\t\t\t\t{ label: \"Discord\", value: \"discord\" },\n\t\t\t\t{ label: \"Both\", value: \"both\" },\n\t\t\t],\n\t\t\tcfg.discord.enabled ? (cfg.telegram.enabled ? \"both\" : \"discord\") : \"telegram\",\n\t\t);\n\t\tfor (const platform of [\"telegram\", \"discord\"] as const) {\n\t\t\tif (choice !== platform && choice !== \"both\") continue;\n\t\t\tconsole.log(\n\t\t\t\tplatform === \"telegram\"\n\t\t\t\t\t? \"\\nTelegram\\n1. Open https://t.me/BotFather and send /newbot.\\n2. Follow its prompts and copy the bot token.\\n3. Open your new bot and send /whoami. If your ID was not configured, it sends a pairing code for local approval.\\nGuide: https://core.telegram.org/bots/tutorial\"\n\t\t\t\t\t: \"\\nDiscord\\n1. Open https://discord.com/developers/applications and create an application.\\n2. Open Bot, create or reset its token, and enable Message Content Intent for ordinary chat.\\n3. Under Installation, add bot and applications.commands scopes. Grant View Channels, Send Messages, Read Message History, Attach Files, and thread permissions if needed.\\n4. Use the install link to add your bot. In Discord, enable Developer Mode and copy your user ID.\\nGuide: https://docs.discord.com/developers/quick-start/getting-started\",\n\t\t\t);\n\t\t\tlet token = cfg[platform].token;\n\t\t\tif (\n\t\t\t\t!token ||\n\t\t\t\t(await choose(`${platform} bot token`, [\n\t\t\t\t\t{ label: \"Keep the saved token\", value: false },\n\t\t\t\t\t{ label: \"Enter a different token\", value: true },\n\t\t\t\t]))\n\t\t\t) {\n\t\t\t\ttoken = (await readSecret(`${platform} bot token`))?.trim();\n\t\t\t}\n\t\t\tif (!token) throw new Error(\"No token entered. Existing configuration was not changed.\");\n\t\t\tconsole.log(`Connected bot: ${await validateBotToken(platform, token)}`);\n\t\t\tcfg[platform].token = token;\n\t\t\tcfg[platform].enabled = true;\n\t\t\tcfg.owners ??= { telegram: [], discord: [] };\n\t\t\tconst ownerChoice = await choose(`${platform} owner access`, [\n\t\t\t\t...(cfg.owners[platform].length ? [{ label: \"Keep the saved owners\", value: \"keep\" }] : []),\n\t\t\t\t{ label: \"Enter my user ID\", value: \"enter\" },\n\t\t\t\t{ label: \"Approve a pairing code later on this computer\", value: \"later\" },\n\t\t\t]);\n\t\t\tconst owner = ownerChoice === \"enter\" ? await ask(`Your ${platform} user ID`) : \"\";\n\t\t\tif (ownerChoice === \"enter\" && !/^\\d+$/.test(owner)) throw new Error(\"User IDs contain digits only.\");\n\t\t\tif (owner) {\n\t\t\t\tcfg.owners[platform] = [...new Set([...cfg.owners[platform], owner])];\n\t\t\t\tcfg[platform].allowedUsers = [...new Set([...cfg[platform].allowedUsers, owner])];\n\t\t\t}\n\t\t}\n\t\tconst folders = [...new Set([cfg.defaultProject, ...(cfg.projectRoots ?? []), process.cwd()])].filter(\n\t\t\t(folder): folder is string => !!folder,\n\t\t);\n\t\tconst folder = await choose<string | null>(\"Default project\", [\n\t\t\t...folders.map((value) => ({ label: value, value })),\n\t\t\t{ label: \"Enter another folder path\", value: null },\n\t\t]);\n\t\tconst root = folder ?? (await ask(\"Project folder path\"));\n\t\tif (!root) throw new Error(\"Enter a project folder. Nothing saved.\");\n\t\tconst path = resolve(root);\n\t\tif (!existsSync(path) || !statSync(path).isDirectory()) throw new Error(\"That project folder does not exist.\");\n\t\tcfg.defaultProject = realpathSync(path);\n\t\tcfg.projectRoots = [...new Set([...(cfg.projectRoots ?? []), cfg.defaultProject])];\n\t\tconst startup = await choose<StartupMode>(\n\t\t\t\"Automatic startup\",\n\t\t\t[\n\t\t\t\t{ label: \"Off. Start manually\", value: \"off\" },\n\t\t\t\t{ label: \"When I sign in\", value: \"login\" },\n\t\t\t\t{ label: \"When the computer boots\", value: \"boot\" },\n\t\t\t],\n\t\t\tloadServiceSettings().startup,\n\t\t);\n\t\tif (startup === \"boot\")\n\t\t\tconsole.log(\n\t\t\t\t\"Boot startup may require administrator permission or OS-managed account credentials. lunR does not save your account password.\",\n\t\t\t);\n\t\tconst { ModelRuntime } = await import(\"../core/model-runtime.ts\");\n\t\tconst runtime = await ModelRuntime.create({ allowModelNetwork: false });\n\t\tconst models = await runtime.getAvailable();\n\t\tif (!models.length)\n\t\t\tthrow new Error(\n\t\t\t\t\"No saved model credentials found. Open lunr locally and use /login, then rerun gateway setup. Nothing saved.\",\n\t\t\t);\n\t\tconst settings = SettingsManager.create(cfg.defaultProject);\n\t\tconst providers = [...new Set(models.map((model) => model.provider))];\n\t\tconst provider = await choose(\n\t\t\t\"Model provider\",\n\t\t\tproviders.map((value) => ({ label: value, value })),\n\t\t\tsettings.getDefaultProvider(),\n\t\t);\n\t\tconst choices = models.filter((model) => model.provider === provider);\n\t\tconst model = await choose(\n\t\t\t\"Model\",\n\t\t\tchoices.map((value) => ({ label: value.id, value })),\n\t\t\tchoices.find((model) => model.id === settings.getDefaultModel()),\n\t\t);\n\t\tif (\n\t\t\t!(await choose(\n\t\t\t\t`Save gateway settings?\\nProject: ${cfg.defaultProject}\\nModel: ${model.provider}/${model.id}\\nStartup: ${startup}`,\n\t\t\t\t[\n\t\t\t\t\t{ label: \"Save\", value: true },\n\t\t\t\t\t{ label: \"Cancel\", value: false },\n\t\t\t\t],\n\t\t\t))\n\t\t) {\n\t\t\tconsole.log(\"Cancelled. Nothing saved.\");\n\t\t\treturn;\n\t\t}\n\t\tsaveGatewayConfig(cfg);\n\t\tsaved = true;\n\t\tsettings.setDefaultModelAndProvider(model.provider, model.id);\n\t\tawait settings.flush();\n\t\tconst features = loadInstallFeatures();\n\t\tfeatures.features[\"chat-platforms\"] = {\n\t\t\tenabled: true,\n\t\t\toptions: {\n\t\t\t\t...features.features[\"chat-platforms\"]?.options,\n\t\t\t\tautostart: loadServiceSettings().startup !== \"off\",\n\t\t\t},\n\t\t};\n\t\tsaveInstallFeatures(features);\n\t\ttry {\n\t\t\tawait configureStartup(startup);\n\t\t} finally {\n\t\t\tfeatures.features[\"chat-platforms\"].options.autostart = loadServiceSettings().startup !== \"off\";\n\t\t\tsaveInstallFeatures(features);\n\t\t}\n\t\tif (\n\t\t\tawait choose(\"Start the gateway now?\", [\n\t\t\t\t{ label: \"Start now\", value: true },\n\t\t\t\t{ label: \"Not now\", value: false },\n\t\t\t])\n\t\t)\n\t\t\tconsole.log(await startGatewayService());\n\t\tconsole.log(\n\t\t\t\"\\nIf you entered your user ID, send /start to your bot, then send a task. Otherwise send /whoami, then on this computer run:\\nlunr gateway pair approve <telegram|discord> <code> --owner\\nOrdinary pairing without --owner does not grant project or TUI access.\",\n\t\t);\n\t} catch (error) {\n\t\tif (error !== cancelled) throw error;\n\t\tconsole.log(saved ? \"Settings saved. Start the gateway with lunr gateway start.\" : \"Cancelled. Nothing saved.\");\n\t}\n}\n\nexport function approveGatewayOwner(platform: string, code: string): string {\n\tif (platform !== \"telegram\" && platform !== \"discord\") throw new Error(\"Choose telegram or discord.\");\n\tconst pairing = createPairingStore();\n\tconst user = pairing.approve(platform, code);\n\tif (!user) throw new Error(\"Pairing code is invalid or expired.\");\n\tconst cfg = loadGatewayConfig();\n\tcfg.owners ??= { telegram: [], discord: [] };\n\tcfg.owners[platform] = [...new Set([...cfg.owners[platform], user])];\n\tcfg[platform].allowedUsers = [...new Set([...cfg[platform].allowedUsers, user])];\n\tsaveGatewayConfig(cfg);\n\treturn `Approved ${platform} owner ${user}. Owner access includes local projects and TUI session history.`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/gateway/setup.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAM9D,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBvG;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACxC,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC,EAC3C,OAAO,CAAC,EAAE,CAAC,GACT,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAgCxB;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CA6JlD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAW1E","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { Container, SelectList, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport { readSecret } from \"../cli/read-secret.ts\";\nimport { createStartupTui, showStartupInput, startStartupTui } from \"../cli/startup-ui.ts\";\nimport { loadInstallFeatures, saveInstallFeatures } from \"../core/install-features.ts\";\nimport { SettingsManager } from \"../core/settings-manager.ts\";\nimport { getSelectListTheme, theme } from \"../modes/interactive/theme/theme.ts\";\nimport { loadGatewayConfig, saveGatewayConfig } from \"./config.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { configureStartup, loadServiceSettings, type StartupMode, startGatewayService } from \"./service.ts\";\n\nexport async function validateBotToken(platform: \"telegram\" | \"discord\", token: string): Promise<string> {\n\tconst url =\n\t\tplatform === \"telegram\" ? `https://api.telegram.org/bot${token}/getMe` : \"https://discord.com/api/v10/users/@me\";\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\theaders: platform === \"discord\" ? { Authorization: `Bot ${token}` } : {},\n\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t});\n\t} catch {\n\t\tthrow new Error(`${platform}: could not reach the bot API. Check your connection and try again.`);\n\t}\n\tif (!response.ok)\n\t\tthrow new Error(\n\t\t\t`${platform}: token check failed with HTTP ${response.status}. Check the token and bot permissions.`,\n\t\t);\n\tconst value: unknown = await response.json();\n\tif (!value || typeof value !== \"object\") throw new Error(`${platform}: unexpected bot API response.`);\n\tconst user = platform === \"telegram\" && \"result\" in value ? value.result : value;\n\tif (!user || typeof user !== \"object\" || !(\"id\" in user)) throw new Error(`${platform}: bot identity was missing.`);\n\treturn \"username\" in user && typeof user.username === \"string\" ? user.username : String(user.id);\n}\n\nexport async function selectSetupOption<T>(\n\tsettings: SettingsManager,\n\ttitle: string,\n\toptions: Array<{ label: string; value: T }>,\n\tinitial?: T,\n): Promise<T | undefined> {\n\tconst ui = await createStartupTui(settings);\n\ttry {\n\t\treturn await new Promise<T | undefined>((resolve) => {\n\t\t\tconst list = new SelectList(\n\t\t\t\toptions.map((option, index) => ({ label: option.label, value: String(index) })),\n\t\t\t\t8,\n\t\t\t\tgetSelectListTheme(),\n\t\t\t);\n\t\t\tlist.setSelectedIndex(\n\t\t\t\tMath.max(\n\t\t\t\t\t0,\n\t\t\t\t\toptions.findIndex((option) => option.value === initial),\n\t\t\t\t),\n\t\t\t);\n\t\t\tlist.onSelect = (item) => resolve(options[Number(item.value)].value);\n\t\t\tlist.onCancel = () => resolve(undefined);\n\t\t\tconst panel = new Container();\n\t\t\tpanel.addChild(new Text(theme.fg(\"accent\", title), 1, 0));\n\t\t\tpanel.addChild(new Spacer(1));\n\t\t\tpanel.addChild(list);\n\t\t\tpanel.addChild(new Text(\"Up/Down to navigate, Enter to select, Esc to cancel\", 1, 1));\n\t\t\tui.addChild(panel);\n\t\t\tui.setFocus(list);\n\t\t\tstartStartupTui(ui, settings);\n\t\t});\n\t} finally {\n\t\tui.clear();\n\t\tui.requestRender();\n\t\tawait new Promise((resolve) => setTimeout(resolve, 25));\n\t\tui.stop();\n\t}\n}\n\nexport async function setupGateway(): Promise<void> {\n\tif (!process.stdin.isTTY || !process.stdout.isTTY)\n\t\tthrow new Error(\n\t\t\t\"Run lunr gateway setup in a terminal. Bot tokens are entered privately, not as command arguments.\",\n\t\t);\n\tconst uiSettings = SettingsManager.inMemory({ theme: \"moon\" });\n\tconst cancelled = new Error(\"Setup cancelled.\");\n\tlet saved = false;\n\tconst choose = async <T>(title: string, options: Array<{ label: string; value: T }>, initial?: T) => {\n\t\tconst value = await selectSetupOption(uiSettings, title, options, initial);\n\t\tif (value === undefined) throw cancelled;\n\t\treturn value;\n\t};\n\tconst ask = async (title: string) => {\n\t\tconst value = await showStartupInput(uiSettings, title);\n\t\tif (value === undefined) throw cancelled;\n\t\treturn value.trim();\n\t};\n\tconst cfg = loadGatewayConfig();\n\ttry {\n\t\tconsole.log(\"Set up lunR on your phone\\nYour computer must stay awake and connected to the internet.\\n\");\n\t\tconst choice = await choose(\n\t\t\t\"Connect a chat platform\",\n\t\t\t[\n\t\t\t\t{ label: \"Telegram\", value: \"telegram\" },\n\t\t\t\t{ label: \"Discord\", value: \"discord\" },\n\t\t\t\t{ label: \"Both\", value: \"both\" },\n\t\t\t],\n\t\t\tcfg.discord.enabled ? (cfg.telegram.enabled ? \"both\" : \"discord\") : \"telegram\",\n\t\t);\n\t\tfor (const platform of [\"telegram\", \"discord\"] as const) {\n\t\t\tif (choice !== platform && choice !== \"both\") continue;\n\t\t\tconsole.log(\n\t\t\t\tplatform === \"telegram\"\n\t\t\t\t\t? \"\\nTelegram\\n1. Open https://t.me/BotFather and send /newbot.\\n2. Follow its prompts and copy the bot token.\\n3. Open your new bot and send /whoami. If your ID was not configured, it sends a pairing code for local approval.\\nGuide: https://core.telegram.org/bots/tutorial\"\n\t\t\t\t\t: \"\\nDiscord\\n1. Open https://discord.com/developers/applications and create an application.\\n2. Open Bot, create or reset its token, and enable Message Content Intent for ordinary chat.\\n3. Under Installation, add bot and applications.commands scopes. Grant View Channels, Send Messages, Read Message History, Attach Files, and thread permissions if needed.\\n4. Use the install link to add your bot. In Discord, enable Developer Mode and copy your user ID.\\nGuide: https://docs.discord.com/developers/quick-start/getting-started\",\n\t\t\t);\n\t\t\tlet token = cfg[platform].token;\n\t\t\tif (\n\t\t\t\t!token ||\n\t\t\t\t(await choose(`${platform} bot token`, [\n\t\t\t\t\t{ label: \"Keep the saved token\", value: false },\n\t\t\t\t\t{ label: \"Enter a different token\", value: true },\n\t\t\t\t]))\n\t\t\t) {\n\t\t\t\ttoken = (await readSecret(`${platform} bot token`))?.trim();\n\t\t\t}\n\t\t\tif (!token) throw new Error(\"No token entered. Existing configuration was not changed.\");\n\t\t\tconsole.log(`Connected bot: ${await validateBotToken(platform, token)}`);\n\t\t\tcfg[platform].token = token;\n\t\t\tcfg[platform].enabled = true;\n\t\t\tcfg.owners ??= { telegram: [], discord: [] };\n\t\t\tconst ownerChoice = await choose(`${platform} gateway access`, [\n\t\t\t\t...(cfg.owners[platform].length || cfg[platform].allowedUsers.length\n\t\t\t\t\t? [{ label: \"Keep the approved users\", value: \"keep\" }]\n\t\t\t\t\t: []),\n\t\t\t\t{ label: \"Enter my user ID\", value: \"enter\" },\n\t\t\t\t{ label: \"Approve a pairing code later on this computer\", value: \"later\" },\n\t\t\t]);\n\t\t\tconst owner = ownerChoice === \"enter\" ? await ask(`Your ${platform} user ID`) : \"\";\n\t\t\tif (ownerChoice === \"enter\" && !/^\\d+$/.test(owner)) throw new Error(\"User IDs contain digits only.\");\n\t\t\tif (owner) {\n\t\t\t\tcfg.owners[platform] = [...new Set([...cfg.owners[platform], owner])];\n\t\t\t\tcfg[platform].allowedUsers = [...new Set([...cfg[platform].allowedUsers, owner])];\n\t\t\t}\n\t\t}\n\t\tconst folders = [...new Set([cfg.defaultProject, ...(cfg.projectRoots ?? []), process.cwd()])].filter(\n\t\t\t(folder): folder is string => !!folder,\n\t\t);\n\t\tconst folder = await choose<string | null>(\"Default project\", [\n\t\t\t...folders.map((value) => ({ label: value, value })),\n\t\t\t{ label: \"Enter another folder path\", value: null },\n\t\t]);\n\t\tconst root = folder ?? (await ask(\"Project folder path\"));\n\t\tif (!root) throw new Error(\"Enter a project folder. Nothing saved.\");\n\t\tconst path = resolve(root);\n\t\tif (!existsSync(path) || !statSync(path).isDirectory()) throw new Error(\"That project folder does not exist.\");\n\t\tcfg.defaultProject = realpathSync(path);\n\t\tcfg.projectRoots = [...new Set([...(cfg.projectRoots ?? []), cfg.defaultProject])];\n\t\tconst startup = await choose<StartupMode>(\n\t\t\t\"Automatic startup\",\n\t\t\t[\n\t\t\t\t{ label: \"Off. Start manually\", value: \"off\" },\n\t\t\t\t{ label: \"When I sign in\", value: \"login\" },\n\t\t\t\t{ label: \"When the computer boots\", value: \"boot\" },\n\t\t\t],\n\t\t\tloadServiceSettings().startup,\n\t\t);\n\t\tif (startup === \"boot\")\n\t\t\tconsole.log(\n\t\t\t\t\"Boot startup may require administrator permission or OS-managed account credentials. lunR does not save your account password.\",\n\t\t\t);\n\t\tconst { ModelRuntime } = await import(\"../core/model-runtime.ts\");\n\t\tconst runtime = await ModelRuntime.create({ allowModelNetwork: false });\n\t\tconst models = await runtime.getAvailable();\n\t\tif (!models.length)\n\t\t\tthrow new Error(\n\t\t\t\t\"No saved model credentials found. Open lunr locally and use /login, then rerun gateway setup. Nothing saved.\",\n\t\t\t);\n\t\tconst settings = SettingsManager.create(cfg.defaultProject);\n\t\tconst providers = [...new Set(models.map((model) => model.provider))];\n\t\tconst provider = await choose(\n\t\t\t\"Model provider\",\n\t\t\tproviders.map((value) => ({ label: value, value })),\n\t\t\tsettings.getDefaultProvider(),\n\t\t);\n\t\tconst choices = models.filter((model) => model.provider === provider);\n\t\tconst model = await choose(\n\t\t\t\"Model\",\n\t\t\tchoices.map((value) => ({ label: value.id, value })),\n\t\t\tchoices.find((model) => model.id === settings.getDefaultModel()),\n\t\t);\n\t\tif (\n\t\t\t!(await choose(\n\t\t\t\t`Save gateway settings?\\nProject: ${cfg.defaultProject}\\nModel: ${model.provider}/${model.id}\\nStartup: ${startup}`,\n\t\t\t\t[\n\t\t\t\t\t{ label: \"Save\", value: true },\n\t\t\t\t\t{ label: \"Cancel\", value: false },\n\t\t\t\t],\n\t\t\t))\n\t\t) {\n\t\t\tconsole.log(\"Cancelled. Nothing saved.\");\n\t\t\treturn;\n\t\t}\n\t\tsaveGatewayConfig(cfg);\n\t\tsaved = true;\n\t\tsettings.setDefaultModelAndProvider(model.provider, model.id);\n\t\tawait settings.flush();\n\t\tconst features = loadInstallFeatures();\n\t\tfeatures.features[\"chat-platforms\"] = {\n\t\t\tenabled: true,\n\t\t\toptions: {\n\t\t\t\t...features.features[\"chat-platforms\"]?.options,\n\t\t\t\tautostart: loadServiceSettings().startup !== \"off\",\n\t\t\t},\n\t\t};\n\t\tsaveInstallFeatures(features);\n\t\ttry {\n\t\t\tawait configureStartup(startup);\n\t\t} finally {\n\t\t\tfeatures.features[\"chat-platforms\"].options.autostart = loadServiceSettings().startup !== \"off\";\n\t\t\tsaveInstallFeatures(features);\n\t\t}\n\t\tif (\n\t\t\tawait choose(\"Start the gateway now?\", [\n\t\t\t\t{ label: \"Start now\", value: true },\n\t\t\t\t{ label: \"Not now\", value: false },\n\t\t\t])\n\t\t)\n\t\t\tconsole.log(await startGatewayService());\n\t\tconsole.log(\n\t\t\t\"\\nIf you entered your user ID, send /start to your bot, then send a task. Otherwise send /whoami, then on this computer run:\\nlunr gateway pair approve <telegram|discord> <code>\\nApproval grants full gateway access, including local projects and saved sessions. Only approve people you trust with this computer.\",\n\t\t);\n\t} catch (error) {\n\t\tif (error !== cancelled) throw error;\n\t\tconsole.log(saved ? \"Settings saved. Start the gateway with lunr gateway start.\" : \"Cancelled. Nothing saved.\");\n\t}\n}\n\nexport function approveGatewayOwner(platform: string, code: string): string {\n\tif (platform !== \"telegram\" && platform !== \"discord\") throw new Error(\"Choose telegram or discord.\");\n\tconst pairing = createPairingStore();\n\tconst user = pairing.approve(platform, code);\n\tif (!user) throw new Error(\"Pairing code is invalid or expired.\");\n\tconst cfg = loadGatewayConfig();\n\tcfg.owners ??= { telegram: [], discord: [] };\n\tcfg.owners[platform] = [...new Set([...cfg.owners[platform], user])];\n\tcfg[platform].allowedUsers = [...new Set([...cfg[platform].allowedUsers, user])];\n\tsaveGatewayConfig(cfg);\n\treturn `Approved ${platform} user ${user}. Gateway access includes local projects and saved sessions.`;\n}\n"]}
|