@ashx-j/lunr 0.2.23 → 0.2.25
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 +22 -1
- package/README.md +10 -10
- package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
- package/dist/builtin-extensions/ashxj-tui.js +2 -2
- package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
- package/dist/builtin-extensions/lunr-plan-tools.d.ts +7 -7
- package/dist/builtin-extensions/lunr-plan-tools.d.ts.map +1 -1
- package/dist/builtin-extensions/lunr-plan-tools.js +12 -12
- package/dist/builtin-extensions/lunr-plan-tools.js.map +1 -1
- package/dist/builtin-extensions/pi-intercom/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-intercom/index.js +2 -0
- package/dist/builtin-extensions/pi-intercom/index.js.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.js +1 -0
- package/dist/builtin-extensions/pi-mcp-adapter/direct-tools.js.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/index.js +1 -1
- package/dist/builtin-extensions/pi-mcp-adapter/index.js.map +1 -1
- 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 +6 -4
- package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.d.ts +2 -0
- package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.js +6 -3
- package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.js +7 -2
- package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.d.ts +2 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js +4 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js.map +1 -1
- 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 +1 -1
- 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 +1 -0
- 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 +5 -1
- 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 +3 -2
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +19 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.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 +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/child-tools.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.d.ts +2 -0
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.js +17 -15
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.js +3 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js +2 -2
- package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +1 -0
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +5 -4
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/browser/schema.d.ts +1 -1
- package/dist/core/browser/schema.d.ts.map +1 -1
- package/dist/core/browser/schema.js +1 -1
- package/dist/core/browser/schema.js.map +1 -1
- package/dist/core/catalog-cache-scope.d.ts.map +1 -1
- package/dist/core/catalog-cache-scope.js +3 -1
- package/dist/core/catalog-cache-scope.js.map +1 -1
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +3 -2
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/permissions.d.ts +16 -23
- package/dist/core/permissions.d.ts.map +1 -1
- package/dist/core/permissions.js +66 -141
- package/dist/core/permissions.js.map +1 -1
- package/dist/core/plan-mode.d.ts +10 -12
- package/dist/core/plan-mode.d.ts.map +1 -1
- package/dist/core/plan-mode.js +16 -18
- package/dist/core/plan-mode.js.map +1 -1
- package/dist/core/runtime-bridges.d.ts.map +1 -1
- package/dist/core/runtime-bridges.js +2 -0
- package/dist/core/runtime-bridges.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +13 -6
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +9 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +9 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +40 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -4
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagent-permission-inherit.d.ts +6 -10
- package/dist/core/subagent-permission-inherit.d.ts.map +1 -1
- package/dist/core/subagent-permission-inherit.js +14 -14
- package/dist/core/subagent-permission-inherit.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +9 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/gateway/adapters/discord.d.ts.map +1 -1
- package/dist/gateway/adapters/discord.js +3 -8
- package/dist/gateway/adapters/discord.js.map +1 -1
- package/dist/gateway/adapters/telegram.d.ts.map +1 -1
- package/dist/gateway/adapters/telegram.js +7 -0
- package/dist/gateway/adapters/telegram.js.map +1 -1
- package/dist/gateway/agent-bridge.d.ts +4 -1
- package/dist/gateway/agent-bridge.d.ts.map +1 -1
- package/dist/gateway/agent-bridge.js +57 -17
- package/dist/gateway/agent-bridge.js.map +1 -1
- package/dist/gateway/approval.d.ts +2 -6
- package/dist/gateway/approval.d.ts.map +1 -1
- package/dist/gateway/approval.js +3 -7
- package/dist/gateway/approval.js.map +1 -1
- package/dist/gateway/buttons.d.ts.map +1 -1
- package/dist/gateway/buttons.js +29 -19
- package/dist/gateway/buttons.js.map +1 -1
- package/dist/gateway/commands.d.ts +2 -0
- package/dist/gateway/commands.d.ts.map +1 -1
- package/dist/gateway/commands.js +39 -18
- package/dist/gateway/commands.js.map +1 -1
- package/dist/gateway/config.d.ts +1 -0
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +2 -0
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/cron.d.ts +1 -1
- package/dist/gateway/cron.d.ts.map +1 -1
- package/dist/gateway/cron.js +2 -2
- package/dist/gateway/cron.js.map +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +26 -26
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/mobile-commands.d.ts.map +1 -1
- package/dist/gateway/mobile-commands.js +15 -14
- package/dist/gateway/mobile-commands.js.map +1 -1
- package/dist/gateway/presenter.d.ts +12 -1
- package/dist/gateway/presenter.d.ts.map +1 -1
- package/dist/gateway/presenter.js +146 -48
- package/dist/gateway/presenter.js.map +1 -1
- package/dist/gateway/router.d.ts +5 -5
- package/dist/gateway/router.d.ts.map +1 -1
- package/dist/gateway/router.js +96 -42
- package/dist/gateway/router.js.map +1 -1
- package/dist/gateway/service.d.ts.map +1 -1
- package/dist/gateway/service.js +13 -4
- package/dist/gateway/service.js.map +1 -1
- package/dist/gateway/setup.d.ts +5 -0
- package/dist/gateway/setup.d.ts.map +1 -1
- package/dist/gateway/setup.js +98 -42
- package/dist/gateway/setup.js.map +1 -1
- package/dist/gateway/stream.d.ts +6 -5
- package/dist/gateway/stream.d.ts.map +1 -1
- package/dist/gateway/stream.js +24 -11
- package/dist/gateway/stream.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -2
- package/dist/main.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 +23 -3
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-tail.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-tail.js +2 -2
- package/dist/modes/interactive/components/thinking-tail.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +76 -90
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/node-runtime/{agent-session-services-GZVSOGYR.js → agent-session-services-YXXFCP5J.js} +12 -12
- package/dist/node-runtime/{builtin-extensions-QVCXNQT4.js → builtin-extensions-ZLAX7TUH.js} +3 -3
- package/dist/node-runtime/{chunk-W64JC4E7.js → chunk-2HHLJWGD.js} +3 -87
- package/dist/node-runtime/{chunk-VWCE2HSZ.js → chunk-2HMIRK7W.js} +4 -4
- package/dist/node-runtime/{chunk-XLTL257W.js → chunk-2KIPT4HJ.js} +5 -1
- package/dist/node-runtime/{chunk-RIR5EDDJ.js → chunk-3XPLBUEM.js} +17 -12
- package/dist/node-runtime/{chunk-QAZOULTU.js → chunk-6XL67VVY.js} +40 -6
- package/dist/node-runtime/chunk-7S3T6YEA.js +97 -0
- package/dist/node-runtime/{chunk-R3K2F5GI.js → chunk-B4HP6ZWM.js} +2 -0
- package/dist/node-runtime/{chunk-TQGPP4SC.js → chunk-CDVHZOOK.js} +1 -1
- package/dist/node-runtime/{chunk-YJMMG7C6.js → chunk-DSIKTUPW.js} +1 -1
- package/dist/node-runtime/{chunk-LFZDVKSP.js → chunk-E4D3IGP2.js} +41 -25
- package/dist/node-runtime/{chunk-OFKSVRV7.js → chunk-GYIXXQAO.js} +1 -1
- package/dist/node-runtime/{chunk-BX2RB477.js → chunk-HFVZUPFJ.js} +1 -1
- package/dist/node-runtime/{chunk-AZJHNQKQ.js → chunk-I54LGHWM.js} +1 -1
- package/dist/node-runtime/{chunk-YHQ3NBB3.js → chunk-IJIFHRTB.js} +20 -18
- package/dist/node-runtime/{chunk-GS66LV4M.js → chunk-KJ6DWS3K.js} +4 -4
- package/dist/node-runtime/{chunk-HPUP7E4T.js → chunk-KWIYUUZK.js} +68 -42
- package/dist/node-runtime/{chunk-TVJUOJZJ.js → chunk-LQP3RB67.js} +184 -1527
- package/dist/node-runtime/{chunk-VSELYKWU.js → chunk-LWYIVUGV.js} +2 -1
- package/dist/node-runtime/{chunk-JHYKXIMM.js → chunk-MPKSOJBT.js} +1 -1
- package/dist/node-runtime/{chunk-GMHJCCEN.js → chunk-NRKGMJYU.js} +4 -1
- package/dist/node-runtime/{chunk-CSUZVNYE.js → chunk-OVX6X377.js} +10 -6
- package/dist/node-runtime/{chunk-DD5YWFSJ.js → chunk-QTU6AKNJ.js} +130 -65
- package/dist/node-runtime/{chunk-LLUIPGDR.js → chunk-RC7DMV45.js} +110 -77
- package/dist/node-runtime/{chunk-UWPJL22I.js → chunk-SLVOAQOY.js} +15 -4
- package/dist/node-runtime/{chunk-EKVFP6YF.js → chunk-TORR5XBV.js} +17 -16
- package/dist/node-runtime/{chunk-UP2XBVH5.js → chunk-UOEDI7RP.js} +1 -1
- package/dist/node-runtime/{chunk-DQMGU675.js → chunk-VQFSUJJG.js} +20 -17
- package/dist/node-runtime/{chunk-X3URCYCU.js → chunk-WF7PGWRZ.js} +14 -14
- package/dist/node-runtime/chunk-YCUZEGJG.js +1361 -0
- package/dist/node-runtime/{chunk-5D7LC2ZQ.js → chunk-ZXSGTGJ6.js} +54 -85
- package/dist/node-runtime/cli-runtime.js +1 -1
- package/dist/node-runtime/{command-PDVTBRWA.js → command-6WDOYKS6.js} +5 -5
- package/dist/node-runtime/{commands-KAMO2T6P.js → commands-NZLSOHPE.js} +2 -2
- package/dist/node-runtime/{commands-XDCFGIGW.js → commands-QC6VUZJQ.js} +9 -9
- package/dist/node-runtime/{config-RORQUQFG.js → config-D4QUPAHX.js} +1 -1
- package/dist/node-runtime/{direct-tool-executor-YAVOLDFY.js → direct-tool-executor-77GW4F5P.js} +3 -3
- package/dist/node-runtime/{export-html-KSOIMVPI.js → export-html-VM4J2MNR.js} +1 -1
- package/dist/node-runtime/{gateway-AYWM5DIO.js → gateway-INAYBF4Q.js} +141 -85
- package/dist/node-runtime/index.js +23 -21
- package/dist/node-runtime/{init-X2LIUB2H.js → init-HS6NQXSK.js} +2 -2
- package/dist/node-runtime/{install-cli-3BO4MMQ6.js → install-cli-KL4UDDAD.js} +3 -3
- package/dist/node-runtime/{interactive-mode-4MFRKAHG.js → interactive-mode-Z5M7WYYZ.js} +14 -14
- package/dist/node-runtime/{interactive-view-TUZHCQM3.js → interactive-view-NGFAMHPW.js} +2 -2
- package/dist/node-runtime/{lunr-browser-RYFIIVJN.js → lunr-browser-IR7TL3J7.js} +4 -4
- package/dist/node-runtime/{lunr-cron-YWXEPZMQ.js → lunr-cron-LWIZJM2N.js} +8 -8
- package/dist/node-runtime/main.js +19 -18
- package/dist/node-runtime/{mobile-commands-LVHDAUDP.js → mobile-commands-J7OJEKFO.js} +6 -6
- package/dist/node-runtime/{model-runtime-PLQKAG4B.js → model-runtime-OO5HFLWE.js} +1 -1
- package/dist/node-runtime/{package-manager-cli-6GT3Z3CK.js → package-manager-cli-VYZM5WHN.js} +10 -8
- package/dist/node-runtime/{pi-intercom-HBPK5HSJ.js → pi-intercom-QM53RLTS.js} +2 -0
- package/dist/node-runtime/{pi-mcp-adapter-E6WULBKF.js → pi-mcp-adapter-5UKBZP44.js} +7 -6
- package/dist/node-runtime/{pi-subagents-CDWK2AKY.js → pi-subagents-DNY7EAIG.js} +23 -13
- package/dist/node-runtime/{proxy-modes-OSQCWRBI.js → proxy-modes-LGEUYD7R.js} +3 -3
- package/dist/node-runtime/rpc-entry.js +19 -18
- package/dist/node-runtime/{runtime-bridges-3XUQ46XW.js → runtime-bridges-VRDDR73P.js} +8 -1
- package/dist/node-runtime/{service-ISSP6VHE.js → service-MDQIPQ26.js} +1 -1
- package/dist/node-runtime/{session-handoff-HC7R5E24.js → session-handoff-GWARVEWT.js} +2 -2
- package/dist/node-runtime/{session-manager-CFN4UFM2.js → session-manager-JB5J6XBN.js} +1 -1
- package/dist/node-runtime/{settings-manager-6VEMXRDL.js → settings-manager-J4SPQPDJ.js} +10 -7
- package/dist/node-runtime/setup-CQ2NLVK5.js +214 -0
- package/dist/node-runtime/{subagent-executor-M5VWB3D3.js → subagent-executor-KZZQGAKV.js} +40 -7
- package/dist/node-runtime/{update-cli-UVKJWRRJ.js → update-cli-GTFJC7XK.js} +4 -5
- package/dist/startup/interactive-view.d.ts.map +1 -1
- package/dist/startup/interactive-view.js +5 -1
- package/dist/startup/interactive-view.js.map +1 -1
- package/docs/browser.md +1 -1
- package/docs/features.md +19 -18
- package/docs/index.md +1 -1
- package/docs/keybindings.md +1 -1
- package/docs/providers.md +6 -2
- package/docs/quickstart.md +2 -2
- package/docs/settings.md +1 -1
- package/docs/usage.md +4 -4
- 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/plan-mode/README.md +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/node-runtime/chunk-LISUTZS3.js +0 -19
- package/dist/node-runtime/setup-5SR2J35A.js +0 -155
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../src/gateway/approval.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway approval prompt for manual permission mode.\n *\n * The gateway runs headless sessions. When a mutating tool call arrives in\n * manual mode, this module posts an inline-button approve/reject prompt to\n * the originating chat and awaits the callback. If no chat context is present\n * the request is rejected (fail-closed).\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n\ttype ApprovalRequest,\n\ttype ApprovalResponse,\n\tNO_HANDLER_REASON,\n\tregisterApprovalHandler,\n\tsetPermissionMode,\n} from \"../core/permissions.ts\";\nimport { isAuthorized, isGatewayOwner } from \"./authz.ts\";\nimport { loadGatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { getSession } from \"./store.ts\";\nimport type { ButtonSpec, CallbackEvent, PlatformAdapter, SessionSource } from \"./types.ts\";\n\nconst CALLBACK_PREFIX = \"ap:\";\nconst ID_LENGTH = 8;\nconst CODE_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\";\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\ninterface ApprovalContext {\n\tkey: string;\n\tadapter: PlatformAdapter;\n\tsource: SessionSource;\n\ttimeoutMs?: number;\n}\n\ninterface PendingApproval {\n\tkey?: string;\n\tsource: SessionSource;\n\tplan: boolean;\n\tresolve: (value: ApprovalResponse) => void;\n\treject: (reason?: unknown) => void;\n\ttimeout: ReturnType<typeof setTimeout>;\n\tmessageId?: string;\n\tadapter: PlatformAdapter;\n\tchatId: string;\n\tthreadId?: string;\n\tuserId: string;\n}\n\nconst approvalContext = new AsyncLocalStorage<ApprovalContext>();\nconst pending = new Map<string, PendingApproval>();\n\nfunction generateId(): string {\n\tlet id = \"\";\n\tconst bytes = randomBytes(ID_LENGTH);\n\tfor (let i = 0; i < ID_LENGTH; i++) {\n\t\tid += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];\n\t}\n\treturn id;\n}\n\nfunction makeCallbackData(id: string, action: ApprovalResponse | \"cancel\"): string {\n\treturn `${CALLBACK_PREFIX}${id}:${action}`;\n}\n\nfunction parseCallbackData(data: string): { id: string; action: ApprovalResponse | \"cancel\" } | null {\n\tif (!data.startsWith(CALLBACK_PREFIX)) return null;\n\tconst rest = data.slice(CALLBACK_PREFIX.length);\n\tconst colon = rest.indexOf(\":\");\n\tif (colon === -1) return null;\n\tconst id = rest.slice(0, colon);\n\tconst action = rest.slice(colon + 1);\n\tif (action === \"once\" || action === \"session\" || action === \"reject\" || action === \"cancel\") {\n\t\treturn { id, action };\n\t}\n\treturn null;\n}\n\n/** Run code with the gateway approval context available to tool-call gates. */\nexport function runWithApprovalContext<T>(ctx: ApprovalContext, fn: () => Promise<T>): Promise<T> {\n\treturn approvalContext.run(ctx, fn);\n}\n\nasync function createApprovalPrompt(\n\tadapter: PlatformAdapter,\n\tsource: SessionSource,\n\treq: ApprovalRequest,\n\ttimeoutMs = DEFAULT_TIMEOUT_MS,\n\tkey?: string,\n): Promise<ApprovalResponse> {\n\tconst id = generateId();\n\t// Large child launches get a dedicated title; inline buttons cannot capture feedback.\n\tconst title =\n\t\treq.kind === \"large-subagent-launch\"\n\t\t\t? `Approve large subagent launch?\\n${req.detail}`\n\t\t\t: `Approve ${req.toolName}?\\n${req.detail}`;\n\tconst rows: ButtonSpec[][] = [\n\t\t[\n\t\t\t{ label: \"✓ Approve once\", data: makeCallbackData(id, \"once\") },\n\t\t\t{ label: \"✓ Approve session\", data: makeCallbackData(id, \"session\") },\n\t\t],\n\t\t[{ label: \"✗ Reject\", data: makeCallbackData(id, \"reject\") }],\n\t];\n\n\treturn new Promise<ApprovalResponse>((resolve, reject) => {\n\t\tlet messageId: string | undefined;\n\t\tconst timeout = setTimeout(() => {\n\t\t\tpending.delete(id);\n\t\t\treject(new Error(\"Approval request timed out\"));\n\t\t\tif (messageId) {\n\t\t\t\tvoid adapter.editMessage(source.chatId, messageId, \"⏱ Approval expired.\", []).catch(() => {});\n\t\t\t}\n\t\t}, timeoutMs);\n\n\t\tconst entry: PendingApproval = {\n\t\t\tkey,\n\t\t\tsource,\n\t\t\tplan: req.kind === \"plan\",\n\t\t\tresolve,\n\t\t\treject,\n\t\t\ttimeout,\n\t\t\tadapter,\n\t\t\tchatId: source.chatId,\n\t\t\tthreadId: source.threadId,\n\t\t\tuserId: source.userId,\n\t\t};\n\t\tpending.set(id, entry);\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst result = await adapter.sendButtons(source.chatId, title, rows, { threadId: source.threadId });\n\t\t\t\tif (!result.success || !result.messageId) throw new Error(result.error ?? \"Failed to send approval prompt\");\n\t\t\t\tmessageId = result.messageId;\n\t\t\t\tif (pending.get(id) !== entry) {\n\t\t\t\t\tawait adapter\n\t\t\t\t\t\t.editMessage(source.chatId, messageId, \"Approval cancelled or expired.\", [])\n\t\t\t\t\t\t.catch(() => {});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tentry.messageId = messageId;\n\t\t\t} catch (err) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tpending.delete(id);\n\t\t\t\treject(err);\n\t\t\t}\n\t\t})();\n\t});\n}\n\n/** Approval handler passed to core permissions. */\nexport async function createGatewayApprovalRequest(req: ApprovalRequest): Promise<ApprovalResponse> {\n\tconst ctx = approvalContext.getStore();\n\tif (!ctx) {\n\t\tthrow new Error(NO_HANDLER_REASON);\n\t}\n\treturn createApprovalPrompt(ctx.adapter, ctx.source, req, ctx.timeoutMs, ctx.key);\n}\n\n/** Register the gateway approval handler with the core permission gate. */\nexport function registerGatewayApprovalHandler(): void {\n\tregisterApprovalHandler(createGatewayApprovalRequest);\n}\n\n/**\n * Dispatch a callback event to an outstanding approval prompt.\n * Returns true when the data matched the approval prefix (whether or not a\n * pending prompt existed).\n */\nexport async function handleApprovalCallback(event: CallbackEvent, adapter?: PlatformAdapter): Promise<boolean> {\n\tconst parsed = parseCallbackData(event.data);\n\tif (!parsed) return false;\n\n\tconst entry = pending.get(parsed.id);\n\tif (!entry) {\n\t\t// Best-effort answer for stale callbacks.\n\t\tawait adapter?.answerCallback(event.id, \"Approval expired.\").catch(() => {});\n\t\treturn true;\n\t}\n\n\tif (\n\t\tevent.userId !== entry.userId ||\n\t\tevent.chatId !== entry.chatId ||\n\t\tevent.threadId !== entry.threadId ||\n\t\t(adapter && adapter.platform !== entry.adapter.platform)\n\t) {\n\t\tawait entry.adapter.answerCallback(event.id, \"⛔ Not your approval.\").catch(() => {});\n\t\treturn true;\n\t}\n\n\tclearTimeout(entry.timeout);\n\tpending.delete(parsed.id);\n\n\tconst response: ApprovalResponse =\n\t\tparsed.action === \"reject\" || parsed.action === \"cancel\" ? \"reject\" : parsed.action;\n\tconst binding = entry.key ? conversationBinding(entry.key) : undefined;\n\tif (binding?.owner && (!isGatewayOwner(entry.source, loadGatewayConfig()) || binding.owner !== event.userId)) {\n\t\tentry.resolve(\"reject\");\n\t\tawait entry.adapter.answerCallback(event.id, \"Owner access was removed.\").catch(() => {});\n\t\treturn true;\n\t}\n\tif (binding && !isAuthorized(entry.source, loadGatewayConfig(), createPairingStore())) {\n\t\tentry.resolve(\"reject\");\n\t\treturn true;\n\t}\n\tif (entry.plan && response !== \"reject\" && entry.key) {\n\t\tconst sessionId = getSession(entry.key)?.sessionId;\n\t\tif (sessionId) setPermissionMode(\"manual\", sessionId);\n\t}\n\tentry.resolve(response);\n\n\tconst label =\n\t\tparsed.action === \"reject\" || parsed.action === \"cancel\" ? \"Rejected.\" : `Approved (${parsed.action}).`;\n\tawait entry.adapter.editMessage(entry.chatId, entry.messageId ?? \"\", label, []).catch(() => {});\n\tawait entry.adapter.answerCallback(event.id).catch(() => {});\n\treturn true;\n}\n\n/** Test hook: clear all pending approvals and stop their timeouts. */\nexport function cancelGatewayApprovals(key: string): void {\n\tfor (const [id, entry] of pending) {\n\t\tif (entry.key !== key) continue;\n\t\tclearTimeout(entry.timeout);\n\t\tpending.delete(id);\n\t\tentry.resolve(\"reject\");\n\t\tif (entry.messageId)\n\t\t\tvoid entry.adapter.editMessage(entry.chatId, entry.messageId, \"Approval cancelled.\", []).catch(() => {});\n\t}\n}\n\nexport function resetApprovalRegistry(): void {\n\tfor (const entry of pending.values()) {\n\t\tclearTimeout(entry.timeout);\n\t\ttry {\n\t\t\tentry.reject(new Error(\"Approval registry reset\"));\n\t\t} catch {\n\t\t\t// ignore settled promise\n\t\t}\n\t}\n\tpending.clear();\n}\n\n/** Test hook: number of outstanding approval prompts. */\nexport function activeApprovalCount(): number {\n\treturn pending.size;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../src/gateway/approval.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Gateway approvals for large subagent launches and plans.\n * A missing chat context rejects large launches; plans pass through.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n\ttype ApprovalRequest,\n\ttype ApprovalResponse,\n\tNO_HANDLER_REASON,\n\tregisterApprovalHandler,\n\tsetPermissionMode,\n} from \"../core/permissions.ts\";\nimport { isAuthorized, isGatewayOwner } from \"./authz.ts\";\nimport { loadGatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { createPairingStore } from \"./pairing.ts\";\nimport { getSession } from \"./store.ts\";\nimport type { ButtonSpec, CallbackEvent, PlatformAdapter, SessionSource } from \"./types.ts\";\n\nconst CALLBACK_PREFIX = \"ap:\";\nconst ID_LENGTH = 8;\nconst CODE_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\";\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\n\ninterface ApprovalContext {\n\tkey: string;\n\tadapter: PlatformAdapter;\n\tsource: SessionSource;\n\ttimeoutMs?: number;\n}\n\ninterface PendingApproval {\n\tkey?: string;\n\tsource: SessionSource;\n\tplan: boolean;\n\tresolve: (value: ApprovalResponse) => void;\n\treject: (reason?: unknown) => void;\n\ttimeout: ReturnType<typeof setTimeout>;\n\tmessageId?: string;\n\tadapter: PlatformAdapter;\n\tchatId: string;\n\tthreadId?: string;\n\tuserId: string;\n}\n\nconst approvalContext = new AsyncLocalStorage<ApprovalContext>();\nconst pending = new Map<string, PendingApproval>();\n\nfunction generateId(): string {\n\tlet id = \"\";\n\tconst bytes = randomBytes(ID_LENGTH);\n\tfor (let i = 0; i < ID_LENGTH; i++) {\n\t\tid += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];\n\t}\n\treturn id;\n}\n\nfunction makeCallbackData(id: string, action: ApprovalResponse | \"cancel\"): string {\n\treturn `${CALLBACK_PREFIX}${id}:${action}`;\n}\n\nfunction parseCallbackData(data: string): { id: string; action: ApprovalResponse | \"cancel\" } | null {\n\tif (!data.startsWith(CALLBACK_PREFIX)) return null;\n\tconst rest = data.slice(CALLBACK_PREFIX.length);\n\tconst colon = rest.indexOf(\":\");\n\tif (colon === -1) return null;\n\tconst id = rest.slice(0, colon);\n\tconst action = rest.slice(colon + 1);\n\tif (action === \"once\" || action === \"session\" || action === \"reject\" || action === \"cancel\") {\n\t\treturn { id, action };\n\t}\n\treturn null;\n}\n\n/** Run code with the gateway approval context available to tool-call gates. */\nexport function runWithApprovalContext<T>(ctx: ApprovalContext, fn: () => Promise<T>): Promise<T> {\n\treturn approvalContext.run(ctx, fn);\n}\n\nasync function createApprovalPrompt(\n\tadapter: PlatformAdapter,\n\tsource: SessionSource,\n\treq: ApprovalRequest,\n\ttimeoutMs = DEFAULT_TIMEOUT_MS,\n\tkey?: string,\n): Promise<ApprovalResponse> {\n\tconst id = generateId();\n\t// Large child launches get a dedicated title; inline buttons cannot capture feedback.\n\tconst title =\n\t\treq.kind === \"large-subagent-launch\"\n\t\t\t? `Approve large subagent launch?\\n${req.detail}`\n\t\t\t: `Approve ${req.toolName}?\\n${req.detail}`;\n\tconst rows: ButtonSpec[][] = [\n\t\t[\n\t\t\t{ label: \"✓ Approve once\", data: makeCallbackData(id, \"once\") },\n\t\t\t{ label: \"✓ Approve session\", data: makeCallbackData(id, \"session\") },\n\t\t],\n\t\t[{ label: \"✗ Reject\", data: makeCallbackData(id, \"reject\") }],\n\t];\n\n\treturn new Promise<ApprovalResponse>((resolve, reject) => {\n\t\tlet messageId: string | undefined;\n\t\tconst timeout = setTimeout(() => {\n\t\t\tpending.delete(id);\n\t\t\treject(new Error(\"Approval request timed out\"));\n\t\t\tif (messageId) {\n\t\t\t\tvoid adapter.editMessage(source.chatId, messageId, \"⏱ Approval expired.\", []).catch(() => {});\n\t\t\t}\n\t\t}, timeoutMs);\n\n\t\tconst entry: PendingApproval = {\n\t\t\tkey,\n\t\t\tsource,\n\t\t\tplan: req.kind === \"plan\",\n\t\t\tresolve,\n\t\t\treject,\n\t\t\ttimeout,\n\t\t\tadapter,\n\t\t\tchatId: source.chatId,\n\t\t\tthreadId: source.threadId,\n\t\t\tuserId: source.userId,\n\t\t};\n\t\tpending.set(id, entry);\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst result = await adapter.sendButtons(source.chatId, title, rows, { threadId: source.threadId });\n\t\t\t\tif (!result.success || !result.messageId) throw new Error(result.error ?? \"Failed to send approval prompt\");\n\t\t\t\tmessageId = result.messageId;\n\t\t\t\tif (pending.get(id) !== entry) {\n\t\t\t\t\tawait adapter\n\t\t\t\t\t\t.editMessage(source.chatId, messageId, \"Approval cancelled or expired.\", [])\n\t\t\t\t\t\t.catch(() => {});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tentry.messageId = messageId;\n\t\t\t} catch (err) {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\tpending.delete(id);\n\t\t\t\treject(err);\n\t\t\t}\n\t\t})();\n\t});\n}\n\n/** Approval handler passed to core permissions. */\nexport async function createGatewayApprovalRequest(req: ApprovalRequest): Promise<ApprovalResponse> {\n\tconst ctx = approvalContext.getStore();\n\tif (!ctx) {\n\t\tthrow new Error(NO_HANDLER_REASON);\n\t}\n\treturn createApprovalPrompt(ctx.adapter, ctx.source, req, ctx.timeoutMs, ctx.key);\n}\n\n/** Register the gateway approval handler with the core permission gate. */\nexport function registerGatewayApprovalHandler(): void {\n\tregisterApprovalHandler(createGatewayApprovalRequest);\n}\n\n/**\n * Dispatch a callback event to an outstanding approval prompt.\n * Returns true when the data matched the approval prefix (whether or not a\n * pending prompt existed).\n */\nexport async function handleApprovalCallback(event: CallbackEvent, adapter?: PlatformAdapter): Promise<boolean> {\n\tconst parsed = parseCallbackData(event.data);\n\tif (!parsed) return false;\n\n\tconst entry = pending.get(parsed.id);\n\tif (!entry) {\n\t\t// Best-effort answer for stale callbacks.\n\t\tawait adapter?.answerCallback(event.id, \"Approval expired.\").catch(() => {});\n\t\treturn true;\n\t}\n\n\tif (\n\t\tevent.userId !== entry.userId ||\n\t\tevent.chatId !== entry.chatId ||\n\t\tevent.threadId !== entry.threadId ||\n\t\t(adapter && adapter.platform !== entry.adapter.platform)\n\t) {\n\t\tawait entry.adapter.answerCallback(event.id, \"⛔ Not your approval.\").catch(() => {});\n\t\treturn true;\n\t}\n\n\tclearTimeout(entry.timeout);\n\tpending.delete(parsed.id);\n\n\tconst response: ApprovalResponse =\n\t\tparsed.action === \"reject\" || parsed.action === \"cancel\" ? \"reject\" : parsed.action;\n\tconst binding = entry.key ? conversationBinding(entry.key) : undefined;\n\tif (binding?.owner && (!isGatewayOwner(entry.source, loadGatewayConfig()) || binding.owner !== event.userId)) {\n\t\tentry.resolve(\"reject\");\n\t\tawait entry.adapter.answerCallback(event.id, \"Owner access was removed.\").catch(() => {});\n\t\treturn true;\n\t}\n\tif (binding && !isAuthorized(entry.source, loadGatewayConfig(), createPairingStore())) {\n\t\tentry.resolve(\"reject\");\n\t\treturn true;\n\t}\n\tif (entry.plan && response !== \"reject\" && entry.key) {\n\t\tconst sessionId = getSession(entry.key)?.sessionId;\n\t\tif (sessionId) setPermissionMode(\"yolo\", sessionId);\n\t}\n\tentry.resolve(response);\n\n\tconst label =\n\t\tparsed.action === \"reject\" || parsed.action === \"cancel\" ? \"Rejected.\" : `Approved (${parsed.action}).`;\n\tawait entry.adapter.editMessage(entry.chatId, entry.messageId ?? \"\", label, []).catch(() => {});\n\tawait entry.adapter.answerCallback(event.id).catch(() => {});\n\treturn true;\n}\n\n/** Test hook: clear all pending approvals and stop their timeouts. */\nexport function cancelGatewayApprovals(key: string): void {\n\tfor (const [id, entry] of pending) {\n\t\tif (entry.key !== key) continue;\n\t\tclearTimeout(entry.timeout);\n\t\tpending.delete(id);\n\t\tentry.resolve(\"reject\");\n\t\tif (entry.messageId)\n\t\t\tvoid entry.adapter.editMessage(entry.chatId, entry.messageId, \"Approval cancelled.\", []).catch(() => {});\n\t}\n}\n\nexport function resetApprovalRegistry(): void {\n\tfor (const entry of pending.values()) {\n\t\tclearTimeout(entry.timeout);\n\t\ttry {\n\t\t\tentry.reject(new Error(\"Approval registry reset\"));\n\t\t} catch {\n\t\t\t// ignore settled promise\n\t\t}\n\t}\n\tpending.clear();\n}\n\n/** Test hook: number of outstanding approval prompts. */\nexport function activeApprovalCount(): number {\n\treturn pending.size;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buttons.d.ts","sourceRoot":"","sources":["../../src/gateway/buttons.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAc,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUrH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,mBAAmB,GAC5B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/D,QAAQ,CAAC,IAAI,OAAO,CAAC;IACrB,QAAQ,CAAC,IAAI,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACxD;AAcD,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;CACnB;AA0FD;;;GAGG;AACH,wBAAsB,YAAY,CACjC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,UAAU,EAChB,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,UAAU,CAAC,CAqBrB;AAcD,6DAA6D;AAC7D,wBAAsB,cAAc,CACnC,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,UAAU,GAAG;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAgHf;AAED,kDAAkD;AAClD,wBAAgB,kBAAkB,IAAI,IAAI,CAgBzC;AAED,4BAA4B;AAC5B,wBAAgB,iBAAiB,IAAI,IAAI,CAKxC;AAED,0DAA0D;AAC1D,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C","sourcesContent":["/**\n * lunR: gateway inline-button picker registry.\n *\n * Single registry for all interactive prompts (model/thinking/sessions pickers\n * now; approvals later). Callback payloads stay tiny so they fit Telegram's\n * 64-byte callback_data limit; all picker state lives server-side in this module.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { isAuthorized } from \"./authz.ts\";\nimport type { GatewayConfig } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { ButtonSpec, CallbackEvent, PlatformAdapter, SendOptions, SendResult, SessionSource } from \"./types.ts\";\n\nconst PICKER_TTL_MS = 15 * 60 * 1000;\nconst SWEEP_INTERVAL_MS = 60 * 1000;\nconst DEFAULT_PER_PAGE = 8;\nconst BUTTONS_PER_ROW = 2;\nconst CODE_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\";\nconst CODE_LENGTH = 8;\nconst CALLBACK_PREFIX = \"cp:\";\n\nexport interface PickerItem {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport type PickerResolveResult =\n\t| { done: true; text: string }\n\t| { done: false; items: PickerItem[]; title: string; breadcrumbs?: string };\n\nexport interface PickerSpec {\n\tkind: \"model\" | \"thinking\" | \"sessions\" | \"project\" | \"dialog\";\n\tvalidate?(): boolean;\n\tonCancel?(): void;\n\tsessionKey: string;\n\tinvokerId: string;\n\titems: PickerItem[];\n\tperPage?: number;\n\ttitle: string;\n\tbreadcrumbs?: string;\n\tresolve(item: PickerItem): Promise<PickerResolveResult>;\n}\n\ninterface PendingPicker extends PickerSpec {\n\tresolving?: boolean;\n\tid: string;\n\tchatId: string;\n\tmessageId: string;\n\tsource: SessionSource;\n\tadapter: PlatformAdapter;\n\tpage: number;\n\tperPage: number;\n\tcreatedAt: number;\n}\n\nexport interface ButtonDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n}\n\nconst registry = new Map<string, PendingPicker>();\nlet sweeper: ReturnType<typeof setInterval> | undefined;\n\nfunction generatePickerId(): string {\n\tlet id = \"\";\n\tconst bytes = randomBytes(CODE_LENGTH);\n\tfor (let i = 0; i < CODE_LENGTH; i++) {\n\t\tid += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];\n\t}\n\treturn id;\n}\n\nfunction makeCallbackData(id: string, action: \"select\" | \"page\" | \"cancel\" | \"noop\", payload?: string): string {\n\tif (action === \"noop\") return `${CALLBACK_PREFIX}${id}:`;\n\tif (action === \"cancel\") return `${CALLBACK_PREFIX}${id}:x`;\n\tif (action === \"page\") return `${CALLBACK_PREFIX}${id}:p${payload ?? \"0\"}`;\n\treturn `${CALLBACK_PREFIX}${id}:${payload ?? \"\"}`;\n}\n\nfunction parseCallbackData(\n\tdata: string,\n): { id: string; action: \"select\" | \"page\" | \"cancel\" | \"noop\"; payload?: string } | null {\n\tif (!data.startsWith(CALLBACK_PREFIX)) return null;\n\tconst rest = data.slice(CALLBACK_PREFIX.length);\n\tconst colon = rest.indexOf(\":\");\n\tif (colon === -1) return null;\n\tconst id = rest.slice(0, colon);\n\tconst tail = rest.slice(colon + 1);\n\tif (tail === \"\") return { id, action: \"noop\" };\n\tif (tail === \"x\") return { id, action: \"cancel\" };\n\tif (tail.startsWith(\"p\")) return { id, action: \"page\", payload: tail.slice(1) };\n\treturn { id, action: \"select\", payload: tail };\n}\n\nfunction isSameChat(a: SessionSource, b: SessionSource): boolean {\n\treturn a.platform === b.platform && a.chatId === b.chatId && a.threadId === b.threadId;\n}\n\nfunction sourceForAuth(picker: PendingPicker, cb: CallbackEvent): SessionSource {\n\treturn {\n\t\tplatform: picker.source.platform,\n\t\tchatId: cb.chatId,\n\t\tchatType: picker.source.chatType,\n\t\tuserId: cb.userId,\n\t\tuserName: cb.userName,\n\t\tthreadId: cb.threadId,\n\t\t// Do NOT inherit the original invoker's role authorization.\n\t\troleAuthorized: undefined,\n\t};\n}\n\nfunction renderPage(picker: PendingPicker): { text: string; buttons: ButtonSpec[][] } {\n\tconst { id, title, breadcrumbs, items, page, perPage } = picker;\n\tconst totalPages = Math.max(1, Math.ceil(items.length / perPage));\n\tconst clampedPage = Math.max(0, Math.min(page, totalPages - 1));\n\tconst start = clampedPage * perPage;\n\tconst pageItems = items.slice(start, start + perPage);\n\n\tconst rows: ButtonSpec[][] = [];\n\tfor (let i = 0; i < pageItems.length; i += BUTTONS_PER_ROW) {\n\t\tconst row: ButtonSpec[] = [];\n\t\tfor (let j = 0; j < BUTTONS_PER_ROW && i + j < pageItems.length; j++) {\n\t\t\tconst index = start + i + j;\n\t\t\trow.push({ label: pageItems[i + j].label, data: makeCallbackData(id, \"select\", String(index)) });\n\t\t}\n\t\trows.push(row);\n\t}\n\n\tif (totalPages > 1) {\n\t\tconst nav: ButtonSpec[] = [];\n\t\tif (clampedPage > 0) {\n\t\t\tnav.push({ label: \"◀ Prev\", data: makeCallbackData(id, \"page\", String(clampedPage - 1)) });\n\t\t}\n\t\tnav.push({ label: `${clampedPage + 1}/${totalPages}`, data: makeCallbackData(id, \"noop\") });\n\t\tif (clampedPage < totalPages - 1) {\n\t\t\tnav.push({ label: \"Next ▶\", data: makeCallbackData(id, \"page\", String(clampedPage + 1)) });\n\t\t}\n\t\trows.push(nav);\n\t}\n\n\trows.push([{ label: \"✗ Cancel\", data: makeCallbackData(id, \"cancel\") }]);\n\n\tconst header = breadcrumbs ? `${title} · ${breadcrumbs}` : title;\n\tconst text =\n\t\titems.length === 0 ? `${header}\\n\\nNo options available.` : `${header} (${clampedPage + 1}/${totalPages}):`;\n\treturn { text, buttons: rows };\n}\n\n/**\n * Create and send a paginated inline-button picker. On send failure the caller\n * should fall back to a text-based list.\n */\nexport async function createPicker(\n\tadapter: PlatformAdapter,\n\tsource: SessionSource,\n\tspec: PickerSpec,\n\topts?: SendOptions,\n): Promise<SendResult> {\n\tconst id = generatePickerId();\n\tconst pending: PendingPicker = {\n\t\t...spec,\n\t\tid,\n\t\tchatId: source.chatId,\n\t\tsource,\n\t\tadapter,\n\t\tmessageId: \"\",\n\t\tpage: 0,\n\t\tperPage: spec.perPage ?? DEFAULT_PER_PAGE,\n\t\tcreatedAt: Date.now(),\n\t};\n\tconst { text, buttons } = renderPage(pending);\n\tconst result = await adapter.sendButtons(source.chatId, text, buttons, opts);\n\tif (!result.success) return result;\n\tif (result.messageId) {\n\t\tpending.messageId = result.messageId;\n\t\tregistry.set(id, pending);\n\t}\n\treturn result;\n}\n\nasync function answerExpired(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"Picker expired — run the command again.\").catch(() => {});\n}\n\nasync function answerUnauthorized(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not authorized.\").catch(() => {});\n}\n\nasync function answerNotYourPicker(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not your picker.\").catch(() => {});\n}\n\n/** Dispatch a CallbackEvent to the matching picker entry. */\nexport async function handleCallback(\n\tcb: CallbackEvent,\n\tdeps: ButtonDeps & { adapter: PlatformAdapter },\n): Promise<void> {\n\tconst { adapter, cfg, pairing } = deps;\n\tconst parsed = parseCallbackData(cb.data);\n\tif (!parsed) return;\n\n\tconst picker = registry.get(parsed.id);\n\tif (!picker) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (picker.validate && !picker.validate()) {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (Date.now() > picker.createdAt + PICKER_TTL_MS) {\n\t\tpicker.onCancel?.();\n\t\tregistry.delete(parsed.id);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"⏱ Expired — run the command again.\", []);\n\t\t} catch {\n\t\t\t// best-effort expiry message\n\t\t}\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isSameChat(picker.source, sourceForAuth(picker, cb))) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (cb.userId !== picker.invokerId) {\n\t\tawait answerNotYourPicker(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isAuthorized(sourceForAuth(picker, cb), cfg, pairing)) {\n\t\tawait answerUnauthorized(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"cancel\") {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"Cancelled.\", []);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"noop\") {\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"page\") {\n\t\tconst page = Number.parseInt(parsed.payload ?? \"0\", 10);\n\t\tconst totalPages = Math.max(1, Math.ceil(picker.items.length / picker.perPage));\n\t\tpicker.page = Math.max(0, Math.min(page, totalPages - 1));\n\t\tconst { text, buttons } = renderPage(picker);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\t// select\n\tconst index = Number.parseInt(parsed.payload ?? \"\", 10);\n\tconst item = picker.items[index];\n\tif (!item) {\n\t\tawait adapter.answerCallback(cb.id, \"Invalid selection.\").catch(() => {});\n\t\treturn;\n\t}\n\n\tif (picker.resolving) {\n\t\tawait adapter.answerCallback(cb.id, \"Selection is still running.\").catch(() => {});\n\t\treturn;\n\t}\n\tpicker.resolving = true;\n\tawait adapter.answerCallback(cb.id).catch(() => {});\n\ttry {\n\t\tconst result = await picker.resolve(item);\n\t\tif (!result.done) {\n\t\t\tpicker.items = result.items;\n\t\t\tpicker.title = result.title;\n\t\t\tpicker.breadcrumbs = result.breadcrumbs;\n\t\t\tpicker.page = 0;\n\t\t\tconst { text, buttons } = renderPage(picker);\n\t\t\ttry {\n\t\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t\t} catch {}\n\t\t\treturn;\n\t\t}\n\t\tregistry.delete(parsed.id);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, result.text, []);\n\t\t} catch {}\n\t} catch (err) {\n\t\tregistry.delete(parsed.id);\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, `⚠ ${message}`, []);\n\t\t} catch {}\n\t} finally {\n\t\tpicker.resolving = false;\n\t}\n}\n\n/** Start the periodic TTL sweeper. Idempotent. */\nexport function startButtonSweeper(): void {\n\tif (sweeper) return;\n\tsweeper = setInterval(() => {\n\t\tconst cutoff = Date.now() - PICKER_TTL_MS;\n\t\tfor (const [id, entry] of registry) {\n\t\t\tif (entry.createdAt > cutoff) continue;\n\t\t\tregistry.delete(id);\n\t\t\tentry.onCancel?.();\n\t\t\ttry {\n\t\t\t\tvoid entry.adapter.editMessage(entry.chatId, entry.messageId, \"⏱ Expired — run the command again.\", []);\n\t\t\t} catch {\n\t\t\t\t// best-effort expiry edit\n\t\t\t}\n\t\t}\n\t}, SWEEP_INTERVAL_MS);\n\tsweeper.unref?.();\n}\n\n/** Stop the TTL sweeper. */\nexport function stopButtonSweeper(): void {\n\tif (sweeper) {\n\t\tclearInterval(sweeper);\n\t\tsweeper = undefined;\n\t}\n}\n\n/** Test hook: reset the registry and stop the sweeper. */\nexport function resetButtonRegistry(): void {\n\tregistry.clear();\n\tstopButtonSweeper();\n}\n\n/** Test hook: expose active picker ids. */\nexport function activePickerIds(): string[] {\n\treturn [...registry.keys()];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buttons.d.ts","sourceRoot":"","sources":["../../src/gateway/buttons.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAc,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUrH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,mBAAmB,GAC5B;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/D,QAAQ,CAAC,IAAI,OAAO,CAAC;IACrB,QAAQ,CAAC,IAAI,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACxD;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;CACnB;AA0FD;;;GAGG;AACH,wBAAsB,YAAY,CACjC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,UAAU,EAChB,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,UAAU,CAAC,CAqBrB;AAcD,6DAA6D;AAC7D,wBAAsB,cAAc,CACnC,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,UAAU,GAAG;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAiHf;AAaD,kDAAkD;AAClD,wBAAgB,kBAAkB,IAAI,IAAI,CAezC;AAED,4BAA4B;AAC5B,wBAAgB,iBAAiB,IAAI,IAAI,CAKxC;AAED,0DAA0D;AAC1D,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C","sourcesContent":["/**\n * lunR: gateway inline-button picker registry.\n *\n * Single registry for all interactive prompts (model/thinking/sessions pickers\n * now; approvals later). Callback payloads stay tiny so they fit Telegram's\n * 64-byte callback_data limit; all picker state lives server-side in this module.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { isAuthorized } from \"./authz.ts\";\nimport type { GatewayConfig } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { ButtonSpec, CallbackEvent, PlatformAdapter, SendOptions, SendResult, SessionSource } from \"./types.ts\";\n\nconst PICKER_TTL_MS = 15 * 60 * 1000;\nconst SWEEP_INTERVAL_MS = 60 * 1000;\nconst DEFAULT_PER_PAGE = 8;\nconst BUTTONS_PER_ROW = 2;\nconst CODE_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\";\nconst CODE_LENGTH = 8;\nconst CALLBACK_PREFIX = \"cp:\";\n\nexport interface PickerItem {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport type PickerResolveResult =\n\t| { done: true; text: string }\n\t| { done: false; items: PickerItem[]; title: string; breadcrumbs?: string };\n\nexport interface PickerSpec {\n\tkind: \"model\" | \"thinking\" | \"sessions\" | \"project\" | \"dialog\";\n\tvalidate?(): boolean;\n\tonCancel?(): void;\n\tsessionKey: string;\n\tinvokerId: string;\n\titems: PickerItem[];\n\tperPage?: number;\n\ttitle: string;\n\tbreadcrumbs?: string;\n\tresolve(item: PickerItem): Promise<PickerResolveResult>;\n}\n\ninterface PendingPicker extends PickerSpec {\n\tresolving?: boolean;\n\tcompleted?: string;\n\tid: string;\n\tchatId: string;\n\tmessageId: string;\n\tsource: SessionSource;\n\tadapter: PlatformAdapter;\n\tpage: number;\n\tperPage: number;\n\tcreatedAt: number;\n}\n\nexport interface ButtonDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n}\n\nconst registry = new Map<string, PendingPicker>();\nlet sweeper: ReturnType<typeof setInterval> | undefined;\n\nfunction generatePickerId(): string {\n\tlet id = \"\";\n\tconst bytes = randomBytes(CODE_LENGTH);\n\tfor (let i = 0; i < CODE_LENGTH; i++) {\n\t\tid += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];\n\t}\n\treturn id;\n}\n\nfunction makeCallbackData(id: string, action: \"select\" | \"page\" | \"cancel\" | \"noop\", payload?: string): string {\n\tif (action === \"noop\") return `${CALLBACK_PREFIX}${id}:`;\n\tif (action === \"cancel\") return `${CALLBACK_PREFIX}${id}:x`;\n\tif (action === \"page\") return `${CALLBACK_PREFIX}${id}:p${payload ?? \"0\"}`;\n\treturn `${CALLBACK_PREFIX}${id}:${payload ?? \"\"}`;\n}\n\nfunction parseCallbackData(\n\tdata: string,\n): { id: string; action: \"select\" | \"page\" | \"cancel\" | \"noop\"; payload?: string } | null {\n\tif (!data.startsWith(CALLBACK_PREFIX)) return null;\n\tconst rest = data.slice(CALLBACK_PREFIX.length);\n\tconst colon = rest.indexOf(\":\");\n\tif (colon === -1) return null;\n\tconst id = rest.slice(0, colon);\n\tconst tail = rest.slice(colon + 1);\n\tif (tail === \"\") return { id, action: \"noop\" };\n\tif (tail === \"x\") return { id, action: \"cancel\" };\n\tif (tail.startsWith(\"p\")) return { id, action: \"page\", payload: tail.slice(1) };\n\treturn { id, action: \"select\", payload: tail };\n}\n\nfunction isSameChat(a: SessionSource, b: SessionSource): boolean {\n\treturn a.platform === b.platform && a.chatId === b.chatId && a.threadId === b.threadId;\n}\n\nfunction sourceForAuth(picker: PendingPicker, cb: CallbackEvent): SessionSource {\n\treturn {\n\t\tplatform: picker.source.platform,\n\t\tchatId: cb.chatId,\n\t\tchatType: picker.source.chatType,\n\t\tuserId: cb.userId,\n\t\tuserName: cb.userName,\n\t\tthreadId: cb.threadId,\n\t\t// Do NOT inherit the original invoker's role authorization.\n\t\troleAuthorized: undefined,\n\t};\n}\n\nfunction renderPage(picker: PendingPicker): { text: string; buttons: ButtonSpec[][] } {\n\tconst { id, title, breadcrumbs, items, page, perPage } = picker;\n\tconst totalPages = Math.max(1, Math.ceil(items.length / perPage));\n\tconst clampedPage = Math.max(0, Math.min(page, totalPages - 1));\n\tconst start = clampedPage * perPage;\n\tconst pageItems = items.slice(start, start + perPage);\n\n\tconst rows: ButtonSpec[][] = [];\n\tfor (let i = 0; i < pageItems.length; i += BUTTONS_PER_ROW) {\n\t\tconst row: ButtonSpec[] = [];\n\t\tfor (let j = 0; j < BUTTONS_PER_ROW && i + j < pageItems.length; j++) {\n\t\t\tconst index = start + i + j;\n\t\t\trow.push({ label: pageItems[i + j].label, data: makeCallbackData(id, \"select\", String(index)) });\n\t\t}\n\t\trows.push(row);\n\t}\n\n\tif (totalPages > 1) {\n\t\tconst nav: ButtonSpec[] = [];\n\t\tif (clampedPage > 0) {\n\t\t\tnav.push({ label: \"◀ Prev\", data: makeCallbackData(id, \"page\", String(clampedPage - 1)) });\n\t\t}\n\t\tnav.push({ label: `${clampedPage + 1}/${totalPages}`, data: makeCallbackData(id, \"noop\") });\n\t\tif (clampedPage < totalPages - 1) {\n\t\t\tnav.push({ label: \"Next ▶\", data: makeCallbackData(id, \"page\", String(clampedPage + 1)) });\n\t\t}\n\t\trows.push(nav);\n\t}\n\n\trows.push([{ label: \"✗ Cancel\", data: makeCallbackData(id, \"cancel\") }]);\n\n\tconst header = breadcrumbs ? `${title} · ${breadcrumbs}` : title;\n\tconst text =\n\t\titems.length === 0 ? `${header}\\n\\nNo options available.` : `${header} (${clampedPage + 1}/${totalPages}):`;\n\treturn { text, buttons: rows };\n}\n\n/**\n * Create and send a paginated inline-button picker. On send failure the caller\n * should fall back to a text-based list.\n */\nexport async function createPicker(\n\tadapter: PlatformAdapter,\n\tsource: SessionSource,\n\tspec: PickerSpec,\n\topts?: SendOptions,\n): Promise<SendResult> {\n\tconst id = generatePickerId();\n\tconst pending: PendingPicker = {\n\t\t...spec,\n\t\tid,\n\t\tchatId: source.chatId,\n\t\tsource,\n\t\tadapter,\n\t\tmessageId: \"\",\n\t\tpage: 0,\n\t\tperPage: spec.perPage ?? DEFAULT_PER_PAGE,\n\t\tcreatedAt: Date.now(),\n\t};\n\tconst { text, buttons } = renderPage(pending);\n\tconst result = await adapter.sendButtons(source.chatId, text, buttons, opts);\n\tif (!result.success) return result;\n\tif (result.messageId) {\n\t\tpending.messageId = result.messageId;\n\t\tregistry.set(id, pending);\n\t}\n\treturn result;\n}\n\nasync function answerExpired(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"Picker expired — run the command again.\").catch(() => {});\n}\n\nasync function answerUnauthorized(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not authorized.\").catch(() => {});\n}\n\nasync function answerNotYourPicker(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not your picker.\").catch(() => {});\n}\n\n/** Dispatch a CallbackEvent to the matching picker entry. */\nexport async function handleCallback(\n\tcb: CallbackEvent,\n\tdeps: ButtonDeps & { adapter: PlatformAdapter },\n): Promise<void> {\n\tconst { adapter, cfg, pairing } = deps;\n\tconst parsed = parseCallbackData(cb.data);\n\tif (!parsed) return;\n\n\tconst picker = registry.get(parsed.id);\n\tif (!picker) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!picker.completed && picker.validate && !picker.validate()) {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (Date.now() > picker.createdAt + PICKER_TTL_MS) {\n\t\tif (!picker.completed) picker.onCancel?.();\n\t\tregistry.delete(parsed.id);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"⏱ Expired — run the command again.\", []);\n\t\t} catch {\n\t\t\t// best-effort expiry message\n\t\t}\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isSameChat(picker.source, sourceForAuth(picker, cb))) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (cb.userId !== picker.invokerId) {\n\t\tawait answerNotYourPicker(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isAuthorized(sourceForAuth(picker, cb), cfg, pairing)) {\n\t\tawait answerUnauthorized(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (picker.completed !== undefined) {\n\t\tawait adapter.answerCallback(cb.id, \"Already completed.\").catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"cancel\") {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"Cancelled.\", []);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"noop\") {\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"page\") {\n\t\tconst page = Number.parseInt(parsed.payload ?? \"0\", 10);\n\t\tconst totalPages = Math.max(1, Math.ceil(picker.items.length / picker.perPage));\n\t\tpicker.page = Math.max(0, Math.min(page, totalPages - 1));\n\t\tconst { text, buttons } = renderPage(picker);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\t// select\n\tconst index = Number.parseInt(parsed.payload ?? \"\", 10);\n\tconst item = picker.items[index];\n\tif (!item) {\n\t\tawait adapter.answerCallback(cb.id, \"Invalid selection.\").catch(() => {});\n\t\treturn;\n\t}\n\n\tif (picker.resolving) {\n\t\tawait adapter.answerCallback(cb.id, \"Selection is still running.\").catch(() => {});\n\t\treturn;\n\t}\n\tpicker.resolving = true;\n\tawait adapter.answerCallback(cb.id).catch(() => {});\n\ttry {\n\t\tconst result = await picker.resolve(item);\n\t\tif (!result.done) {\n\t\t\tpicker.items = result.items;\n\t\t\tpicker.title = result.title;\n\t\t\tpicker.breadcrumbs = result.breadcrumbs;\n\t\t\tpicker.page = 0;\n\t\t\tconst { text, buttons } = renderPage(picker);\n\t\t\ttry {\n\t\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t\t} catch {}\n\t\t\treturn;\n\t\t}\n\t\tpicker.completed = result.text;\n\t\tawait confirmPicker(picker, result.text);\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tpicker.completed = `⚠ ${message}`;\n\t\tawait confirmPicker(picker, picker.completed);\n\t} finally {\n\t\tpicker.resolving = false;\n\t}\n}\n\nasync function confirmPicker(picker: PendingPicker, text: string): Promise<void> {\n\tconst edited = await picker.adapter\n\t\t.editMessage(picker.chatId, picker.messageId, text, [])\n\t\t.catch(() => ({ success: false }));\n\tif (edited.success) return;\n\tconst sent = await picker.adapter\n\t\t.send(picker.chatId, text, { threadId: picker.source.threadId })\n\t\t.catch(() => ({ success: false }));\n\tif (!sent.success) console.error(`[gateway] picker confirmation failed on ${picker.adapter.platform}`);\n}\n\n/** Start the periodic TTL sweeper. Idempotent. */\nexport function startButtonSweeper(): void {\n\tif (sweeper) return;\n\tsweeper = setInterval(() => {\n\t\tconst cutoff = Date.now() - PICKER_TTL_MS;\n\t\tfor (const [id, entry] of registry) {\n\t\t\tif (entry.createdAt > cutoff) continue;\n\t\t\tregistry.delete(id);\n\t\t\tif (entry.completed) continue;\n\t\t\tentry.onCancel?.();\n\t\t\tvoid entry.adapter\n\t\t\t\t.editMessage(entry.chatId, entry.messageId, \"⏱ Expired — run the command again.\", [])\n\t\t\t\t.catch(() => {});\n\t\t}\n\t}, SWEEP_INTERVAL_MS);\n\tsweeper.unref?.();\n}\n\n/** Stop the TTL sweeper. */\nexport function stopButtonSweeper(): void {\n\tif (sweeper) {\n\t\tclearInterval(sweeper);\n\t\tsweeper = undefined;\n\t}\n}\n\n/** Test hook: reset the registry and stop the sweeper. */\nexport function resetButtonRegistry(): void {\n\tregistry.clear();\n\tstopButtonSweeper();\n}\n\n/** Test hook: expose active picker ids. */\nexport function activePickerIds(): string[] {\n\treturn [...registry].filter(([, picker]) => picker.completed === undefined).map(([id]) => id);\n}\n"]}
|
package/dist/gateway/buttons.js
CHANGED
|
@@ -143,14 +143,15 @@ export async function handleCallback(cb, deps) {
|
|
|
143
143
|
await answerExpired(adapter, cb);
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
|
-
if (picker.validate && !picker.validate()) {
|
|
146
|
+
if (!picker.completed && picker.validate && !picker.validate()) {
|
|
147
147
|
registry.delete(parsed.id);
|
|
148
148
|
picker.onCancel?.();
|
|
149
149
|
await answerExpired(adapter, cb);
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
if (Date.now() > picker.createdAt + PICKER_TTL_MS) {
|
|
153
|
-
picker.
|
|
153
|
+
if (!picker.completed)
|
|
154
|
+
picker.onCancel?.();
|
|
154
155
|
registry.delete(parsed.id);
|
|
155
156
|
try {
|
|
156
157
|
await picker.adapter.editMessage(picker.chatId, picker.messageId, "⏱ Expired — run the command again.", []);
|
|
@@ -173,6 +174,10 @@ export async function handleCallback(cb, deps) {
|
|
|
173
174
|
await answerUnauthorized(adapter, cb);
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
177
|
+
if (picker.completed !== undefined) {
|
|
178
|
+
await adapter.answerCallback(cb.id, "Already completed.").catch(() => { });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
176
181
|
if (parsed.action === "cancel") {
|
|
177
182
|
registry.delete(parsed.id);
|
|
178
183
|
picker.onCancel?.();
|
|
@@ -226,24 +231,30 @@ export async function handleCallback(cb, deps) {
|
|
|
226
231
|
catch { }
|
|
227
232
|
return;
|
|
228
233
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await picker.adapter.editMessage(picker.chatId, picker.messageId, result.text, []);
|
|
232
|
-
}
|
|
233
|
-
catch { }
|
|
234
|
+
picker.completed = result.text;
|
|
235
|
+
await confirmPicker(picker, result.text);
|
|
234
236
|
}
|
|
235
237
|
catch (err) {
|
|
236
|
-
registry.delete(parsed.id);
|
|
237
238
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
catch { }
|
|
239
|
+
picker.completed = `⚠ ${message}`;
|
|
240
|
+
await confirmPicker(picker, picker.completed);
|
|
242
241
|
}
|
|
243
242
|
finally {
|
|
244
243
|
picker.resolving = false;
|
|
245
244
|
}
|
|
246
245
|
}
|
|
246
|
+
async function confirmPicker(picker, text) {
|
|
247
|
+
const edited = await picker.adapter
|
|
248
|
+
.editMessage(picker.chatId, picker.messageId, text, [])
|
|
249
|
+
.catch(() => ({ success: false }));
|
|
250
|
+
if (edited.success)
|
|
251
|
+
return;
|
|
252
|
+
const sent = await picker.adapter
|
|
253
|
+
.send(picker.chatId, text, { threadId: picker.source.threadId })
|
|
254
|
+
.catch(() => ({ success: false }));
|
|
255
|
+
if (!sent.success)
|
|
256
|
+
console.error(`[gateway] picker confirmation failed on ${picker.adapter.platform}`);
|
|
257
|
+
}
|
|
247
258
|
/** Start the periodic TTL sweeper. Idempotent. */
|
|
248
259
|
export function startButtonSweeper() {
|
|
249
260
|
if (sweeper)
|
|
@@ -254,13 +265,12 @@ export function startButtonSweeper() {
|
|
|
254
265
|
if (entry.createdAt > cutoff)
|
|
255
266
|
continue;
|
|
256
267
|
registry.delete(id);
|
|
268
|
+
if (entry.completed)
|
|
269
|
+
continue;
|
|
257
270
|
entry.onCancel?.();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
catch {
|
|
262
|
-
// best-effort expiry edit
|
|
263
|
-
}
|
|
271
|
+
void entry.adapter
|
|
272
|
+
.editMessage(entry.chatId, entry.messageId, "⏱ Expired — run the command again.", [])
|
|
273
|
+
.catch(() => { });
|
|
264
274
|
}
|
|
265
275
|
}, SWEEP_INTERVAL_MS);
|
|
266
276
|
sweeper.unref?.();
|
|
@@ -279,6 +289,6 @@ export function resetButtonRegistry() {
|
|
|
279
289
|
}
|
|
280
290
|
/** Test hook: expose active picker ids. */
|
|
281
291
|
export function activePickerIds() {
|
|
282
|
-
return [...registry.
|
|
292
|
+
return [...registry].filter(([, picker]) => picker.completed === undefined).map(([id]) => id);
|
|
283
293
|
}
|
|
284
294
|
//# sourceMappingURL=buttons.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buttons.js","sourceRoot":"","sources":["../../src/gateway/buttons.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM1C,MAAM,aAAa,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACrC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC;AACpC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,MAAM,aAAa,GAAG,iCAAiC,CAAC;AACxD,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,eAAe,GAAG,KAAK,CAAC;AA2C9B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;AAClD,IAAI,OAAmD,CAAC;AAExD,SAAS,gBAAgB,GAAW;IACnC,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,EAAE,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,EAAE,CAAC;AAAA,CACV;AAED,SAAS,gBAAgB,CAAC,EAAU,EAAE,MAA6C,EAAE,OAAgB,EAAU;IAC9G,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,GAAG,eAAe,GAAG,EAAE,GAAG,CAAC;IACzD,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,GAAG,eAAe,GAAG,EAAE,IAAI,CAAC;IAC5D,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,GAAG,eAAe,GAAG,EAAE,KAAK,OAAO,IAAI,GAAG,EAAE,CAAC;IAC3E,OAAO,GAAG,eAAe,GAAG,EAAE,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC;AAAA,CAClD;AAED,SAAS,iBAAiB,CACzB,IAAY,EAC6E;IACzF,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACnC,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC/C,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAClD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAAA,CAC/C;AAED,SAAS,UAAU,CAAC,CAAgB,EAAE,CAAgB,EAAW;IAChE,OAAO,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC;AAAA,CACvF;AAED,SAAS,aAAa,CAAC,MAAqB,EAAE,EAAiB,EAAiB;IAC/E,OAAO;QACN,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ;QAChC,MAAM,EAAE,EAAE,CAAC,MAAM;QACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ;QAChC,MAAM,EAAE,EAAE,CAAC,MAAM;QACjB,QAAQ,EAAE,EAAE,CAAC,QAAQ;QACrB,QAAQ,EAAE,EAAE,CAAC,QAAQ;QACrB,4DAA4D;QAC5D,cAAc,EAAE,SAAS;KACzB,CAAC;AAAA,CACF;AAED,SAAS,UAAU,CAAC,MAAqB,EAA6C;IACrF,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAChE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,WAAW,GAAG,OAAO,CAAC;IACpC,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC;IAEtD,MAAM,IAAI,GAAmB,EAAE,CAAC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,eAAe,EAAE,CAAC;QAC5D,MAAM,GAAG,GAAiB,EAAE,CAAC;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtE,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAClG,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,GAAG,GAAiB,EAAE,CAAC;QAC7B,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,UAAQ,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,WAAW,GAAG,CAAC,IAAI,UAAU,EAAE,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5F,IAAI,WAAW,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC;YAClC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,UAAQ,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,YAAU,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAEzE,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,OAAM,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACjE,MAAM,IAAI,GACT,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,2BAA2B,CAAC,CAAC,CAAC,GAAG,MAAM,KAAK,WAAW,GAAG,CAAC,IAAI,UAAU,IAAI,CAAC;IAC7G,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAAA,CAC/B;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,OAAwB,EACxB,MAAqB,EACrB,IAAgB,EAChB,IAAkB,EACI;IACtB,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAkB;QAC9B,GAAG,IAAI;QACP,EAAE;QACF,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,MAAM;QACN,OAAO;QACP,SAAS,EAAE,EAAE;QACb,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,gBAAgB;QACzC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACrB,CAAC;IACF,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7E,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC;IACnC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd;AAED,KAAK,UAAU,aAAa,CAAC,OAAwB,EAAE,EAAiB,EAAiB;IACxF,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,2CAAyC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;AAAA,CAC/F;AAED,KAAK,UAAU,kBAAkB,CAAC,OAAwB,EAAE,EAAiB,EAAiB;IAC7F,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,qBAAmB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;AAAA,CACzE;AAED,KAAK,UAAU,mBAAmB,CAAC,OAAwB,EAAE,EAAiB,EAAiB;IAC9F,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,sBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;AAAA,CAC1E;AAED,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,EAAiB,EACjB,IAA+C,EAC/B;IAChB,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACvC,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjC,OAAO;IACR,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC3C,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpB,MAAM,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjC,OAAO;IACR,CAAC;IAED,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,aAAa,EAAE,CAAC;QACnD,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpB,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,wCAAoC,EAAE,EAAE,CAAC,CAAC;QAC7G,CAAC;QAAC,MAAM,CAAC;YACR,6BAA6B;QAC9B,CAAC;QACD,MAAM,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjC,OAAO;IACR,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QAC3D,MAAM,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjC,OAAO;IACR,CAAC;IAED,IAAI,EAAE,CAAC,MAAM,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;QACpC,MAAM,mBAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACvC,OAAO;IACR,CAAC;IAED,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,CAAC;QAC5D,MAAM,kBAAkB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACtC,OAAO;IACR,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAChC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC;QACrF,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QACpD,OAAO;IACR,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QACpD,OAAO;IACR,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1D,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAClF,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QACpD,OAAO;IACR,CAAC;IAED,SAAS;IACT,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QAC1E,OAAO;IACR,CAAC;IAED,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,6BAA6B,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QACnF,OAAO;IACR,CAAC;IACD,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;IACpD,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAClB,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YAC5B,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YAC5B,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;YACxC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC;YAChB,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;YAC7C,IAAI,CAAC;gBACJ,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YAClF,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,OAAO;QACR,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACpF,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACX,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,OAAK,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QACvF,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACX,CAAC;YAAS,CAAC;QACV,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC;IAC1B,CAAC;AAAA,CACD;AAED,kDAAkD;AAClD,MAAM,UAAU,kBAAkB,GAAS;IAC1C,IAAI,OAAO;QAAE,OAAO;IACpB,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,CAAC;QAC1C,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,SAAS,GAAG,MAAM;gBAAE,SAAS;YACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACpB,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC;gBACJ,KAAK,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,EAAE,wCAAoC,EAAE,EAAE,CAAC,CAAC;YACzG,CAAC;YAAC,MAAM,CAAC;gBACR,0BAA0B;YAC3B,CAAC;QACF,CAAC;IAAA,CACD,EAAE,iBAAiB,CAAC,CAAC;IACtB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;AAAA,CAClB;AAED,4BAA4B;AAC5B,MAAM,UAAU,iBAAiB,GAAS;IACzC,IAAI,OAAO,EAAE,CAAC;QACb,aAAa,CAAC,OAAO,CAAC,CAAC;QACvB,OAAO,GAAG,SAAS,CAAC;IACrB,CAAC;AAAA,CACD;AAED,0DAA0D;AAC1D,MAAM,UAAU,mBAAmB,GAAS;IAC3C,QAAQ,CAAC,KAAK,EAAE,CAAC;IACjB,iBAAiB,EAAE,CAAC;AAAA,CACpB;AAED,2CAA2C;AAC3C,MAAM,UAAU,eAAe,GAAa;IAC3C,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;AAAA,CAC5B","sourcesContent":["/**\n * lunR: gateway inline-button picker registry.\n *\n * Single registry for all interactive prompts (model/thinking/sessions pickers\n * now; approvals later). Callback payloads stay tiny so they fit Telegram's\n * 64-byte callback_data limit; all picker state lives server-side in this module.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { isAuthorized } from \"./authz.ts\";\nimport type { GatewayConfig } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { ButtonSpec, CallbackEvent, PlatformAdapter, SendOptions, SendResult, SessionSource } from \"./types.ts\";\n\nconst PICKER_TTL_MS = 15 * 60 * 1000;\nconst SWEEP_INTERVAL_MS = 60 * 1000;\nconst DEFAULT_PER_PAGE = 8;\nconst BUTTONS_PER_ROW = 2;\nconst CODE_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\";\nconst CODE_LENGTH = 8;\nconst CALLBACK_PREFIX = \"cp:\";\n\nexport interface PickerItem {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport type PickerResolveResult =\n\t| { done: true; text: string }\n\t| { done: false; items: PickerItem[]; title: string; breadcrumbs?: string };\n\nexport interface PickerSpec {\n\tkind: \"model\" | \"thinking\" | \"sessions\" | \"project\" | \"dialog\";\n\tvalidate?(): boolean;\n\tonCancel?(): void;\n\tsessionKey: string;\n\tinvokerId: string;\n\titems: PickerItem[];\n\tperPage?: number;\n\ttitle: string;\n\tbreadcrumbs?: string;\n\tresolve(item: PickerItem): Promise<PickerResolveResult>;\n}\n\ninterface PendingPicker extends PickerSpec {\n\tresolving?: boolean;\n\tid: string;\n\tchatId: string;\n\tmessageId: string;\n\tsource: SessionSource;\n\tadapter: PlatformAdapter;\n\tpage: number;\n\tperPage: number;\n\tcreatedAt: number;\n}\n\nexport interface ButtonDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n}\n\nconst registry = new Map<string, PendingPicker>();\nlet sweeper: ReturnType<typeof setInterval> | undefined;\n\nfunction generatePickerId(): string {\n\tlet id = \"\";\n\tconst bytes = randomBytes(CODE_LENGTH);\n\tfor (let i = 0; i < CODE_LENGTH; i++) {\n\t\tid += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];\n\t}\n\treturn id;\n}\n\nfunction makeCallbackData(id: string, action: \"select\" | \"page\" | \"cancel\" | \"noop\", payload?: string): string {\n\tif (action === \"noop\") return `${CALLBACK_PREFIX}${id}:`;\n\tif (action === \"cancel\") return `${CALLBACK_PREFIX}${id}:x`;\n\tif (action === \"page\") return `${CALLBACK_PREFIX}${id}:p${payload ?? \"0\"}`;\n\treturn `${CALLBACK_PREFIX}${id}:${payload ?? \"\"}`;\n}\n\nfunction parseCallbackData(\n\tdata: string,\n): { id: string; action: \"select\" | \"page\" | \"cancel\" | \"noop\"; payload?: string } | null {\n\tif (!data.startsWith(CALLBACK_PREFIX)) return null;\n\tconst rest = data.slice(CALLBACK_PREFIX.length);\n\tconst colon = rest.indexOf(\":\");\n\tif (colon === -1) return null;\n\tconst id = rest.slice(0, colon);\n\tconst tail = rest.slice(colon + 1);\n\tif (tail === \"\") return { id, action: \"noop\" };\n\tif (tail === \"x\") return { id, action: \"cancel\" };\n\tif (tail.startsWith(\"p\")) return { id, action: \"page\", payload: tail.slice(1) };\n\treturn { id, action: \"select\", payload: tail };\n}\n\nfunction isSameChat(a: SessionSource, b: SessionSource): boolean {\n\treturn a.platform === b.platform && a.chatId === b.chatId && a.threadId === b.threadId;\n}\n\nfunction sourceForAuth(picker: PendingPicker, cb: CallbackEvent): SessionSource {\n\treturn {\n\t\tplatform: picker.source.platform,\n\t\tchatId: cb.chatId,\n\t\tchatType: picker.source.chatType,\n\t\tuserId: cb.userId,\n\t\tuserName: cb.userName,\n\t\tthreadId: cb.threadId,\n\t\t// Do NOT inherit the original invoker's role authorization.\n\t\troleAuthorized: undefined,\n\t};\n}\n\nfunction renderPage(picker: PendingPicker): { text: string; buttons: ButtonSpec[][] } {\n\tconst { id, title, breadcrumbs, items, page, perPage } = picker;\n\tconst totalPages = Math.max(1, Math.ceil(items.length / perPage));\n\tconst clampedPage = Math.max(0, Math.min(page, totalPages - 1));\n\tconst start = clampedPage * perPage;\n\tconst pageItems = items.slice(start, start + perPage);\n\n\tconst rows: ButtonSpec[][] = [];\n\tfor (let i = 0; i < pageItems.length; i += BUTTONS_PER_ROW) {\n\t\tconst row: ButtonSpec[] = [];\n\t\tfor (let j = 0; j < BUTTONS_PER_ROW && i + j < pageItems.length; j++) {\n\t\t\tconst index = start + i + j;\n\t\t\trow.push({ label: pageItems[i + j].label, data: makeCallbackData(id, \"select\", String(index)) });\n\t\t}\n\t\trows.push(row);\n\t}\n\n\tif (totalPages > 1) {\n\t\tconst nav: ButtonSpec[] = [];\n\t\tif (clampedPage > 0) {\n\t\t\tnav.push({ label: \"◀ Prev\", data: makeCallbackData(id, \"page\", String(clampedPage - 1)) });\n\t\t}\n\t\tnav.push({ label: `${clampedPage + 1}/${totalPages}`, data: makeCallbackData(id, \"noop\") });\n\t\tif (clampedPage < totalPages - 1) {\n\t\t\tnav.push({ label: \"Next ▶\", data: makeCallbackData(id, \"page\", String(clampedPage + 1)) });\n\t\t}\n\t\trows.push(nav);\n\t}\n\n\trows.push([{ label: \"✗ Cancel\", data: makeCallbackData(id, \"cancel\") }]);\n\n\tconst header = breadcrumbs ? `${title} · ${breadcrumbs}` : title;\n\tconst text =\n\t\titems.length === 0 ? `${header}\\n\\nNo options available.` : `${header} (${clampedPage + 1}/${totalPages}):`;\n\treturn { text, buttons: rows };\n}\n\n/**\n * Create and send a paginated inline-button picker. On send failure the caller\n * should fall back to a text-based list.\n */\nexport async function createPicker(\n\tadapter: PlatformAdapter,\n\tsource: SessionSource,\n\tspec: PickerSpec,\n\topts?: SendOptions,\n): Promise<SendResult> {\n\tconst id = generatePickerId();\n\tconst pending: PendingPicker = {\n\t\t...spec,\n\t\tid,\n\t\tchatId: source.chatId,\n\t\tsource,\n\t\tadapter,\n\t\tmessageId: \"\",\n\t\tpage: 0,\n\t\tperPage: spec.perPage ?? DEFAULT_PER_PAGE,\n\t\tcreatedAt: Date.now(),\n\t};\n\tconst { text, buttons } = renderPage(pending);\n\tconst result = await adapter.sendButtons(source.chatId, text, buttons, opts);\n\tif (!result.success) return result;\n\tif (result.messageId) {\n\t\tpending.messageId = result.messageId;\n\t\tregistry.set(id, pending);\n\t}\n\treturn result;\n}\n\nasync function answerExpired(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"Picker expired — run the command again.\").catch(() => {});\n}\n\nasync function answerUnauthorized(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not authorized.\").catch(() => {});\n}\n\nasync function answerNotYourPicker(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not your picker.\").catch(() => {});\n}\n\n/** Dispatch a CallbackEvent to the matching picker entry. */\nexport async function handleCallback(\n\tcb: CallbackEvent,\n\tdeps: ButtonDeps & { adapter: PlatformAdapter },\n): Promise<void> {\n\tconst { adapter, cfg, pairing } = deps;\n\tconst parsed = parseCallbackData(cb.data);\n\tif (!parsed) return;\n\n\tconst picker = registry.get(parsed.id);\n\tif (!picker) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (picker.validate && !picker.validate()) {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (Date.now() > picker.createdAt + PICKER_TTL_MS) {\n\t\tpicker.onCancel?.();\n\t\tregistry.delete(parsed.id);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"⏱ Expired — run the command again.\", []);\n\t\t} catch {\n\t\t\t// best-effort expiry message\n\t\t}\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isSameChat(picker.source, sourceForAuth(picker, cb))) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (cb.userId !== picker.invokerId) {\n\t\tawait answerNotYourPicker(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isAuthorized(sourceForAuth(picker, cb), cfg, pairing)) {\n\t\tawait answerUnauthorized(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"cancel\") {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"Cancelled.\", []);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"noop\") {\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"page\") {\n\t\tconst page = Number.parseInt(parsed.payload ?? \"0\", 10);\n\t\tconst totalPages = Math.max(1, Math.ceil(picker.items.length / picker.perPage));\n\t\tpicker.page = Math.max(0, Math.min(page, totalPages - 1));\n\t\tconst { text, buttons } = renderPage(picker);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\t// select\n\tconst index = Number.parseInt(parsed.payload ?? \"\", 10);\n\tconst item = picker.items[index];\n\tif (!item) {\n\t\tawait adapter.answerCallback(cb.id, \"Invalid selection.\").catch(() => {});\n\t\treturn;\n\t}\n\n\tif (picker.resolving) {\n\t\tawait adapter.answerCallback(cb.id, \"Selection is still running.\").catch(() => {});\n\t\treturn;\n\t}\n\tpicker.resolving = true;\n\tawait adapter.answerCallback(cb.id).catch(() => {});\n\ttry {\n\t\tconst result = await picker.resolve(item);\n\t\tif (!result.done) {\n\t\t\tpicker.items = result.items;\n\t\t\tpicker.title = result.title;\n\t\t\tpicker.breadcrumbs = result.breadcrumbs;\n\t\t\tpicker.page = 0;\n\t\t\tconst { text, buttons } = renderPage(picker);\n\t\t\ttry {\n\t\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t\t} catch {}\n\t\t\treturn;\n\t\t}\n\t\tregistry.delete(parsed.id);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, result.text, []);\n\t\t} catch {}\n\t} catch (err) {\n\t\tregistry.delete(parsed.id);\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, `⚠ ${message}`, []);\n\t\t} catch {}\n\t} finally {\n\t\tpicker.resolving = false;\n\t}\n}\n\n/** Start the periodic TTL sweeper. Idempotent. */\nexport function startButtonSweeper(): void {\n\tif (sweeper) return;\n\tsweeper = setInterval(() => {\n\t\tconst cutoff = Date.now() - PICKER_TTL_MS;\n\t\tfor (const [id, entry] of registry) {\n\t\t\tif (entry.createdAt > cutoff) continue;\n\t\t\tregistry.delete(id);\n\t\t\tentry.onCancel?.();\n\t\t\ttry {\n\t\t\t\tvoid entry.adapter.editMessage(entry.chatId, entry.messageId, \"⏱ Expired — run the command again.\", []);\n\t\t\t} catch {\n\t\t\t\t// best-effort expiry edit\n\t\t\t}\n\t\t}\n\t}, SWEEP_INTERVAL_MS);\n\tsweeper.unref?.();\n}\n\n/** Stop the TTL sweeper. */\nexport function stopButtonSweeper(): void {\n\tif (sweeper) {\n\t\tclearInterval(sweeper);\n\t\tsweeper = undefined;\n\t}\n}\n\n/** Test hook: reset the registry and stop the sweeper. */\nexport function resetButtonRegistry(): void {\n\tregistry.clear();\n\tstopButtonSweeper();\n}\n\n/** Test hook: expose active picker ids. */\nexport function activePickerIds(): string[] {\n\treturn [...registry.keys()];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buttons.js","sourceRoot":"","sources":["../../src/gateway/buttons.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway inline-button picker registry.\n *\n * Single registry for all interactive prompts (model/thinking/sessions pickers\n * now; approvals later). Callback payloads stay tiny so they fit Telegram's\n * 64-byte callback_data limit; all picker state lives server-side in this module.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { isAuthorized } from \"./authz.ts\";\nimport type { GatewayConfig } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { ButtonSpec, CallbackEvent, PlatformAdapter, SendOptions, SendResult, SessionSource } from \"./types.ts\";\n\nconst PICKER_TTL_MS = 15 * 60 * 1000;\nconst SWEEP_INTERVAL_MS = 60 * 1000;\nconst DEFAULT_PER_PAGE = 8;\nconst BUTTONS_PER_ROW = 2;\nconst CODE_ALPHABET = \"ABCDEFGHJKMNPQRSTUVWXYZ23456789\";\nconst CODE_LENGTH = 8;\nconst CALLBACK_PREFIX = \"cp:\";\n\nexport interface PickerItem {\n\tlabel: string;\n\tvalue: string;\n}\n\nexport type PickerResolveResult =\n\t| { done: true; text: string }\n\t| { done: false; items: PickerItem[]; title: string; breadcrumbs?: string };\n\nexport interface PickerSpec {\n\tkind: \"model\" | \"thinking\" | \"sessions\" | \"project\" | \"dialog\";\n\tvalidate?(): boolean;\n\tonCancel?(): void;\n\tsessionKey: string;\n\tinvokerId: string;\n\titems: PickerItem[];\n\tperPage?: number;\n\ttitle: string;\n\tbreadcrumbs?: string;\n\tresolve(item: PickerItem): Promise<PickerResolveResult>;\n}\n\ninterface PendingPicker extends PickerSpec {\n\tresolving?: boolean;\n\tcompleted?: string;\n\tid: string;\n\tchatId: string;\n\tmessageId: string;\n\tsource: SessionSource;\n\tadapter: PlatformAdapter;\n\tpage: number;\n\tperPage: number;\n\tcreatedAt: number;\n}\n\nexport interface ButtonDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n}\n\nconst registry = new Map<string, PendingPicker>();\nlet sweeper: ReturnType<typeof setInterval> | undefined;\n\nfunction generatePickerId(): string {\n\tlet id = \"\";\n\tconst bytes = randomBytes(CODE_LENGTH);\n\tfor (let i = 0; i < CODE_LENGTH; i++) {\n\t\tid += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length];\n\t}\n\treturn id;\n}\n\nfunction makeCallbackData(id: string, action: \"select\" | \"page\" | \"cancel\" | \"noop\", payload?: string): string {\n\tif (action === \"noop\") return `${CALLBACK_PREFIX}${id}:`;\n\tif (action === \"cancel\") return `${CALLBACK_PREFIX}${id}:x`;\n\tif (action === \"page\") return `${CALLBACK_PREFIX}${id}:p${payload ?? \"0\"}`;\n\treturn `${CALLBACK_PREFIX}${id}:${payload ?? \"\"}`;\n}\n\nfunction parseCallbackData(\n\tdata: string,\n): { id: string; action: \"select\" | \"page\" | \"cancel\" | \"noop\"; payload?: string } | null {\n\tif (!data.startsWith(CALLBACK_PREFIX)) return null;\n\tconst rest = data.slice(CALLBACK_PREFIX.length);\n\tconst colon = rest.indexOf(\":\");\n\tif (colon === -1) return null;\n\tconst id = rest.slice(0, colon);\n\tconst tail = rest.slice(colon + 1);\n\tif (tail === \"\") return { id, action: \"noop\" };\n\tif (tail === \"x\") return { id, action: \"cancel\" };\n\tif (tail.startsWith(\"p\")) return { id, action: \"page\", payload: tail.slice(1) };\n\treturn { id, action: \"select\", payload: tail };\n}\n\nfunction isSameChat(a: SessionSource, b: SessionSource): boolean {\n\treturn a.platform === b.platform && a.chatId === b.chatId && a.threadId === b.threadId;\n}\n\nfunction sourceForAuth(picker: PendingPicker, cb: CallbackEvent): SessionSource {\n\treturn {\n\t\tplatform: picker.source.platform,\n\t\tchatId: cb.chatId,\n\t\tchatType: picker.source.chatType,\n\t\tuserId: cb.userId,\n\t\tuserName: cb.userName,\n\t\tthreadId: cb.threadId,\n\t\t// Do NOT inherit the original invoker's role authorization.\n\t\troleAuthorized: undefined,\n\t};\n}\n\nfunction renderPage(picker: PendingPicker): { text: string; buttons: ButtonSpec[][] } {\n\tconst { id, title, breadcrumbs, items, page, perPage } = picker;\n\tconst totalPages = Math.max(1, Math.ceil(items.length / perPage));\n\tconst clampedPage = Math.max(0, Math.min(page, totalPages - 1));\n\tconst start = clampedPage * perPage;\n\tconst pageItems = items.slice(start, start + perPage);\n\n\tconst rows: ButtonSpec[][] = [];\n\tfor (let i = 0; i < pageItems.length; i += BUTTONS_PER_ROW) {\n\t\tconst row: ButtonSpec[] = [];\n\t\tfor (let j = 0; j < BUTTONS_PER_ROW && i + j < pageItems.length; j++) {\n\t\t\tconst index = start + i + j;\n\t\t\trow.push({ label: pageItems[i + j].label, data: makeCallbackData(id, \"select\", String(index)) });\n\t\t}\n\t\trows.push(row);\n\t}\n\n\tif (totalPages > 1) {\n\t\tconst nav: ButtonSpec[] = [];\n\t\tif (clampedPage > 0) {\n\t\t\tnav.push({ label: \"◀ Prev\", data: makeCallbackData(id, \"page\", String(clampedPage - 1)) });\n\t\t}\n\t\tnav.push({ label: `${clampedPage + 1}/${totalPages}`, data: makeCallbackData(id, \"noop\") });\n\t\tif (clampedPage < totalPages - 1) {\n\t\t\tnav.push({ label: \"Next ▶\", data: makeCallbackData(id, \"page\", String(clampedPage + 1)) });\n\t\t}\n\t\trows.push(nav);\n\t}\n\n\trows.push([{ label: \"✗ Cancel\", data: makeCallbackData(id, \"cancel\") }]);\n\n\tconst header = breadcrumbs ? `${title} · ${breadcrumbs}` : title;\n\tconst text =\n\t\titems.length === 0 ? `${header}\\n\\nNo options available.` : `${header} (${clampedPage + 1}/${totalPages}):`;\n\treturn { text, buttons: rows };\n}\n\n/**\n * Create and send a paginated inline-button picker. On send failure the caller\n * should fall back to a text-based list.\n */\nexport async function createPicker(\n\tadapter: PlatformAdapter,\n\tsource: SessionSource,\n\tspec: PickerSpec,\n\topts?: SendOptions,\n): Promise<SendResult> {\n\tconst id = generatePickerId();\n\tconst pending: PendingPicker = {\n\t\t...spec,\n\t\tid,\n\t\tchatId: source.chatId,\n\t\tsource,\n\t\tadapter,\n\t\tmessageId: \"\",\n\t\tpage: 0,\n\t\tperPage: spec.perPage ?? DEFAULT_PER_PAGE,\n\t\tcreatedAt: Date.now(),\n\t};\n\tconst { text, buttons } = renderPage(pending);\n\tconst result = await adapter.sendButtons(source.chatId, text, buttons, opts);\n\tif (!result.success) return result;\n\tif (result.messageId) {\n\t\tpending.messageId = result.messageId;\n\t\tregistry.set(id, pending);\n\t}\n\treturn result;\n}\n\nasync function answerExpired(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"Picker expired — run the command again.\").catch(() => {});\n}\n\nasync function answerUnauthorized(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not authorized.\").catch(() => {});\n}\n\nasync function answerNotYourPicker(adapter: PlatformAdapter, cb: CallbackEvent): Promise<void> {\n\tawait adapter.answerCallback(cb.id, \"⛔ Not your picker.\").catch(() => {});\n}\n\n/** Dispatch a CallbackEvent to the matching picker entry. */\nexport async function handleCallback(\n\tcb: CallbackEvent,\n\tdeps: ButtonDeps & { adapter: PlatformAdapter },\n): Promise<void> {\n\tconst { adapter, cfg, pairing } = deps;\n\tconst parsed = parseCallbackData(cb.data);\n\tif (!parsed) return;\n\n\tconst picker = registry.get(parsed.id);\n\tif (!picker) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!picker.completed && picker.validate && !picker.validate()) {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (Date.now() > picker.createdAt + PICKER_TTL_MS) {\n\t\tif (!picker.completed) picker.onCancel?.();\n\t\tregistry.delete(parsed.id);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"⏱ Expired — run the command again.\", []);\n\t\t} catch {\n\t\t\t// best-effort expiry message\n\t\t}\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isSameChat(picker.source, sourceForAuth(picker, cb))) {\n\t\tawait answerExpired(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (cb.userId !== picker.invokerId) {\n\t\tawait answerNotYourPicker(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (!isAuthorized(sourceForAuth(picker, cb), cfg, pairing)) {\n\t\tawait answerUnauthorized(adapter, cb);\n\t\treturn;\n\t}\n\n\tif (picker.completed !== undefined) {\n\t\tawait adapter.answerCallback(cb.id, \"Already completed.\").catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"cancel\") {\n\t\tregistry.delete(parsed.id);\n\t\tpicker.onCancel?.();\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, \"Cancelled.\", []);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"noop\") {\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\tif (parsed.action === \"page\") {\n\t\tconst page = Number.parseInt(parsed.payload ?? \"0\", 10);\n\t\tconst totalPages = Math.max(1, Math.ceil(picker.items.length / picker.perPage));\n\t\tpicker.page = Math.max(0, Math.min(page, totalPages - 1));\n\t\tconst { text, buttons } = renderPage(picker);\n\t\ttry {\n\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t} catch {}\n\t\tawait adapter.answerCallback(cb.id).catch(() => {});\n\t\treturn;\n\t}\n\n\t// select\n\tconst index = Number.parseInt(parsed.payload ?? \"\", 10);\n\tconst item = picker.items[index];\n\tif (!item) {\n\t\tawait adapter.answerCallback(cb.id, \"Invalid selection.\").catch(() => {});\n\t\treturn;\n\t}\n\n\tif (picker.resolving) {\n\t\tawait adapter.answerCallback(cb.id, \"Selection is still running.\").catch(() => {});\n\t\treturn;\n\t}\n\tpicker.resolving = true;\n\tawait adapter.answerCallback(cb.id).catch(() => {});\n\ttry {\n\t\tconst result = await picker.resolve(item);\n\t\tif (!result.done) {\n\t\t\tpicker.items = result.items;\n\t\t\tpicker.title = result.title;\n\t\t\tpicker.breadcrumbs = result.breadcrumbs;\n\t\t\tpicker.page = 0;\n\t\t\tconst { text, buttons } = renderPage(picker);\n\t\t\ttry {\n\t\t\t\tawait picker.adapter.editMessage(picker.chatId, picker.messageId, text, buttons);\n\t\t\t} catch {}\n\t\t\treturn;\n\t\t}\n\t\tpicker.completed = result.text;\n\t\tawait confirmPicker(picker, result.text);\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tpicker.completed = `⚠ ${message}`;\n\t\tawait confirmPicker(picker, picker.completed);\n\t} finally {\n\t\tpicker.resolving = false;\n\t}\n}\n\nasync function confirmPicker(picker: PendingPicker, text: string): Promise<void> {\n\tconst edited = await picker.adapter\n\t\t.editMessage(picker.chatId, picker.messageId, text, [])\n\t\t.catch(() => ({ success: false }));\n\tif (edited.success) return;\n\tconst sent = await picker.adapter\n\t\t.send(picker.chatId, text, { threadId: picker.source.threadId })\n\t\t.catch(() => ({ success: false }));\n\tif (!sent.success) console.error(`[gateway] picker confirmation failed on ${picker.adapter.platform}`);\n}\n\n/** Start the periodic TTL sweeper. Idempotent. */\nexport function startButtonSweeper(): void {\n\tif (sweeper) return;\n\tsweeper = setInterval(() => {\n\t\tconst cutoff = Date.now() - PICKER_TTL_MS;\n\t\tfor (const [id, entry] of registry) {\n\t\t\tif (entry.createdAt > cutoff) continue;\n\t\t\tregistry.delete(id);\n\t\t\tif (entry.completed) continue;\n\t\t\tentry.onCancel?.();\n\t\t\tvoid entry.adapter\n\t\t\t\t.editMessage(entry.chatId, entry.messageId, \"⏱ Expired — run the command again.\", [])\n\t\t\t\t.catch(() => {});\n\t\t}\n\t}, SWEEP_INTERVAL_MS);\n\tsweeper.unref?.();\n}\n\n/** Stop the TTL sweeper. */\nexport function stopButtonSweeper(): void {\n\tif (sweeper) {\n\t\tclearInterval(sweeper);\n\t\tsweeper = undefined;\n\t}\n}\n\n/** Test hook: reset the registry and stop the sweeper. */\nexport function resetButtonRegistry(): void {\n\tregistry.clear();\n\tstopButtonSweeper();\n}\n\n/** Test hook: expose active picker ids. */\nexport function activePickerIds(): string[] {\n\treturn [...registry].filter(([, picker]) => picker.completed === undefined).map(([id]) => id);\n}\n"]}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* guards, error wrapping, and /help generation.
|
|
7
7
|
*/
|
|
8
8
|
import type { BridgeSession } from "./agent-bridge.ts";
|
|
9
|
+
import { type GatewayConfig } from "./config.ts";
|
|
9
10
|
import type { BridgeLike } from "./router.ts";
|
|
10
11
|
import type { MessageEvent, PlatformAdapter } from "./types.ts";
|
|
11
12
|
export interface ChatCommandContext {
|
|
@@ -13,6 +14,7 @@ export interface ChatCommandContext {
|
|
|
13
14
|
key: string;
|
|
14
15
|
adapter: PlatformAdapter;
|
|
15
16
|
bridge: BridgeLike;
|
|
17
|
+
cfg?: GatewayConfig;
|
|
16
18
|
session?: BridgeSession;
|
|
17
19
|
reply(text: string): Promise<void>;
|
|
18
20
|
args: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/gateway/commands.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAmEhE,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IAEtB,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAC1D;AA6iBD,eAAO,MAAM,aAAa,EAAE,WAAW,EActC,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,eAAe,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,CASzE;AAED,wBAAgB,cAAc,IAAI,MAAM,CAYvC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BhG;AAED,uEAAuE;AACvE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjH","sourcesContent":["/**\n * lunR: gateway chat command registry.\n *\n * Every command is a small headless handler over AgentSession. Router.ts keeps\n * platform gating/authorization and delegates to runChatCommand for busy/session\n * guards, error wrapping, and /help generation.\n */\n\nimport type { ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport type { Model } from \"@ashx-j/lunr-ai/compat\";\nimport { computeContextBreakdown } from \"../core/context-breakdown.ts\";\nimport type { ToolDefinition } from \"../core/extensions/types.ts\";\nimport { findExactModelReferenceMatch } from \"../core/model-resolver.ts\";\nimport type { ModelRuntime } from \"../core/model-runtime.ts\";\nimport { runtimeScope } from \"../core/runtime-scope.ts\";\nimport type { SessionInfo } from \"../core/session-manager.ts\";\nimport { SessionManager } from \"../core/session-manager.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { createPicker, type PickerItem } from \"./buttons.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { FORWARDED_COMMANDS, MOBILE_COMMANDS } from \"./mobile-commands.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\nconst MODEL_CACHE_TTL_MS = 5 * 60 * 1000;\nconst SESSIONS_CACHE_TTL_MS = 5 * 60 * 1000;\nconst MAX_MODEL_LIST_LINES = 15;\nconst PROVIDER_PER_PAGE = 10;\nconst _MODEL_PER_PAGE = 8;\nconst MAX_MODELS_PER_PROVIDER = 50;\nconst MODEL_ID_MAX = 38;\nconst MODEL_CACHE_CAP = 100;\nconst SESSIONS_CACHE_CAP = 100;\n\ninterface ModelCacheEntry {\n\tmodels: Model<any>[];\n\texpires: number;\n}\n\ninterface SessionsCacheEntry {\n\tsessions: SessionInfo[];\n\texpires: number;\n}\n\nconst modelCache = new Map<string, ModelCacheEntry>();\nconst sessionsCache = new Map<string, SessionsCacheEntry>();\n\nfunction evictLRU<K, V>(map: Map<K, V>, cap: number): void {\n\twhile (map.size > cap) {\n\t\tconst first = map.keys().next().value;\n\t\tif (first !== undefined) map.delete(first);\n\t}\n}\n\nfunction getCachedModels(key: string): Model<any>[] | undefined {\n\tconst cached = modelCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tmodelCache.delete(key);\n\t\treturn undefined;\n\t}\n\t// LRU refresh.\n\tmodelCache.delete(key);\n\tmodelCache.set(key, cached);\n\treturn cached.models;\n}\n\nfunction setCachedModels(key: string, models: Model<any>[]): void {\n\tmodelCache.set(key, { models, expires: Date.now() + MODEL_CACHE_TTL_MS });\n\tevictLRU(modelCache, MODEL_CACHE_CAP);\n}\n\nfunction getCachedSessions(key: string): SessionInfo[] | undefined {\n\tconst cached = sessionsCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tsessionsCache.delete(key);\n\t\treturn undefined;\n\t}\n\tsessionsCache.delete(key);\n\tsessionsCache.set(key, cached);\n\treturn cached.sessions;\n}\n\nfunction setCachedSessions(key: string, sessions: SessionInfo[]): void {\n\tsessionsCache.set(key, { sessions, expires: Date.now() + SESSIONS_CACHE_TTL_MS });\n\tevictLRU(sessionsCache, SESSIONS_CACHE_CAP);\n}\n\nexport interface ChatCommandContext {\n\tevent: MessageEvent;\n\tkey: string;\n\tadapter: PlatformAdapter;\n\tbridge: BridgeLike;\n\tsession?: BridgeSession;\n\treply(text: string): Promise<void>;\n\targs: string;\n}\n\nexport interface ChatCommand {\n\tname: string;\n\taliases?: string[];\n\tdescription: string;\n\tbypassBusy: boolean;\n\tneedsSession: boolean;\n\t// biome-ignore lint/suspicious/noConfusingVoidType: handlers may return nothing; runChatCommand treats undefined as consumed.\n\thandler(ctx: ChatCommandContext): Promise<boolean | void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction prefixLines(text: string, prefix: string): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line, index) => (index === 0 ? prefix : \" \") + line)\n\t\t.join(\"\\n\");\n}\n\nfunction truncate(text: string, max: number): string {\n\tif (text.length <= max) return text;\n\treturn `${text.slice(0, max - 1)}…`;\n}\n\nfunction formatRelativeTime(date: Date): string {\n\tconst seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));\n\tif (seconds < 60) return `${seconds}s ago`;\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) return `${minutes}m ago`;\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) return `${hours}h ago`;\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d ago`;\n}\n\nfunction formatModelList(models: Model<any>[], maxLines = MAX_MODEL_LIST_LINES): string[] {\n\tif (models.length === 0) return [\"No models available.\"];\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tconst providers = [...byProvider.keys()].sort();\n\tconst lines: string[] = [];\n\tlet index = 1;\n\tfor (const provider of providers) {\n\t\tconst list = (byProvider.get(provider) ?? []).sort((a, b) => a.id.localeCompare(b.id));\n\t\tfor (const model of list) {\n\t\t\tlines.push(`${index}) ${provider}/${model.id}`);\n\t\t\tindex++;\n\t\t}\n\t}\n\tif (lines.length <= maxLines) return lines;\n\treturn [\n\t\t`${models.length} models across ${providers.length} providers.`,\n\t\t\"Use /model <provider/id> or /model <query>.\",\n\t\t...providers.map((p) => `- ${p}`),\n\t];\n}\n\nfunction groupModelsByProvider(models: Model<any>[]): Map<string, Model<any>[]> {\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tfor (const list of byProvider.values()) {\n\t\tlist.sort((a, b) => a.id.localeCompare(b.id));\n\t}\n\treturn byProvider;\n}\n\nfunction truncateModelId(id: string): string {\n\tconst lastSlash = id.lastIndexOf(\"/\");\n\tconst name = lastSlash >= 0 ? id.slice(lastSlash + 1) : id;\n\treturn truncate(name, MODEL_ID_MAX);\n}\n\nasync function refreshAndCacheModels(session: BridgeSession, key: string): Promise<Model<any>[]> {\n\tconst runtime: ModelRuntime = session.modelRuntime;\n\tawait runtime.refresh();\n\tconst models = [...(await runtime.getAvailable())].sort(\n\t\t(a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),\n\t);\n\tsetCachedModels(key, models);\n\treturn models;\n}\n\nasync function setModelAndReply(ctx: ChatCommandContext, model: Model<any>): Promise<void> {\n\tconst session = ctx.session!;\n\ttry {\n\t\tawait session.setModel(model);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (message.includes(\"No API key\")) {\n\t\t\tawait ctx.reply(`No API key for ${model.provider}.`);\n\t\t\treturn;\n\t\t}\n\t\tthrow error;\n\t}\n\tawait ctx.reply(`Model → ${model.provider}/${model.id} (thinking level re-clamped to ${session.thinkingLevel})`);\n}\n\nconst newCommand: ChatCommand = {\n\tname: \"new\",\n\taliases: [\"reset\"],\n\tdescription: \"start a fresh session for this chat\",\n\t// lunr: busy-session /new must bypass the guard (same as /stop).\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.reset(ctx.key);\n\t\tawait ctx.reply(\"Session reset — next message starts a fresh session.\");\n\t},\n};\n\nconst undoCommand: ChatCommand = {\n\tname: \"undo\",\n\tdescription: \"rewind the session tree before the last user turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst { userText } = await ctx.bridge.undo(ctx.key);\n\t\tconst text = userText ? ` \"${truncate(userText, 60)}\"` : \"\";\n\t\tawait ctx.reply(`Undone — rewound before${text}. /redo to restore.`);\n\t},\n};\n\nconst redoCommand: ChatCommand = {\n\tname: \"redo\",\n\tdescription: \"restore the last undone turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.redo(ctx.key);\n\t\tawait ctx.reply(\"Redone.\");\n\t},\n};\n\nconst stopCommand: ChatCommand = {\n\tname: \"stop\",\n\tdescription: \"abort the running turn\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.abort(ctx.key);\n\t\tawait ctx.reply(\"Stopped.\");\n\t},\n};\n\nconst statusCommand: ChatCommand = {\n\tname: \"status\",\n\tdescription: \"session status\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst status = ctx.bridge.getStatus(ctx.key);\n\t\tconst age = status.createdAt\n\t\t\t? `${Math.max(0, Math.round((Date.now() - Date.parse(status.createdAt)) / 1000))}s`\n\t\t\t: \"no session yet\";\n\t\tawait ctx.reply(\n\t\t\t`${ctx.event.source.platform} · session age ${age} · ${status.busy ? \"busy\" : \"idle\"} · queue ${status.queueDepth}\\nProject: ${conversationBinding(ctx.key)?.cwd ?? \"not selected\"}`,\n\t\t);\n\t},\n};\n\nconst whoamiCommand: ChatCommand = {\n\tname: \"whoami\",\n\tdescription: \"your ids and session key\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst { userId, chatId } = ctx.event.source;\n\t\tawait ctx.reply(`userId: ${userId}\\nchatId: ${chatId}\\nkey: ${ctx.key}`);\n\t},\n};\n\nconst helpCommand: ChatCommand = {\n\tname: \"help\",\n\tdescription: \"this message\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.reply(formatHelpText());\n\t},\n};\n\nconst modelCommand: ChatCommand = {\n\tname: \"model\",\n\tdescription: \"list or switch models\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tif (!arg) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tconst models = cached ?? (await refreshAndCacheModels(session, ctx.key));\n\t\t\tconst byProvider = groupModelsByProvider(models);\n\t\t\tconst current = session.model;\n\t\t\tconst currentRef = current ? `${current.provider}/${current.id}` : \"\";\n\t\t\tconst providers = [...byProvider.keys()].sort();\n\t\t\tconst providerItems: PickerItem[] = providers.map((provider) => ({\n\t\t\t\tlabel: `${current?.provider === provider ? \"✓ \" : \"\"}${provider} (${byProvider.get(provider)?.length ?? 0})`,\n\t\t\t\tvalue: provider,\n\t\t\t}));\n\n\t\t\tconst pickerOpts = {\n\t\t\t\treplyTo: ctx.event.messageId,\n\t\t\t\tthreadId: ctx.event.source.threadId,\n\t\t\t};\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"model\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems: providerItems,\n\t\t\t\t\tperPage: PROVIDER_PER_PAGE,\n\t\t\t\t\ttitle: \"Pick a model provider\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tif (item.value === \"__back__\") {\n\t\t\t\t\t\t\treturn { done: false, items: providerItems, title: \"Pick a model provider\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!item.value.includes(\"/\")) {\n\t\t\t\t\t\t\tconst provider = item.value;\n\t\t\t\t\t\t\tconst list = byProvider.get(provider) ?? [];\n\t\t\t\t\t\t\tconst capped = list.slice(0, MAX_MODELS_PER_PROVIDER);\n\t\t\t\t\t\t\tconst note =\n\t\t\t\t\t\t\t\tlist.length > MAX_MODELS_PER_PROVIDER\n\t\t\t\t\t\t\t\t\t? ` — ${list.length - MAX_MODELS_PER_PROVIDER} more available`\n\t\t\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\t\tconst modelItems: PickerItem[] = [];\n\t\t\t\t\t\t\tfor (const m of capped) {\n\t\t\t\t\t\t\t\tconst ref = `${provider}/${m.id}`;\n\t\t\t\t\t\t\t\tmodelItems.push({\n\t\t\t\t\t\t\t\t\tlabel: `${currentRef === ref ? \"✓ \" : \"\"}${truncateModelId(m.id)}`,\n\t\t\t\t\t\t\t\t\tvalue: ref,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmodelItems.unshift({ label: \"◀ Back\", value: \"__back__\" });\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\t\titems: modelItems,\n\t\t\t\t\t\t\t\ttitle: `${provider} (${list.length})${note}`,\n\t\t\t\t\t\t\t\tbreadcrumbs: provider,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst exact = findExactModelReferenceMatch(item.value, models);\n\t\t\t\t\t\tif (!exact) {\n\t\t\t\t\t\t\treturn { done: true, text: \"That model is no longer available.\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait session.setModel(exact);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tif (message.includes(\"No API key\")) {\n\t\t\t\t\t\t\t\treturn { done: true, text: `No API key for ${exact.provider}.` };\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tdone: true,\n\t\t\t\t\t\t\ttext: `☾ Model → ${exact.provider}/${exact.id} (thinking level re-clamped to ${session.thinkingLevel})`,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpickerOpts,\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst currentText = `${current?.provider ?? \"none\"}/${current?.id ?? \"none\"}`;\n\t\t\t\tconst lines = formatModelList(models);\n\t\t\t\tawait ctx.reply([`Current: ${currentText}`, ...lines, \"Use /model <n> or /model <provider/id>\"].join(\"\\n\"));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /model to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait setModelAndReply(ctx, cached[numeric - 1]);\n\t\t\treturn;\n\t\t}\n\n\t\tconst models = getCachedModels(ctx.key) ?? (await refreshAndCacheModels(session, ctx.key));\n\t\tconst exact = findExactModelReferenceMatch(arg, models);\n\t\tif (exact) {\n\t\t\tawait setModelAndReply(ctx, exact);\n\t\t\treturn;\n\t\t}\n\n\t\tconst query = arg.toLowerCase();\n\t\tconst matches = models.filter(\n\t\t\t(m) =>\n\t\t\t\tm.id.toLowerCase().includes(query) ||\n\t\t\t\t`${m.provider}/${m.id}`.toLowerCase().includes(query) ||\n\t\t\t\t(m.name?.toLowerCase().includes(query) ?? false),\n\t\t);\n\t\tif (matches.length === 0) {\n\t\t\tawait ctx.reply(`No model matches \"${arg}\".`);\n\t\t\treturn;\n\t\t}\n\t\tif (matches.length > 1) {\n\t\t\tawait ctx.reply(formatModelList(matches).join(\"\\n\"));\n\t\t\treturn;\n\t\t}\n\t\tawait setModelAndReply(ctx, matches[0]);\n\t},\n};\n\nconst sessionsCommand: ChatCommand = {\n\tname: \"sessions\",\n\tdescription: \"list or switch sessions\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tconst currentFile = session.sessionManager?.getSessionFile();\n\n\t\tif (!arg) {\n\t\t\tconst all = await SessionManager.list(process.cwd());\n\t\t\tall.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t\tconst sessions = all.slice(0, 10);\n\t\t\tsetCachedSessions(ctx.key, sessions);\n\t\t\tconst items: PickerItem[] = sessions.map((s) => ({\n\t\t\t\tlabel: `${s.path === currentFile ? \"☾ \" : \"\"}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || \"(empty)\"}`,\n\t\t\t\tvalue: s.path,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"sessions\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: 8,\n\t\t\t\t\ttitle: \"Pick a session\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tawait ctx.bridge.switchSession(ctx.key, item.value);\n\t\t\t\t\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\t\t\t\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\t\t\t\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\t\t\t\t\treturn { done: true, text: `☾ Switched to \"${name}\" (${count} msgs). History continues here.` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst lines = sessions.map((s, i) => {\n\t\t\t\t\tconst marker = s.path === currentFile ? \" ☾\" : \"\";\n\t\t\t\t\tconst label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || \"(empty)\";\n\t\t\t\t\treturn `${i + 1}) ${label}${marker} · ${formatRelativeTime(s.modified)} · ${s.messageCount} msgs`;\n\t\t\t\t});\n\t\t\t\tawait ctx.reply(\n\t\t\t\t\t[`Sessions for ${process.cwd()}`, ...lines, \"Use /sessions <n> or /sessions <id-prefix>\"].join(\"\\n\"),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tlet path: string | undefined;\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tconst cached = getCachedSessions(ctx.key);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /sessions to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = cached[numeric - 1].path;\n\t\t} else {\n\t\t\tconst pool = cached ?? (await SessionManager.list(process.cwd()));\n\t\t\tconst matches = pool.filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));\n\t\t\tif (matches.length === 0) {\n\t\t\t\tawait ctx.reply(`No session matches \"${arg}\".`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matches.length > 1) {\n\t\t\t\tawait ctx.reply(`Multiple sessions match \"${arg}\" — use a longer prefix.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = matches[0].path;\n\t\t}\n\n\t\tif (!path) {\n\t\t\tawait ctx.reply(\"No session selected.\");\n\t\t\treturn;\n\t\t}\n\n\t\tawait ctx.bridge.switchSession(ctx.key, path);\n\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\tawait ctx.reply(`Switched to \"${name}\" (${count} msgs). History continues here.`);\n\t},\n};\n\nconst titleCommand: ChatCommand = {\n\tname: \"title\",\n\taliases: [\"name\"],\n\tdescription: \"name the current session\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst name = ctx.args.trim();\n\t\tif (!name) {\n\t\t\tconst current = session.sessionManager?.getSessionName();\n\t\t\tawait ctx.reply(current ? `Session title: ${current}` : \"(unnamed)\");\n\t\t\treturn;\n\t\t}\n\t\tsession.setSessionName(name);\n\t\tconst readBack = session.sessionManager?.getSessionName() ?? name;\n\t\tawait ctx.reply(`Session titled \"${readBack}\".`);\n\t},\n};\n\nconst contextCommand: ChatCommand = {\n\tname: \"context\",\n\tdescription: \"estimated context window breakdown\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst model = session.model;\n\t\tif (!model) {\n\t\t\tawait ctx.reply(\"No model selected.\");\n\t\t\treturn;\n\t\t}\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) {\n\t\t\tawait ctx.reply(\"Context window unknown for this model.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst tools: ToolDefinition[] = session\n\t\t\t.getActiveToolNames()\n\t\t\t.map((name) => session.getToolDefinition(name))\n\t\t\t.filter((d): d is ToolDefinition => d !== undefined);\n\n\t\tconst breakdown = computeContextBreakdown({\n\t\t\tsystemPrompt: session.systemPrompt,\n\t\t\ttools,\n\t\t\tmessages: session.messages,\n\t\t\tcontextWindow,\n\t\t});\n\n\t\tconst usage = session.getContextUsage();\n\t\tconst tokens = usage?.tokens ?? breakdown.total;\n\t\tconst percent = usage?.percent ?? (contextWindow > 0 ? (tokens / contextWindow) * 100 : 0);\n\n\t\tconst messageTokens =\n\t\t\tbreakdown.user +\n\t\t\tbreakdown.assistantText +\n\t\t\tbreakdown.thinking +\n\t\t\tbreakdown.toolCalls +\n\t\t\tbreakdown.toolResults +\n\t\t\tbreakdown.summaries;\n\n\t\tconst stats = session.getSessionStats();\n\t\tawait ctx.reply(\n\t\t\t[\n\t\t\t\t`Context: ${tokens.toLocaleString(\"en-US\")} / ${contextWindow.toLocaleString(\"en-US\")} tokens (${Math.round(percent)}%)`,\n\t\t\t\t` system ${breakdown.systemPrompt.toLocaleString(\"en-US\")} · tools ${breakdown.toolDefinitions.toLocaleString(\"en-US\")} · messages ${messageTokens.toLocaleString(\"en-US\")}`,\n\t\t\t\t`Session: ${stats.totalMessages} messages · ${stats.userMessages} turns · model ${model.provider}/${model.id}`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t},\n};\n\nconst compactCommand: ChatCommand = {\n\tname: \"compact\",\n\tdescription: \"summarize and compress session context\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst instructions = ctx.args.trim() || undefined;\n\t\ttry {\n\t\t\tconst result = await session.compact(instructions);\n\t\t\tconst before = result.tokensBefore.toLocaleString(\"en-US\");\n\t\t\tconst after =\n\t\t\t\tresult.estimatedTokensAfter !== undefined\n\t\t\t\t\t? `~${result.estimatedTokensAfter.toLocaleString(\"en-US\")}`\n\t\t\t\t\t: \"~?\";\n\t\t\tawait ctx.reply(`Compacted: ${before} → ${after} tokens.`);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tif (\n\t\t\t\tmessage.includes(\"Nothing to compact\") ||\n\t\t\t\tmessage.includes(\"Already compacted\") ||\n\t\t\t\tmessage.includes(\"No model selected\")\n\t\t\t) {\n\t\t\t\tawait ctx.reply(message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t},\n};\n\nconst thinkingCommand: ChatCommand = {\n\tname: \"thinking\",\n\taliases: [\"effort\", \"reasoning\"],\n\tdescription: \"get or set the thinking level\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst levels = session.getAvailableThinkingLevels();\n\t\tif (!session.supportsThinking()) {\n\t\t\tawait ctx.reply(\n\t\t\t\t`Model ${session.model?.provider ?? \"none\"}/${session.model?.id ?? \"none\"} doesn't support thinking.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst arg = ctx.args.trim().toLowerCase() as ThinkingLevel;\n\t\tif (!arg) {\n\t\t\tconst current = session.thinkingLevel;\n\t\t\tconst items: PickerItem[] = levels.map((level) => ({\n\t\t\t\tlabel: `${level === current ? \"✓ \" : \"\"}${level}`,\n\t\t\t\tvalue: level,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"thinking\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: levels.length,\n\t\t\t\t\ttitle: \"Pick a thinking level\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tconst level = item.value as ThinkingLevel;\n\t\t\t\t\t\tsession.setThinkingLevel(level);\n\t\t\t\t\t\treturn { done: true, text: `☾ Thinking → ${level}` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tawait ctx.reply(`Level: ${current} — available: ${levels.join(\", \")}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!levels.includes(arg)) {\n\t\t\tawait ctx.reply(`Invalid level. Available: ${levels.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\t\tsession.setThinkingLevel(arg);\n\t\tawait ctx.reply(`Thinking → ${arg}`);\n\t},\n};\n\nexport const CHAT_COMMANDS: ChatCommand[] = [\n\tnewCommand,\n\tundoCommand,\n\tredoCommand,\n\tstopCommand,\n\tstatusCommand,\n\twhoamiCommand,\n\thelpCommand,\n\tmodelCommand,\n\tsessionsCommand,\n\ttitleCommand,\n\tcontextCommand,\n\tcompactCommand,\n\tthinkingCommand,\n];\n\n/** Platform menu specs (e.g. Telegram setMyCommands): canonical names only, aliases skipped. */\nexport function botCommandSpecs(): { name: string; description: string }[] {\n\treturn [\n\t\t...new Map(\n\t\t\t[...CHAT_COMMANDS, ...MOBILE_COMMANDS, ...FORWARDED_COMMANDS].map((c) => [\n\t\t\t\tc.name,\n\t\t\t\t{ name: c.name, description: c.description },\n\t\t\t]),\n\t\t).values(),\n\t];\n}\n\nexport function formatHelpText(): string {\n\tconst lines = [\"lunR gateway commands:\"];\n\tfor (const cmd of CHAT_COMMANDS) {\n\t\tconst names = [cmd.name, ...(cmd.aliases ?? [])].map((a) => `/${a}`).join(\" | \");\n\t\tlines.push(`${names} — ${cmd.description}`);\n\t}\n\tfor (const cmd of botCommandSpecs()) {\n\t\tif (!CHAT_COMMANDS.some((c) => c.name === cmd.name)) lines.push(`/${cmd.name} · ${cmd.description}`);\n\t}\n\tlines.push(\"\");\n\tlines.push(\"Tap to pick: /model, /thinking, /sessions (run without args to see buttons).\");\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Run a chat command with busy/session guards and error → \"⚠ one-line\".\n * Returns true when the event was consumed by the command, false when the\n * router should continue when a command mutates the event and wants a normal turn.\n */\nexport async function runChatCommand(cmd: ChatCommand, ctx: ChatCommandContext): Promise<boolean> {\n\tif (!cmd.bypassBusy && ctx.bridge.getStatus(ctx.key).busy) {\n\t\tawait ctx.reply(\"⏳ busy — /stop first or wait\");\n\t\treturn true;\n\t}\n\tif (cmd.needsSession) {\n\t\tconst session = await ctx.bridge.getSession(ctx.key);\n\t\tif (!session) {\n\t\t\tawait ctx.reply(\"No session yet — send a message first.\");\n\t\t\treturn true;\n\t\t}\n\t\tctx.session = session;\n\t}\n\ttry {\n\t\tconst settingsManager = ctx.session?.settingsManager;\n\t\tconst consumed = settingsManager\n\t\t\t? await runtimeScope.run(\n\t\t\t\t\t{ settingsManager, modelRuntime: ctx.session?.modelRuntime, thinking: () => ctx.session!.thinkingLevel },\n\t\t\t\t\t() => cmd.handler(ctx),\n\t\t\t\t)\n\t\t\t: await cmd.handler(ctx);\n\t\treturn consumed ?? true;\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tawait ctx.adapter.send(ctx.event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: ctx.event.messageId,\n\t\t\tthreadId: ctx.event.source.threadId,\n\t\t});\n\t\treturn true;\n\t}\n}\n\n/** Reply helper used by the router to build the ChatCommandContext. */\nexport async function sendCommandReply(adapter: PlatformAdapter, event: MessageEvent, text: string): Promise<void> {\n\tawait adapter.send(event.source.chatId, prefixLines(text, \"☾ \"), {\n\t\treplyTo: event.messageId,\n\t\tthreadId: event.source.threadId,\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/gateway/commands.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAGvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAmEhE,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IAEtB,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAC1D;AA4jBD,eAAO,MAAM,aAAa,EAAE,WAAW,EActC,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,eAAe,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,CAWzE;AAED,wBAAgB,cAAc,IAAI,MAAM,CAYvC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BhG;AAED,uEAAuE;AACvE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjH","sourcesContent":["/**\n * lunR: gateway chat command registry.\n *\n * Every command is a small headless handler over AgentSession. Router.ts keeps\n * platform gating/authorization and delegates to runChatCommand for busy/session\n * guards, error wrapping, and /help generation.\n */\n\nimport type { ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport type { Model } from \"@ashx-j/lunr-ai/compat\";\nimport { computeContextBreakdown } from \"../core/context-breakdown.ts\";\nimport type { ToolDefinition } from \"../core/extensions/types.ts\";\nimport { findExactModelReferenceMatch } from \"../core/model-resolver.ts\";\nimport type { ModelRuntime } from \"../core/model-runtime.ts\";\nimport { runtimeScope } from \"../core/runtime-scope.ts\";\nimport type { SessionInfo } from \"../core/session-manager.ts\";\nimport { SessionManager } from \"../core/session-manager.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { isGatewayOwner } from \"./authz.ts\";\nimport { createPicker, type PickerItem } from \"./buttons.ts\";\nimport { defaultGatewayConfig, type GatewayConfig } from \"./config.ts\";\nimport { conversationBinding } from \"./conversations.ts\";\nimport { FORWARDED_COMMANDS, MOBILE_COMMANDS, resolveWithinRoots } from \"./mobile-commands.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\nconst MODEL_CACHE_TTL_MS = 5 * 60 * 1000;\nconst SESSIONS_CACHE_TTL_MS = 5 * 60 * 1000;\nconst MAX_MODEL_LIST_LINES = 15;\nconst PROVIDER_PER_PAGE = 10;\nconst _MODEL_PER_PAGE = 8;\nconst MAX_MODELS_PER_PROVIDER = 50;\nconst MODEL_ID_MAX = 38;\nconst MODEL_CACHE_CAP = 100;\nconst SESSIONS_CACHE_CAP = 100;\n\ninterface ModelCacheEntry {\n\tmodels: Model<any>[];\n\texpires: number;\n}\n\ninterface SessionsCacheEntry {\n\tsessions: SessionInfo[];\n\texpires: number;\n}\n\nconst modelCache = new Map<string, ModelCacheEntry>();\nconst sessionsCache = new Map<string, SessionsCacheEntry>();\n\nfunction evictLRU<K, V>(map: Map<K, V>, cap: number): void {\n\twhile (map.size > cap) {\n\t\tconst first = map.keys().next().value;\n\t\tif (first !== undefined) map.delete(first);\n\t}\n}\n\nfunction getCachedModels(key: string): Model<any>[] | undefined {\n\tconst cached = modelCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tmodelCache.delete(key);\n\t\treturn undefined;\n\t}\n\t// LRU refresh.\n\tmodelCache.delete(key);\n\tmodelCache.set(key, cached);\n\treturn cached.models;\n}\n\nfunction setCachedModels(key: string, models: Model<any>[]): void {\n\tmodelCache.set(key, { models, expires: Date.now() + MODEL_CACHE_TTL_MS });\n\tevictLRU(modelCache, MODEL_CACHE_CAP);\n}\n\nfunction getCachedSessions(key: string): SessionInfo[] | undefined {\n\tconst cached = sessionsCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tsessionsCache.delete(key);\n\t\treturn undefined;\n\t}\n\tsessionsCache.delete(key);\n\tsessionsCache.set(key, cached);\n\treturn cached.sessions;\n}\n\nfunction setCachedSessions(key: string, sessions: SessionInfo[]): void {\n\tsessionsCache.set(key, { sessions, expires: Date.now() + SESSIONS_CACHE_TTL_MS });\n\tevictLRU(sessionsCache, SESSIONS_CACHE_CAP);\n}\n\nexport interface ChatCommandContext {\n\tevent: MessageEvent;\n\tkey: string;\n\tadapter: PlatformAdapter;\n\tbridge: BridgeLike;\n\tcfg?: GatewayConfig;\n\tsession?: BridgeSession;\n\treply(text: string): Promise<void>;\n\targs: string;\n}\n\nexport interface ChatCommand {\n\tname: string;\n\taliases?: string[];\n\tdescription: string;\n\tbypassBusy: boolean;\n\tneedsSession: boolean;\n\t// biome-ignore lint/suspicious/noConfusingVoidType: handlers may return nothing; runChatCommand treats undefined as consumed.\n\thandler(ctx: ChatCommandContext): Promise<boolean | void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction prefixLines(text: string, prefix: string): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line, index) => (index === 0 ? prefix : \" \") + line)\n\t\t.join(\"\\n\");\n}\n\nfunction truncate(text: string, max: number): string {\n\tif (text.length <= max) return text;\n\treturn `${text.slice(0, max - 1)}…`;\n}\n\nfunction formatRelativeTime(date: Date): string {\n\tconst seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));\n\tif (seconds < 60) return `${seconds}s ago`;\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) return `${minutes}m ago`;\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) return `${hours}h ago`;\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d ago`;\n}\n\nfunction formatModelList(models: Model<any>[], maxLines = MAX_MODEL_LIST_LINES): string[] {\n\tif (models.length === 0) return [\"No models available.\"];\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tconst providers = [...byProvider.keys()].sort();\n\tconst lines: string[] = [];\n\tlet index = 1;\n\tfor (const provider of providers) {\n\t\tconst list = (byProvider.get(provider) ?? []).sort((a, b) => a.id.localeCompare(b.id));\n\t\tfor (const model of list) {\n\t\t\tlines.push(`${index}) ${provider}/${model.id}`);\n\t\t\tindex++;\n\t\t}\n\t}\n\tif (lines.length <= maxLines) return lines;\n\treturn [\n\t\t`${models.length} models across ${providers.length} providers.`,\n\t\t\"Use /model <provider/id> or /model <query>.\",\n\t\t...providers.map((p) => `- ${p}`),\n\t];\n}\n\nfunction groupModelsByProvider(models: Model<any>[]): Map<string, Model<any>[]> {\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tfor (const list of byProvider.values()) {\n\t\tlist.sort((a, b) => a.id.localeCompare(b.id));\n\t}\n\treturn byProvider;\n}\n\nfunction truncateModelId(id: string): string {\n\tconst lastSlash = id.lastIndexOf(\"/\");\n\tconst name = lastSlash >= 0 ? id.slice(lastSlash + 1) : id;\n\treturn truncate(name, MODEL_ID_MAX);\n}\n\nasync function refreshAndCacheModels(session: BridgeSession, key: string): Promise<Model<any>[]> {\n\tconst runtime: ModelRuntime = session.modelRuntime;\n\tawait runtime.refresh();\n\tconst models = [...(await runtime.getAvailable())].sort(\n\t\t(a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),\n\t);\n\tsetCachedModels(key, models);\n\treturn models;\n}\n\nasync function setModelAndReply(ctx: ChatCommandContext, model: Model<any>): Promise<void> {\n\tconst session = ctx.session!;\n\ttry {\n\t\tawait session.setModel(model);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (message.includes(\"No API key\")) {\n\t\t\tawait ctx.reply(`No API key for ${model.provider}.`);\n\t\t\treturn;\n\t\t}\n\t\tthrow error;\n\t}\n\tawait ctx.reply(`Model → ${model.provider}/${model.id} (thinking level re-clamped to ${session.thinkingLevel})`);\n}\n\nconst newCommand: ChatCommand = {\n\tname: \"new\",\n\taliases: [\"reset\"],\n\tdescription: \"start a fresh session for this chat\",\n\t// lunr: busy-session /new must bypass the guard (same as /stop).\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.reset(ctx.key);\n\t\tconst cfg = ctx.cfg ?? defaultGatewayConfig();\n\t\tconst owner = isGatewayOwner(ctx.event.source, cfg);\n\t\tlet ready = Boolean(conversationBinding(ctx.key)?.cwd);\n\t\tif (!ready && owner && cfg.defaultProject) {\n\t\t\ttry {\n\t\t\t\tresolveWithinRoots(cfg.defaultProject, cfg.projectRoots ?? []);\n\t\t\t\tready = true;\n\t\t\t} catch {}\n\t\t}\n\t\tawait ctx.reply(\n\t\t\tready\n\t\t\t\t? \"Session reset. Send a message to start fresh in the approved project.\"\n\t\t\t\t: owner\n\t\t\t\t\t? \"Session reset. Use /project to choose an approved project before sending a message.\"\n\t\t\t\t\t: \"Session reset. Ask the computer's gateway owner to configure this chat's project access locally.\",\n\t\t);\n\t},\n};\n\nconst undoCommand: ChatCommand = {\n\tname: \"undo\",\n\tdescription: \"rewind the session tree before the last user turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst { userText } = await ctx.bridge.undo(ctx.key);\n\t\tconst text = userText ? ` \"${truncate(userText, 60)}\"` : \"\";\n\t\tawait ctx.reply(`Undone — rewound before${text}. /redo to restore.`);\n\t},\n};\n\nconst redoCommand: ChatCommand = {\n\tname: \"redo\",\n\tdescription: \"restore the last undone turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.redo(ctx.key);\n\t\tawait ctx.reply(\"Redone.\");\n\t},\n};\n\nconst stopCommand: ChatCommand = {\n\tname: \"stop\",\n\tdescription: \"abort the running turn\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.abort(ctx.key);\n\t\tawait ctx.reply(\"Stopped.\");\n\t},\n};\n\nconst statusCommand: ChatCommand = {\n\tname: \"status\",\n\tdescription: \"session status\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst status = ctx.bridge.getStatus(ctx.key);\n\t\tconst age = status.createdAt\n\t\t\t? `${Math.max(0, Math.round((Date.now() - Date.parse(status.createdAt)) / 1000))}s`\n\t\t\t: \"no session yet\";\n\t\tawait ctx.reply(\n\t\t\t`${ctx.event.source.platform} · session age ${age} · ${status.busy ? \"busy\" : \"idle\"} · queue ${status.queueDepth}\\nProject: ${conversationBinding(ctx.key)?.cwd ?? \"not selected\"}`,\n\t\t);\n\t},\n};\n\nconst whoamiCommand: ChatCommand = {\n\tname: \"whoami\",\n\tdescription: \"your ids and session key\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst { userId, chatId } = ctx.event.source;\n\t\tawait ctx.reply(`userId: ${userId}\\nchatId: ${chatId}\\nkey: ${ctx.key}`);\n\t},\n};\n\nconst helpCommand: ChatCommand = {\n\tname: \"help\",\n\tdescription: \"this message\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.reply(formatHelpText());\n\t},\n};\n\nconst modelCommand: ChatCommand = {\n\tname: \"model\",\n\tdescription: \"list or switch models\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tif (!arg) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tconst models = cached ?? (await refreshAndCacheModels(session, ctx.key));\n\t\t\tconst byProvider = groupModelsByProvider(models);\n\t\t\tconst current = session.model;\n\t\t\tconst currentRef = current ? `${current.provider}/${current.id}` : \"\";\n\t\t\tconst providers = [...byProvider.keys()].sort();\n\t\t\tconst providerItems: PickerItem[] = providers.map((provider) => ({\n\t\t\t\tlabel: `${current?.provider === provider ? \"✓ \" : \"\"}${provider} (${byProvider.get(provider)?.length ?? 0})`,\n\t\t\t\tvalue: provider,\n\t\t\t}));\n\n\t\t\tconst pickerOpts = {\n\t\t\t\treplyTo: ctx.event.messageId,\n\t\t\t\tthreadId: ctx.event.source.threadId,\n\t\t\t};\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"model\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems: providerItems,\n\t\t\t\t\tperPage: PROVIDER_PER_PAGE,\n\t\t\t\t\ttitle: \"Pick a model provider\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tif (item.value === \"__back__\") {\n\t\t\t\t\t\t\treturn { done: false, items: providerItems, title: \"Pick a model provider\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!item.value.includes(\"/\")) {\n\t\t\t\t\t\t\tconst provider = item.value;\n\t\t\t\t\t\t\tconst list = byProvider.get(provider) ?? [];\n\t\t\t\t\t\t\tconst capped = list.slice(0, MAX_MODELS_PER_PROVIDER);\n\t\t\t\t\t\t\tconst note =\n\t\t\t\t\t\t\t\tlist.length > MAX_MODELS_PER_PROVIDER\n\t\t\t\t\t\t\t\t\t? ` — ${list.length - MAX_MODELS_PER_PROVIDER} more available`\n\t\t\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\t\tconst modelItems: PickerItem[] = [];\n\t\t\t\t\t\t\tfor (const m of capped) {\n\t\t\t\t\t\t\t\tconst ref = `${provider}/${m.id}`;\n\t\t\t\t\t\t\t\tmodelItems.push({\n\t\t\t\t\t\t\t\t\tlabel: `${currentRef === ref ? \"✓ \" : \"\"}${truncateModelId(m.id)}`,\n\t\t\t\t\t\t\t\t\tvalue: ref,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmodelItems.unshift({ label: \"◀ Back\", value: \"__back__\" });\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\t\titems: modelItems,\n\t\t\t\t\t\t\t\ttitle: `${provider} (${list.length})${note}`,\n\t\t\t\t\t\t\t\tbreadcrumbs: provider,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst exact = findExactModelReferenceMatch(item.value, models);\n\t\t\t\t\t\tif (!exact) {\n\t\t\t\t\t\t\treturn { done: true, text: \"That model is no longer available.\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait session.setModel(exact);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tif (message.includes(\"No API key\")) {\n\t\t\t\t\t\t\t\treturn { done: true, text: `No API key for ${exact.provider}.` };\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tdone: true,\n\t\t\t\t\t\t\ttext: `Model → ${exact.provider}/${exact.id} (thinking level re-clamped to ${session.thinkingLevel})`,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpickerOpts,\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst currentText = `${current?.provider ?? \"none\"}/${current?.id ?? \"none\"}`;\n\t\t\t\tconst lines = formatModelList(models);\n\t\t\t\tawait ctx.reply([`Current: ${currentText}`, ...lines, \"Use /model <n> or /model <provider/id>\"].join(\"\\n\"));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /model to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait setModelAndReply(ctx, cached[numeric - 1]);\n\t\t\treturn;\n\t\t}\n\n\t\tconst models = getCachedModels(ctx.key) ?? (await refreshAndCacheModels(session, ctx.key));\n\t\tconst exact = findExactModelReferenceMatch(arg, models);\n\t\tif (exact) {\n\t\t\tawait setModelAndReply(ctx, exact);\n\t\t\treturn;\n\t\t}\n\n\t\tconst query = arg.toLowerCase();\n\t\tconst matches = models.filter(\n\t\t\t(m) =>\n\t\t\t\tm.id.toLowerCase().includes(query) ||\n\t\t\t\t`${m.provider}/${m.id}`.toLowerCase().includes(query) ||\n\t\t\t\t(m.name?.toLowerCase().includes(query) ?? false),\n\t\t);\n\t\tif (matches.length === 0) {\n\t\t\tawait ctx.reply(`No model matches \"${arg}\".`);\n\t\t\treturn;\n\t\t}\n\t\tif (matches.length > 1) {\n\t\t\tawait ctx.reply(formatModelList(matches).join(\"\\n\"));\n\t\t\treturn;\n\t\t}\n\t\tawait setModelAndReply(ctx, matches[0]);\n\t},\n};\n\nconst sessionsCommand: ChatCommand = {\n\tname: \"sessions\",\n\tdescription: \"list or switch sessions\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tconst currentFile = session.sessionManager?.getSessionFile();\n\t\tconst cwd = session.sessionManager?.getCwd() ?? conversationBinding(ctx.key)?.cwd;\n\t\tif (!cwd) throw new Error(\"Select a project before listing sessions.\");\n\n\t\tif (!arg) {\n\t\t\tconst all = await SessionManager.list(cwd);\n\t\t\tall.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t\tconst sessions = all.slice(0, 10);\n\t\t\tsetCachedSessions(ctx.key, sessions);\n\t\t\tconst items: PickerItem[] = sessions.map((s) => ({\n\t\t\t\tlabel: `${s.path === currentFile ? \"(current) \" : \"\"}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || \"(empty)\"}`,\n\t\t\t\tvalue: s.path,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"sessions\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: 8,\n\t\t\t\t\ttitle: \"Pick a session\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tawait ctx.bridge.switchSession(ctx.key, item.value);\n\t\t\t\t\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\t\t\t\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\t\t\t\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\t\t\t\t\treturn { done: true, text: `Switched to \"${name}\" (${count} msgs). History continues here.` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst lines = sessions.map((s, i) => {\n\t\t\t\t\tconst marker = s.path === currentFile ? \" (current)\" : \"\";\n\t\t\t\t\tconst label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || \"(empty)\";\n\t\t\t\t\treturn `${i + 1}) ${label}${marker} · ${formatRelativeTime(s.modified)} · ${s.messageCount} msgs`;\n\t\t\t\t});\n\t\t\t\tawait ctx.reply([`Sessions for ${cwd}`, ...lines, \"Use /sessions <n> or /sessions <id-prefix>\"].join(\"\\n\"));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tlet path: string | undefined;\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tconst cached = getCachedSessions(ctx.key);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /sessions to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = cached[numeric - 1].path;\n\t\t} else {\n\t\t\tconst pool = cached ?? (await SessionManager.list(cwd));\n\t\t\tconst matches = pool.filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));\n\t\t\tif (matches.length === 0) {\n\t\t\t\tawait ctx.reply(`No session matches \"${arg}\".`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matches.length > 1) {\n\t\t\t\tawait ctx.reply(`Multiple sessions match \"${arg}\" — use a longer prefix.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = matches[0].path;\n\t\t}\n\n\t\tif (!path) {\n\t\t\tawait ctx.reply(\"No session selected.\");\n\t\t\treturn;\n\t\t}\n\n\t\tawait ctx.bridge.switchSession(ctx.key, path);\n\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\tawait ctx.reply(`Switched to \"${name}\" (${count} msgs). History continues here.`);\n\t},\n};\n\nconst titleCommand: ChatCommand = {\n\tname: \"title\",\n\taliases: [\"name\"],\n\tdescription: \"name the current session\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst name = ctx.args.trim();\n\t\tif (!name) {\n\t\t\tconst current = session.sessionManager?.getSessionName();\n\t\t\tawait ctx.reply(current ? `Session title: ${current}` : \"(unnamed)\");\n\t\t\treturn;\n\t\t}\n\t\tsession.setSessionName(name);\n\t\tconst readBack = session.sessionManager?.getSessionName() ?? name;\n\t\tawait ctx.reply(`Session titled \"${readBack}\".`);\n\t},\n};\n\nconst contextCommand: ChatCommand = {\n\tname: \"context\",\n\tdescription: \"estimated context window breakdown\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst model = session.model;\n\t\tif (!model) {\n\t\t\tawait ctx.reply(\"No model selected.\");\n\t\t\treturn;\n\t\t}\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) {\n\t\t\tawait ctx.reply(\"Context window unknown for this model.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst tools: ToolDefinition[] = session\n\t\t\t.getActiveToolNames()\n\t\t\t.map((name) => session.getToolDefinition(name))\n\t\t\t.filter((d): d is ToolDefinition => d !== undefined);\n\n\t\tconst breakdown = computeContextBreakdown({\n\t\t\tsystemPrompt: session.systemPrompt,\n\t\t\ttools,\n\t\t\tmessages: session.messages,\n\t\t\tcontextWindow,\n\t\t});\n\n\t\tconst usage = session.getContextUsage();\n\t\tconst tokens = usage?.tokens ?? breakdown.total;\n\t\tconst percent = usage?.percent ?? (contextWindow > 0 ? (tokens / contextWindow) * 100 : 0);\n\n\t\tconst messageTokens =\n\t\t\tbreakdown.user +\n\t\t\tbreakdown.assistantText +\n\t\t\tbreakdown.thinking +\n\t\t\tbreakdown.toolCalls +\n\t\t\tbreakdown.toolResults +\n\t\t\tbreakdown.summaries;\n\n\t\tconst stats = session.getSessionStats();\n\t\tawait ctx.reply(\n\t\t\t[\n\t\t\t\t`Context: ${tokens.toLocaleString(\"en-US\")} / ${contextWindow.toLocaleString(\"en-US\")} tokens (${Math.round(percent)}%)`,\n\t\t\t\t` system ${breakdown.systemPrompt.toLocaleString(\"en-US\")} · tools ${breakdown.toolDefinitions.toLocaleString(\"en-US\")} · messages ${messageTokens.toLocaleString(\"en-US\")}`,\n\t\t\t\t`Session: ${stats.totalMessages} messages · ${stats.userMessages} turns · model ${model.provider}/${model.id}`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t},\n};\n\nconst compactCommand: ChatCommand = {\n\tname: \"compact\",\n\tdescription: \"summarize and compress session context\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst instructions = ctx.args.trim() || undefined;\n\t\ttry {\n\t\t\tconst result = await session.compact(instructions);\n\t\t\tconst before = result.tokensBefore.toLocaleString(\"en-US\");\n\t\t\tconst after =\n\t\t\t\tresult.estimatedTokensAfter !== undefined\n\t\t\t\t\t? `~${result.estimatedTokensAfter.toLocaleString(\"en-US\")}`\n\t\t\t\t\t: \"~?\";\n\t\t\tawait ctx.reply(`Compacted: ${before} → ${after} tokens.`);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tif (\n\t\t\t\tmessage.includes(\"Nothing to compact\") ||\n\t\t\t\tmessage.includes(\"Already compacted\") ||\n\t\t\t\tmessage.includes(\"No model selected\")\n\t\t\t) {\n\t\t\t\tawait ctx.reply(message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t},\n};\n\nconst thinkingCommand: ChatCommand = {\n\tname: \"thinking\",\n\taliases: [\"effort\", \"reasoning\"],\n\tdescription: \"get or set the thinking level\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst levels = session.getAvailableThinkingLevels();\n\t\tif (!session.supportsThinking()) {\n\t\t\tawait ctx.reply(\n\t\t\t\t`Model ${session.model?.provider ?? \"none\"}/${session.model?.id ?? \"none\"} doesn't support thinking.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst arg = ctx.args.trim().toLowerCase() as ThinkingLevel;\n\t\tif (!arg) {\n\t\t\tconst current = session.thinkingLevel;\n\t\t\tconst items: PickerItem[] = levels.map((level) => ({\n\t\t\t\tlabel: `${level === current ? \"✓ \" : \"\"}${level}`,\n\t\t\t\tvalue: level,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"thinking\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: levels.length,\n\t\t\t\t\ttitle: \"Pick a thinking level\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tconst level = item.value as ThinkingLevel;\n\t\t\t\t\t\tsession.setThinkingLevel(level);\n\t\t\t\t\t\treturn { done: true, text: `Thinking → ${level}` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tawait ctx.reply(`Level: ${current} — available: ${levels.join(\", \")}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!levels.includes(arg)) {\n\t\t\tawait ctx.reply(`Invalid level. Available: ${levels.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\t\tsession.setThinkingLevel(arg);\n\t\tawait ctx.reply(`Thinking → ${arg}`);\n\t},\n};\n\nexport const CHAT_COMMANDS: ChatCommand[] = [\n\tnewCommand,\n\tundoCommand,\n\tredoCommand,\n\tstopCommand,\n\tstatusCommand,\n\twhoamiCommand,\n\thelpCommand,\n\tmodelCommand,\n\tsessionsCommand,\n\ttitleCommand,\n\tcontextCommand,\n\tcompactCommand,\n\tthinkingCommand,\n];\n\n/** Platform menu specs (e.g. Telegram setMyCommands): canonical names only, aliases skipped. */\nexport function botCommandSpecs(): { name: string; description: string }[] {\n\treturn [\n\t\t...new Map(\n\t\t\t[\n\t\t\t\t{ name: \"start\", description: \"Check gateway readiness\" },\n\t\t\t\t...CHAT_COMMANDS,\n\t\t\t\t...MOBILE_COMMANDS,\n\t\t\t\t...FORWARDED_COMMANDS,\n\t\t\t].map((c) => [c.name, { name: c.name, description: c.description }]),\n\t\t).values(),\n\t];\n}\n\nexport function formatHelpText(): string {\n\tconst lines = [\"lunR gateway commands:\", \"/start — check gateway readiness\"];\n\tfor (const cmd of CHAT_COMMANDS) {\n\t\tconst names = [cmd.name, ...(cmd.aliases ?? [])].map((a) => `/${a}`).join(\" | \");\n\t\tlines.push(`${names} — ${cmd.description}`);\n\t}\n\tfor (const cmd of botCommandSpecs()) {\n\t\tif (!CHAT_COMMANDS.some((c) => c.name === cmd.name)) lines.push(`/${cmd.name} · ${cmd.description}`);\n\t}\n\tlines.push(\"\");\n\tlines.push(\"Tap to pick: /model, /thinking, /sessions (run without args to see buttons).\");\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Run a chat command with busy/session guards and error → \"⚠ one-line\".\n * Returns true when the event was consumed by the command, false when the\n * router should continue when a command mutates the event and wants a normal turn.\n */\nexport async function runChatCommand(cmd: ChatCommand, ctx: ChatCommandContext): Promise<boolean> {\n\tif (!cmd.bypassBusy && ctx.bridge.getStatus(ctx.key).busy) {\n\t\tawait ctx.reply(\"⏳ busy — /stop first or wait\");\n\t\treturn true;\n\t}\n\tif (cmd.needsSession) {\n\t\tconst session = await ctx.bridge.getSession(ctx.key, cmd.name === \"model\");\n\t\tif (!session) {\n\t\t\tawait ctx.reply(\"No session yet. Select an approved project with /project first.\");\n\t\t\treturn true;\n\t\t}\n\t\tctx.session = session;\n\t}\n\ttry {\n\t\tconst settingsManager = ctx.session?.settingsManager;\n\t\tconst consumed = settingsManager\n\t\t\t? await runtimeScope.run(\n\t\t\t\t\t{ settingsManager, modelRuntime: ctx.session?.modelRuntime, thinking: () => ctx.session!.thinkingLevel },\n\t\t\t\t\t() => cmd.handler(ctx),\n\t\t\t\t)\n\t\t\t: await cmd.handler(ctx);\n\t\treturn consumed ?? true;\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tawait ctx.adapter.send(ctx.event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: ctx.event.messageId,\n\t\t\tthreadId: ctx.event.source.threadId,\n\t\t});\n\t\treturn true;\n\t}\n}\n\n/** Reply helper used by the router to build the ChatCommandContext. */\nexport async function sendCommandReply(adapter: PlatformAdapter, event: MessageEvent, text: string): Promise<void> {\n\tawait adapter.send(event.source.chatId, prefixLines(text, \"\"), {\n\t\treplyTo: event.messageId,\n\t\tthreadId: event.source.threadId,\n\t});\n}\n"]}
|