@ashx-j/lunr 0.2.23 → 0.2.24
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 +14 -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 +1 -1
- package/dist/gateway/adapters/discord.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/mobile-commands.d.ts.map +1 -1
- package/dist/gateway/mobile-commands.js +12 -11
- package/dist/gateway/mobile-commands.js.map +1 -1
- package/dist/gateway/router.d.ts.map +1 -1
- package/dist/gateway/router.js +1 -1
- package/dist/gateway/router.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-IEF7LQWD.js} +8 -9
- package/dist/node-runtime/{builtin-extensions-QVCXNQT4.js → builtin-extensions-EBAWTH4M.js} +3 -3
- package/dist/node-runtime/{chunk-XLTL257W.js → chunk-2KIPT4HJ.js} +5 -1
- package/dist/node-runtime/{chunk-YHQ3NBB3.js → chunk-5Q3EHOHI.js} +17 -17
- package/dist/node-runtime/{chunk-TQGPP4SC.js → chunk-CDVHZOOK.js} +1 -1
- package/dist/node-runtime/{chunk-DQMGU675.js → chunk-DE2JB6J5.js} +20 -17
- package/dist/node-runtime/{chunk-YJMMG7C6.js → chunk-DSIKTUPW.js} +1 -1
- package/dist/node-runtime/{chunk-TVJUOJZJ.js → chunk-F4FLTQKV.js} +1 -1
- package/dist/node-runtime/{chunk-GMHJCCEN.js → chunk-FU6OODRZ.js} +4 -1
- 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-DD5YWFSJ.js → chunk-IGV54Q5V.js} +2 -2
- package/dist/node-runtime/{chunk-RIR5EDDJ.js → chunk-K5AGHH57.js} +17 -12
- package/dist/node-runtime/{chunk-GS66LV4M.js → chunk-KJ6DWS3K.js} +4 -4
- package/dist/node-runtime/{chunk-LLUIPGDR.js → chunk-L3LX4SNR.js} +108 -75
- 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-W64JC4E7.js → chunk-OF7PWMG4.js} +2 -2
- package/dist/node-runtime/{chunk-CSUZVNYE.js → chunk-OVX6X377.js} +10 -6
- package/dist/node-runtime/{chunk-X3URCYCU.js → chunk-QM43FEAE.js} +14 -14
- package/dist/node-runtime/{chunk-QAZOULTU.js → chunk-RORY2HMJ.js} +39 -5
- package/dist/node-runtime/{chunk-EKVFP6YF.js → chunk-WHLRAGUQ.js} +13 -12
- package/dist/node-runtime/{chunk-HPUP7E4T.js → chunk-Y73WPE6S.js} +16 -16
- package/dist/node-runtime/{chunk-LFZDVKSP.js → chunk-YVI6L6LU.js} +5 -5
- 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-7BG7VVTS.js} +3 -3
- package/dist/node-runtime/{commands-KAMO2T6P.js → commands-NZLSOHPE.js} +2 -2
- package/dist/node-runtime/{commands-XDCFGIGW.js → commands-QFDKNQTL.js} +7 -7
- 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-TWR3N65G.js} +12 -12
- package/dist/node-runtime/index.js +16 -17
- package/dist/node-runtime/{init-X2LIUB2H.js → init-HS6NQXSK.js} +2 -2
- package/dist/node-runtime/{install-cli-3BO4MMQ6.js → install-cli-VAP6KBFN.js} +1 -1
- package/dist/node-runtime/{interactive-mode-4MFRKAHG.js → interactive-mode-6J5VZ2TT.js} +10 -11
- package/dist/node-runtime/{interactive-view-TUZHCQM3.js → interactive-view-NGFAMHPW.js} +2 -2
- package/dist/node-runtime/{lunr-browser-RYFIIVJN.js → lunr-browser-J5ZZYVGM.js} +1 -1
- package/dist/node-runtime/{lunr-cron-YWXEPZMQ.js → lunr-cron-CXOL37AL.js} +5 -5
- package/dist/node-runtime/main.js +14 -15
- package/dist/node-runtime/{mobile-commands-LVHDAUDP.js → mobile-commands-VTBM5DSZ.js} +4 -4
- 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-ZLW5DZXF.js} +3 -4
- 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-XPCLLKIM.js} +23 -13
- package/dist/node-runtime/{proxy-modes-OSQCWRBI.js → proxy-modes-LGEUYD7R.js} +3 -3
- package/dist/node-runtime/rpc-entry.js +14 -15
- package/dist/node-runtime/{runtime-bridges-3XUQ46XW.js → runtime-bridges-QYXUX2TG.js} +8 -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-WCQDUVQB.js} +7 -4
- package/dist/node-runtime/{setup-5SR2J35A.js → setup-YRBKH2LD.js} +2 -2
- package/dist/node-runtime/{subagent-executor-M5VWB3D3.js → subagent-executor-OTNISDQU.js} +40 -7
- package/dist/node-runtime/{update-cli-UVKJWRRJ.js → update-cli-VBR2777K.js} +1 -2
- 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 +15 -14
- 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
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* lunR: parent-delegated children inherit an explicit permission level, not a
|
|
3
3
|
* named agent role. The parent process resolves the requested level against
|
|
4
4
|
* its own mode, then snapshots that resolved child permission into the child
|
|
5
|
-
* environment. Full maps to child runtime Auto; read-only maps to
|
|
6
|
-
*
|
|
7
|
-
* Ordinary `lunr -p`, cron, and gateway headless sessions are not children and
|
|
8
|
-
* stay fail-closed.
|
|
5
|
+
* environment. Full maps to child runtime Auto; read-only maps to Read-only.
|
|
9
6
|
*/
|
|
10
7
|
import { type PermissionMode } from "./permissions.ts";
|
|
11
8
|
export declare const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
@@ -13,7 +10,7 @@ export declare const SUBAGENT_CHILD_PERMISSION_ENV = "PI_SUBAGENT_CHILD_PERMISSI
|
|
|
13
10
|
/** @deprecated Replaced by SUBAGENT_CHILD_PERMISSION_ENV. Kept so leftover env is ignored. */
|
|
14
11
|
export declare const SUBAGENT_PARENT_PERMISSION_MODE_ENV = "PI_SUBAGENT_PARENT_PERMISSION_MODE";
|
|
15
12
|
export type ChildPermission = "full" | "read-only";
|
|
16
|
-
export declare const
|
|
13
|
+
export declare const READ_ONLY_WRITE_SPAWN_ERROR = "Cannot launch a full-access child in read-only mode. Relaunch with permissions: \"read-only\".";
|
|
17
14
|
export declare function isSubagentChildProcess(env?: NodeJS.ProcessEnv): boolean;
|
|
18
15
|
export declare function parseParentPermissionMode(value: string | undefined): PermissionMode | undefined;
|
|
19
16
|
export declare function parseChildPermission(value: string | undefined): ChildPermission | undefined;
|
|
@@ -35,11 +32,10 @@ export interface RejectedChildPermissions {
|
|
|
35
32
|
* Resolve the child's permission level before spawn.
|
|
36
33
|
*
|
|
37
34
|
* Parent mode Requested Result
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Plan full / omitted Reject (omission resolves to full)
|
|
35
|
+
* YOLO/Auto omitted/full Full
|
|
36
|
+
* YOLO/Auto read-only Read-only
|
|
37
|
+
* Read-only read-only Read-only
|
|
38
|
+
* Read-only full / omitted Reject (omission resolves to full)
|
|
43
39
|
*/
|
|
44
40
|
export declare function resolveChildPermissions(parentMode: string | undefined, requested?: string): ResolvedChildPermissions | RejectedChildPermissions;
|
|
45
41
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-permission-inherit.d.ts","sourceRoot":"","sources":["../../src/core/subagent-permission-inherit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"subagent-permission-inherit.d.ts","sourceRoot":"","sources":["../../src/core/subagent-permission-inherit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAqB,KAAK,cAAc,EAAoB,MAAM,kBAAkB,CAAC;AAE5F,eAAO,MAAM,kBAAkB,sBAAsB,CAAC;AACtD,eAAO,MAAM,6BAA6B,iCAAiC,CAAC;AAC5E,8FAA8F;AAC9F,eAAO,MAAM,mCAAmC,uCAAuC,CAAC;AAExF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,CAAC;AAEnD,eAAO,MAAM,2BAA2B,mGACuD,CAAC;AAEhG,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEpF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAK/F;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAG3F;AAED,+FAA+F;AAC/F,wBAAgB,+BAA+B,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAEnF;AAED,wBAAgB,iCAAiC,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,CAErG;AAED,wBAAgB,4BAA4B,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,cAAc,CAE/E;AAED,MAAM,WAAW,wBAAwB;IACxC,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,eAAe,CAAC;IAC3B,SAAS,EAAE,eAAe,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACxC,EAAE,EAAE,KAAK,CAAC;IACV,SAAS,EAAE,eAAe,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACtC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,SAAS,CAAC,EAAE,MAAM,GAChB,wBAAwB,GAAG,wBAAwB,CAMrD;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,cAAc,GAAG,SAAS,CAKlH","sourcesContent":["/**\n * lunR: parent-delegated children inherit an explicit permission level, not a\n * named agent role. The parent process resolves the requested level against\n * its own mode, then snapshots that resolved child permission into the child\n * environment. Full maps to child runtime Auto; read-only maps to Read-only.\n */\n\nimport { getPermissionMode, type PermissionMode, resetPermissions } from \"./permissions.ts\";\n\nexport const SUBAGENT_CHILD_ENV = \"PI_SUBAGENT_CHILD\";\nexport const SUBAGENT_CHILD_PERMISSION_ENV = \"PI_SUBAGENT_CHILD_PERMISSION\";\n/** @deprecated Replaced by SUBAGENT_CHILD_PERMISSION_ENV. Kept so leftover env is ignored. */\nexport const SUBAGENT_PARENT_PERMISSION_MODE_ENV = \"PI_SUBAGENT_PARENT_PERMISSION_MODE\";\n\nexport type ChildPermission = \"full\" | \"read-only\";\n\nexport const READ_ONLY_WRITE_SPAWN_ERROR =\n\t'Cannot launch a full-access child in read-only mode. Relaunch with permissions: \"read-only\".';\n\nexport function isSubagentChildProcess(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn env[SUBAGENT_CHILD_ENV] === \"1\";\n}\n\nexport function parseParentPermissionMode(value: string | undefined): PermissionMode | undefined {\n\tif (value === \"yolo\" || value === \"auto\" || value === \"read-only\") return value;\n\tif (value === \"plan\") return \"read-only\";\n\tif (value === \"manual\") return \"yolo\";\n\treturn undefined;\n}\n\nexport function parseChildPermission(value: string | undefined): ChildPermission | undefined {\n\tif (value === \"full\" || value === \"read-only\") return value;\n\treturn undefined;\n}\n\n/** Omitted permissions resolve to full. Unknown values are rejected by the schema/executor. */\nexport function resolveRequestedChildPermission(requested?: string): ChildPermission {\n\treturn requested === \"read-only\" ? \"read-only\" : \"full\";\n}\n\nexport function resolveChildRuntimePermissionMode(childPermission: string | undefined): PermissionMode {\n\treturn parseChildPermission(childPermission) === \"read-only\" ? \"read-only\" : \"auto\";\n}\n\nexport function snapshotParentPermissionMode(sessionId?: string): PermissionMode {\n\treturn getPermissionMode(sessionId);\n}\n\nexport interface ResolvedChildPermissions {\n\tok: true;\n\trequested: ChildPermission;\n\teffective: ChildPermission;\n}\n\nexport interface RejectedChildPermissions {\n\tok: false;\n\trequested: ChildPermission;\n\terror: string;\n}\n\n/**\n * Resolve the child's permission level before spawn.\n *\n * Parent mode Requested Result\n * YOLO/Auto omitted/full Full\n * YOLO/Auto read-only Read-only\n * Read-only read-only Read-only\n * Read-only full / omitted Reject (omission resolves to full)\n */\nexport function resolveChildPermissions(\n\tparentMode: string | undefined,\n\trequested?: string,\n): ResolvedChildPermissions | RejectedChildPermissions {\n\tconst requestedPermission = resolveRequestedChildPermission(requested);\n\tif (parseParentPermissionMode(parentMode) === \"read-only\" && requestedPermission === \"full\") {\n\t\treturn { ok: false, requested: requestedPermission, error: READ_ONLY_WRITE_SPAWN_ERROR };\n\t}\n\treturn { ok: true, requested: requestedPermission, effective: requestedPermission };\n}\n\n/**\n * Apply the resolved child permission before any tool call. No-op for non-child\n * processes so `lunr -p` stays fail-closed.\n */\nexport function applyInheritedSubagentPermissions(env: NodeJS.ProcessEnv = process.env): PermissionMode | undefined {\n\tif (!isSubagentChildProcess(env)) return undefined;\n\tconst childMode = resolveChildRuntimePermissionMode(env[SUBAGENT_CHILD_PERMISSION_ENV]);\n\tresetPermissions(childMode);\n\treturn childMode;\n}\n"]}
|
|
@@ -2,23 +2,24 @@
|
|
|
2
2
|
* lunR: parent-delegated children inherit an explicit permission level, not a
|
|
3
3
|
* named agent role. The parent process resolves the requested level against
|
|
4
4
|
* its own mode, then snapshots that resolved child permission into the child
|
|
5
|
-
* environment. Full maps to child runtime Auto; read-only maps to
|
|
6
|
-
*
|
|
7
|
-
* Ordinary `lunr -p`, cron, and gateway headless sessions are not children and
|
|
8
|
-
* stay fail-closed.
|
|
5
|
+
* environment. Full maps to child runtime Auto; read-only maps to Read-only.
|
|
9
6
|
*/
|
|
10
7
|
import { getPermissionMode, resetPermissions } from "./permissions.js";
|
|
11
8
|
export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
12
9
|
export const SUBAGENT_CHILD_PERMISSION_ENV = "PI_SUBAGENT_CHILD_PERMISSION";
|
|
13
10
|
/** @deprecated Replaced by SUBAGENT_CHILD_PERMISSION_ENV. Kept so leftover env is ignored. */
|
|
14
11
|
export const SUBAGENT_PARENT_PERMISSION_MODE_ENV = "PI_SUBAGENT_PARENT_PERMISSION_MODE";
|
|
15
|
-
export const
|
|
12
|
+
export const READ_ONLY_WRITE_SPAWN_ERROR = 'Cannot launch a full-access child in read-only mode. Relaunch with permissions: "read-only".';
|
|
16
13
|
export function isSubagentChildProcess(env = process.env) {
|
|
17
14
|
return env[SUBAGENT_CHILD_ENV] === "1";
|
|
18
15
|
}
|
|
19
16
|
export function parseParentPermissionMode(value) {
|
|
20
|
-
if (value === "
|
|
17
|
+
if (value === "yolo" || value === "auto" || value === "read-only")
|
|
21
18
|
return value;
|
|
19
|
+
if (value === "plan")
|
|
20
|
+
return "read-only";
|
|
21
|
+
if (value === "manual")
|
|
22
|
+
return "yolo";
|
|
22
23
|
return undefined;
|
|
23
24
|
}
|
|
24
25
|
export function parseChildPermission(value) {
|
|
@@ -31,7 +32,7 @@ export function resolveRequestedChildPermission(requested) {
|
|
|
31
32
|
return requested === "read-only" ? "read-only" : "full";
|
|
32
33
|
}
|
|
33
34
|
export function resolveChildRuntimePermissionMode(childPermission) {
|
|
34
|
-
return parseChildPermission(childPermission) === "read-only" ? "
|
|
35
|
+
return parseChildPermission(childPermission) === "read-only" ? "read-only" : "auto";
|
|
35
36
|
}
|
|
36
37
|
export function snapshotParentPermissionMode(sessionId) {
|
|
37
38
|
return getPermissionMode(sessionId);
|
|
@@ -40,16 +41,15 @@ export function snapshotParentPermissionMode(sessionId) {
|
|
|
40
41
|
* Resolve the child's permission level before spawn.
|
|
41
42
|
*
|
|
42
43
|
* Parent mode Requested Result
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* Plan full / omitted Reject (omission resolves to full)
|
|
44
|
+
* YOLO/Auto omitted/full Full
|
|
45
|
+
* YOLO/Auto read-only Read-only
|
|
46
|
+
* Read-only read-only Read-only
|
|
47
|
+
* Read-only full / omitted Reject (omission resolves to full)
|
|
48
48
|
*/
|
|
49
49
|
export function resolveChildPermissions(parentMode, requested) {
|
|
50
50
|
const requestedPermission = resolveRequestedChildPermission(requested);
|
|
51
|
-
if (parseParentPermissionMode(parentMode) === "
|
|
52
|
-
return { ok: false, requested: requestedPermission, error:
|
|
51
|
+
if (parseParentPermissionMode(parentMode) === "read-only" && requestedPermission === "full") {
|
|
52
|
+
return { ok: false, requested: requestedPermission, error: READ_ONLY_WRITE_SPAWN_ERROR };
|
|
53
53
|
}
|
|
54
54
|
return { ok: true, requested: requestedPermission, effective: requestedPermission };
|
|
55
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-permission-inherit.js","sourceRoot":"","sources":["../../src/core/subagent-permission-inherit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"subagent-permission-inherit.js","sourceRoot":"","sources":["../../src/core/subagent-permission-inherit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,iBAAiB,EAAuB,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAE5F,MAAM,CAAC,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AACtD,MAAM,CAAC,MAAM,6BAA6B,GAAG,8BAA8B,CAAC;AAC5E,8FAA8F;AAC9F,MAAM,CAAC,MAAM,mCAAmC,GAAG,oCAAoC,CAAC;AAIxF,MAAM,CAAC,MAAM,2BAA2B,GACvC,8FAA8F,CAAC;AAEhG,MAAM,UAAU,sBAAsB,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG,EAAW;IACrF,OAAO,GAAG,CAAC,kBAAkB,CAAC,KAAK,GAAG,CAAC;AAAA,CACvC;AAED,MAAM,UAAU,yBAAyB,CAAC,KAAyB,EAA8B;IAChG,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAChF,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,WAAW,CAAC;IACzC,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACtC,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAyB,EAA+B;IAC5F,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAC5D,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,+FAA+F;AAC/F,MAAM,UAAU,+BAA+B,CAAC,SAAkB,EAAmB;IACpF,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;AAAA,CACxD;AAED,MAAM,UAAU,iCAAiC,CAAC,eAAmC,EAAkB;IACtG,OAAO,oBAAoB,CAAC,eAAe,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;AAAA,CACpF;AAED,MAAM,UAAU,4BAA4B,CAAC,SAAkB,EAAkB;IAChF,OAAO,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAAA,CACpC;AAcD;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACtC,UAA8B,EAC9B,SAAkB,EACoC;IACtD,MAAM,mBAAmB,GAAG,+BAA+B,CAAC,SAAS,CAAC,CAAC;IACvE,IAAI,yBAAyB,CAAC,UAAU,CAAC,KAAK,WAAW,IAAI,mBAAmB,KAAK,MAAM,EAAE,CAAC;QAC7F,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;IAC1F,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,mBAAmB,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC;AAAA,CACpF;AAED;;;GAGG;AACH,MAAM,UAAU,iCAAiC,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG,EAA8B;IACnH,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACnD,MAAM,SAAS,GAAG,iCAAiC,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC,CAAC;IACxF,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC5B,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["/**\n * lunR: parent-delegated children inherit an explicit permission level, not a\n * named agent role. The parent process resolves the requested level against\n * its own mode, then snapshots that resolved child permission into the child\n * environment. Full maps to child runtime Auto; read-only maps to Read-only.\n */\n\nimport { getPermissionMode, type PermissionMode, resetPermissions } from \"./permissions.ts\";\n\nexport const SUBAGENT_CHILD_ENV = \"PI_SUBAGENT_CHILD\";\nexport const SUBAGENT_CHILD_PERMISSION_ENV = \"PI_SUBAGENT_CHILD_PERMISSION\";\n/** @deprecated Replaced by SUBAGENT_CHILD_PERMISSION_ENV. Kept so leftover env is ignored. */\nexport const SUBAGENT_PARENT_PERMISSION_MODE_ENV = \"PI_SUBAGENT_PARENT_PERMISSION_MODE\";\n\nexport type ChildPermission = \"full\" | \"read-only\";\n\nexport const READ_ONLY_WRITE_SPAWN_ERROR =\n\t'Cannot launch a full-access child in read-only mode. Relaunch with permissions: \"read-only\".';\n\nexport function isSubagentChildProcess(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn env[SUBAGENT_CHILD_ENV] === \"1\";\n}\n\nexport function parseParentPermissionMode(value: string | undefined): PermissionMode | undefined {\n\tif (value === \"yolo\" || value === \"auto\" || value === \"read-only\") return value;\n\tif (value === \"plan\") return \"read-only\";\n\tif (value === \"manual\") return \"yolo\";\n\treturn undefined;\n}\n\nexport function parseChildPermission(value: string | undefined): ChildPermission | undefined {\n\tif (value === \"full\" || value === \"read-only\") return value;\n\treturn undefined;\n}\n\n/** Omitted permissions resolve to full. Unknown values are rejected by the schema/executor. */\nexport function resolveRequestedChildPermission(requested?: string): ChildPermission {\n\treturn requested === \"read-only\" ? \"read-only\" : \"full\";\n}\n\nexport function resolveChildRuntimePermissionMode(childPermission: string | undefined): PermissionMode {\n\treturn parseChildPermission(childPermission) === \"read-only\" ? \"read-only\" : \"auto\";\n}\n\nexport function snapshotParentPermissionMode(sessionId?: string): PermissionMode {\n\treturn getPermissionMode(sessionId);\n}\n\nexport interface ResolvedChildPermissions {\n\tok: true;\n\trequested: ChildPermission;\n\teffective: ChildPermission;\n}\n\nexport interface RejectedChildPermissions {\n\tok: false;\n\trequested: ChildPermission;\n\terror: string;\n}\n\n/**\n * Resolve the child's permission level before spawn.\n *\n * Parent mode Requested Result\n * YOLO/Auto omitted/full Full\n * YOLO/Auto read-only Read-only\n * Read-only read-only Read-only\n * Read-only full / omitted Reject (omission resolves to full)\n */\nexport function resolveChildPermissions(\n\tparentMode: string | undefined,\n\trequested?: string,\n): ResolvedChildPermissions | RejectedChildPermissions {\n\tconst requestedPermission = resolveRequestedChildPermission(requested);\n\tif (parseParentPermissionMode(parentMode) === \"read-only\" && requestedPermission === \"full\") {\n\t\treturn { ok: false, requested: requestedPermission, error: READ_ONLY_WRITE_SPAWN_ERROR };\n\t}\n\treturn { ok: true, requested: requestedPermission, effective: requestedPermission };\n}\n\n/**\n * Apply the resolved child permission before any tool call. No-op for non-child\n * processes so `lunr -p` stays fail-closed.\n */\nexport function applyInheritedSubagentPermissions(env: NodeJS.ProcessEnv = process.env): PermissionMode | undefined {\n\tif (!isSubagentChildProcess(env)) return undefined;\n\tconst childMode = resolveChildRuntimePermissionMode(env[SUBAGENT_CHILD_PERMISSION_ENV]);\n\tresetPermissions(childMode);\n\treturn childMode;\n}\n"]}
|
|
@@ -7,6 +7,8 @@ export interface BuildSystemPromptOptions {
|
|
|
7
7
|
customPrompt?: string;
|
|
8
8
|
/** Current model in provider/id form. */
|
|
9
9
|
modelSlug?: string;
|
|
10
|
+
/** Whether the default prompt encourages autonomous delegation. */
|
|
11
|
+
automaticSubagentDelegation?: boolean;
|
|
10
12
|
/** Active tools; used to decide whether read-dependent skill metadata is visible. */
|
|
11
13
|
selectedTools?: string[];
|
|
12
14
|
/** Legacy extension metadata retained for API compatibility; tool definitions are sent separately. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,sGAAsG;IACtG,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,sGAAsG;IACtG,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,oGAAoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;CACjB;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CA4H3E","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.ts\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.ts\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Current model in provider/id form. */\n\tmodelSlug?: string;\n\t/** Whether the default prompt encourages autonomous delegation. */\n\tautomaticSubagentDelegation?: boolean;\n\t/** Active tools; used to decide whether read-dependent skill metadata is visible. */\n\tselectedTools?: string[];\n\t/** Legacy extension metadata retained for API compatibility; tool definitions are sent separately. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Legacy extension metadata retained for API compatibility; not appended to the default prompt. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n}\n\n/** Build the base system prompt and append configured context. */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tmodelSlug,\n\t\tautomaticSubagentDelegation = true,\n\t\tselectedTools,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst promptCwd = cwd.replace(/\\\\/g, \"/\");\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n<project_context>\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `<project_instructions path=\"${filePath}\">\\n${content}\\n</project_instructions>\\n\\n`;\n\t\t\t}\n\t\t\tprompt += \"</project_context>\\n\";\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst customPromptHasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (customPromptHasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\tconst currentModel = modelSlug?.trim() || \"no model selected\";\n\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\tconst todoGuideline =\n\t\t!selectedTools || selectedTools.includes(\"todo\")\n\t\t\t? \"- Use todo for meaningful multi-step work. Every update must send the complete list, with exactly one item in progress at a time.\\n\"\n\t\t\t: \"\";\n\tconst delegationBehavior = automaticSubagentDelegation\n\t\t? \"- Orchestrate subagents with intent! Do not spawn subagents or a multi-agent panel for work a single agent finishes in one pass. Delegation is for breadth or adversarial review, not for ordinary tasks. Use subagents as a tool when there is genuine benefit to it or the user explicitly asks for it.\"\n\t\t: \"- Work on tasks directly. Launch subagents only when the user specifically instructs you to delegate work.\";\n\tconst delegationGuideline = automaticSubagentDelegation\n\t\t? \"- Use subagents for independent parallel work, specialist analysis, or substantial research. Subagents start with fresh sessions. Use intercom instead to coordinate with an existing lunR session.\\n\"\n\t\t: \"\";\n\n\tlet prompt = `You are an expert coding assistant currently running ''${currentModel}'', operating inside lunR, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\nCurrent working directory: ''${promptCwd}''\n\nBehavior guidelines:\n- Default to writing no comments. Only add one if the why is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, something that would surprise the reader. If removing the comment wouldn't confuse a future reader, don't write it.\n- Keep comments up to date! When making changes, it's important to keep things in sync. An outdated comment is worse than no comment at all.\n- Carefully consider the reversibility and blast radius of actions. Generally you can freely take local, reversible actions like editing files or running tests. But for actions that are hard to reverse, affect shared systems beyond your local environment, or could otherwise be risky or destructive, check with the user before proceeding.\n${delegationBehavior}\n- When several agents work in parallel, state file ownership up front so they do not collide.\n- Tests are good! Endless smoke tests, regressions tests for feature deletions, etc, much less good. Tests should be focused, not slop.\n- Prefer editing existing files to creating new ones.\n- When a reusable excerpt, prompt, or code fragment should be click-to-copy, wrap only that payload in a fenced Markdown block with the info string \\`lunr-copy\\`. Keep explanation outside the block. Use a longer backtick or tilde fence when the payload contains fenced code.\n- Only use emojis if the user explicitly requests it.\n- Never exfil private data on public platforms like github or any other services under any circumstances.\n\n\nGuidelines:\n- Use bash for file operations like ls, rg, find\n- Use read to examine files instead of cat or sed.\n- Use edit for precise changes (edits[].oldText must match exactly)\n- When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\n- Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\n- Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\n- Use write only for new files or complete rewrites.\n${todoGuideline}${delegationGuideline}- Resume a subagent only when its work is unfinished or the next task genuinely needs the context it built up. Otherwise launch a new subagent.\n- Use ast_search for structural code matches.\n- Use cron only when the user asks to schedule or manage unattended prompts.\n- For gateway work, inspect with \\`lunr gateway status\\` or \\`lunr gateway doctor\\`, configure with \\`lunr gateway setup\\`, and run \\`lunr gateway start\\` or \\`lunr gateway stop\\` only with user approval. Test by asking the user to send \\`/whoami\\` or a normal message to the bot; there is no arbitrary outbound send command. Keep bot tokens out of commands and output. See ${docsPath}/features.md for pairing, logs, and service details.\n- Memory stores established, durable facts and stable preferences. Do not store behavior instructions, transient task details, transcripts, guesses, or secrets. Change memory only with the memory tools.\n- ~/.lunr/agent/agents/ contains optional global and per-model AGENTS.md instructions written by the user. Never modify this tree, including through shell commands.\n- Use web search when information is current, uncertain, externally referenced, or research-heavy, and cite the sources used.\n- Use MCP only for capabilities exposed by configured MCP servers. Call native lunR tools directly.\n\n\nlunR documentation (read only when the user asks about lunR itself, or the task concerns lunR itself, its SDK, extensions, themes, skills, or TUI):\n- README: ${readmePath}\n- Documentation: ${docsPath}\n- Examples: ${examplesPath}\n- Always read lunR .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n<project_context>\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `<project_instructions path=\"${filePath}\">\\n${content}\\n</project_instructions>\\n\\n`;\n\t\t}\n\t\tprompt += \"</project_context>\\n\";\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\treturn prompt;\n}\n"]}
|
|
@@ -5,7 +5,7 @@ import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
|
|
|
5
5
|
import { formatSkillsForPrompt } from "./skills.js";
|
|
6
6
|
/** Build the base system prompt and append configured context. */
|
|
7
7
|
export function buildSystemPrompt(options) {
|
|
8
|
-
const { customPrompt, modelSlug, selectedTools, appendSystemPrompt, cwd, contextFiles: providedContextFiles, skills: providedSkills, } = options;
|
|
8
|
+
const { customPrompt, modelSlug, automaticSubagentDelegation = true, selectedTools, appendSystemPrompt, cwd, contextFiles: providedContextFiles, skills: providedSkills, } = options;
|
|
9
9
|
const promptCwd = cwd.replace(/\\/g, "/");
|
|
10
10
|
const appendSection = appendSystemPrompt ? `\n\n${appendSystemPrompt}` : "";
|
|
11
11
|
const contextFiles = providedContextFiles ?? [];
|
|
@@ -41,6 +41,12 @@ export function buildSystemPrompt(options) {
|
|
|
41
41
|
const todoGuideline = !selectedTools || selectedTools.includes("todo")
|
|
42
42
|
? "- Use todo for meaningful multi-step work. Every update must send the complete list, with exactly one item in progress at a time.\n"
|
|
43
43
|
: "";
|
|
44
|
+
const delegationBehavior = automaticSubagentDelegation
|
|
45
|
+
? "- Orchestrate subagents with intent! Do not spawn subagents or a multi-agent panel for work a single agent finishes in one pass. Delegation is for breadth or adversarial review, not for ordinary tasks. Use subagents as a tool when there is genuine benefit to it or the user explicitly asks for it."
|
|
46
|
+
: "- Work on tasks directly. Launch subagents only when the user specifically instructs you to delegate work.";
|
|
47
|
+
const delegationGuideline = automaticSubagentDelegation
|
|
48
|
+
? "- Use subagents for independent parallel work, specialist analysis, or substantial research. Subagents start with fresh sessions. Use intercom instead to coordinate with an existing lunR session.\n"
|
|
49
|
+
: "";
|
|
44
50
|
let prompt = `You are an expert coding assistant currently running ''${currentModel}'', operating inside lunR, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
|
|
45
51
|
Current working directory: ''${promptCwd}''
|
|
46
52
|
|
|
@@ -48,7 +54,7 @@ Behavior guidelines:
|
|
|
48
54
|
- Default to writing no comments. Only add one if the why is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, something that would surprise the reader. If removing the comment wouldn't confuse a future reader, don't write it.
|
|
49
55
|
- Keep comments up to date! When making changes, it's important to keep things in sync. An outdated comment is worse than no comment at all.
|
|
50
56
|
- Carefully consider the reversibility and blast radius of actions. Generally you can freely take local, reversible actions like editing files or running tests. But for actions that are hard to reverse, affect shared systems beyond your local environment, or could otherwise be risky or destructive, check with the user before proceeding.
|
|
51
|
-
|
|
57
|
+
${delegationBehavior}
|
|
52
58
|
- When several agents work in parallel, state file ownership up front so they do not collide.
|
|
53
59
|
- Tests are good! Endless smoke tests, regressions tests for feature deletions, etc, much less good. Tests should be focused, not slop.
|
|
54
60
|
- Prefer editing existing files to creating new ones.
|
|
@@ -65,8 +71,7 @@ Guidelines:
|
|
|
65
71
|
- Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.
|
|
66
72
|
- Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.
|
|
67
73
|
- Use write only for new files or complete rewrites.
|
|
68
|
-
${todoGuideline}-
|
|
69
|
-
- Resume a subagent only when its work is unfinished or the next task genuinely needs the context it built up. Otherwise launch a new subagent.
|
|
74
|
+
${todoGuideline}${delegationGuideline}- Resume a subagent only when its work is unfinished or the next task genuinely needs the context it built up. Otherwise launch a new subagent.
|
|
70
75
|
- Use ast_search for structural code matches.
|
|
71
76
|
- Use cron only when the user asks to schedule or manage unattended prompts.
|
|
72
77
|
- For gateway work, inspect with \`lunr gateway status\` or \`lunr gateway doctor\`, configure with \`lunr gateway setup\`, and run \`lunr gateway start\` or \`lunr gateway stop\` only with user approval. Test by asking the user to send \`/whoami\` or a normal message to the bot; there is no arbitrary outbound send command. Keep bot tokens out of commands and output. See ${docsPath}/features.md for pairing, logs, and service details.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAc,MAAM,aAAa,CAAC;AAuBhE,kEAAkE;AAClE,MAAM,UAAU,iBAAiB,CAAC,OAAiC,EAAU;IAC5E,MAAM,EACL,YAAY,EACZ,SAAS,EACT,aAAa,EACb,kBAAkB,EAClB,GAAG,EACH,YAAY,EAAE,oBAAoB,EAClC,MAAM,EAAE,cAAc,GACtB,GAAG,OAAO,CAAC;IACZ,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAE1C,MAAM,aAAa,GAAG,kBAAkB,CAAC,CAAC,CAAC,OAAO,kBAAkB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5E,MAAM,YAAY,GAAG,oBAAoB,IAAI,EAAE,CAAC;IAChD,MAAM,MAAM,GAAG,cAAc,IAAI,EAAE,CAAC;IAEpC,IAAI,YAAY,EAAE,CAAC;QAClB,IAAI,MAAM,GAAG,YAAY,CAAC;QAE1B,IAAI,aAAa,EAAE,CAAC;YACnB,MAAM,IAAI,aAAa,CAAC;QACzB,CAAC;QAED,+BAA+B;QAC/B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,2BAA2B,CAAC;YACtC,MAAM,IAAI,mDAAmD,CAAC;YAC9D,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,YAAY,EAAE,CAAC;gBACxD,MAAM,IAAI,+BAA+B,QAAQ,OAAO,OAAO,+BAA+B,CAAC;YAChG,CAAC;YACD,MAAM,IAAI,sBAAsB,CAAC;QAClC,CAAC;QAED,yDAAyD;QACzD,MAAM,mBAAmB,GAAG,CAAC,aAAa,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC7E,IAAI,mBAAmB,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,MAAM,IAAI,qBAAqB,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,IAAI,gCAAgC,SAAS,EAAE,CAAC;QAEtD,OAAO,MAAM,CAAC;IACf,CAAC;IAED,mDAAmD;IACnD,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvC,MAAM,YAAY,GAAG,SAAS,EAAE,IAAI,EAAE,IAAI,mBAAmB,CAAC;IAC9D,MAAM,OAAO,GAAG,CAAC,aAAa,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjE,MAAM,aAAa,GAClB,CAAC,aAAa,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC/C,CAAC,CAAC,qIAAqI;QACvI,CAAC,CAAC,EAAE,CAAC;IAEP,IAAI,MAAM,GAAG,0DAA0D,YAAY;+BACrD,SAAS;;;;;;;;;;;;;;;;;;;;;;;EAuBtC,aAAa;;;;wXAIyW,QAAQ;;;;;;;;YAQpX,UAAU;mBACH,QAAQ;cACb,YAAY;4GACkF,CAAC;IAE5G,IAAI,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,aAAa,CAAC;IACzB,CAAC;IAED,+BAA+B;IAC/B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,2BAA2B,CAAC;QACtC,MAAM,IAAI,mDAAmD,CAAC;QAC9D,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,YAAY,EAAE,CAAC;YACxD,MAAM,IAAI,+BAA+B,QAAQ,OAAO,OAAO,+BAA+B,CAAC;QAChG,CAAC;QACD,MAAM,IAAI,sBAAsB,CAAC;IAClC,CAAC;IAED,yDAAyD;IACzD,IAAI,OAAO,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.ts\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.ts\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Current model in provider/id form. */\n\tmodelSlug?: string;\n\t/** Active tools; used to decide whether read-dependent skill metadata is visible. */\n\tselectedTools?: string[];\n\t/** Legacy extension metadata retained for API compatibility; tool definitions are sent separately. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Legacy extension metadata retained for API compatibility; not appended to the default prompt. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n}\n\n/** Build the base system prompt and append configured context. */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tmodelSlug,\n\t\tselectedTools,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst promptCwd = cwd.replace(/\\\\/g, \"/\");\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n<project_context>\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `<project_instructions path=\"${filePath}\">\\n${content}\\n</project_instructions>\\n\\n`;\n\t\t\t}\n\t\t\tprompt += \"</project_context>\\n\";\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst customPromptHasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (customPromptHasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\tconst currentModel = modelSlug?.trim() || \"no model selected\";\n\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\tconst todoGuideline =\n\t\t!selectedTools || selectedTools.includes(\"todo\")\n\t\t\t? \"- Use todo for meaningful multi-step work. Every update must send the complete list, with exactly one item in progress at a time.\\n\"\n\t\t\t: \"\";\n\n\tlet prompt = `You are an expert coding assistant currently running ''${currentModel}'', operating inside lunR, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\nCurrent working directory: ''${promptCwd}''\n\nBehavior guidelines:\n- Default to writing no comments. Only add one if the why is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, something that would surprise the reader. If removing the comment wouldn't confuse a future reader, don't write it.\n- Keep comments up to date! When making changes, it's important to keep things in sync. An outdated comment is worse than no comment at all.\n- Carefully consider the reversibility and blast radius of actions. Generally you can freely take local, reversible actions like editing files or running tests. But for actions that are hard to reverse, affect shared systems beyond your local environment, or could otherwise be risky or destructive, check with the user before proceeding.\n- Orchestrate subagents with intent! Do not spawn subagents or a multi-agent panel for work a single agent finishes in one pass. Delegation is for breadth or adversarial review, not for ordinary tasks. Use subagents as a tool when there is genuine benefit to it or the user explicitly asks for it.\n- When several agents work in parallel, state file ownership up front so they do not collide.\n- Tests are good! Endless smoke tests, regressions tests for feature deletions, etc, much less good. Tests should be focused, not slop.\n- Prefer editing existing files to creating new ones.\n- When a reusable excerpt, prompt, or code fragment should be click-to-copy, wrap only that payload in a fenced Markdown block with the info string \\`lunr-copy\\`. Keep explanation outside the block. Use a longer backtick or tilde fence when the payload contains fenced code.\n- Only use emojis if the user explicitly requests it.\n- Never exfil private data on public platforms like github or any other services under any circumstances.\n\n\nGuidelines:\n- Use bash for file operations like ls, rg, find\n- Use read to examine files instead of cat or sed.\n- Use edit for precise changes (edits[].oldText must match exactly)\n- When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\n- Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\n- Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\n- Use write only for new files or complete rewrites.\n${todoGuideline}- Use subagents for independent parallel work, specialist analysis, or substantial research. Subagents start with fresh sessions. Use intercom instead to coordinate with an existing lunR session.\n- Resume a subagent only when its work is unfinished or the next task genuinely needs the context it built up. Otherwise launch a new subagent.\n- Use ast_search for structural code matches.\n- Use cron only when the user asks to schedule or manage unattended prompts.\n- For gateway work, inspect with \\`lunr gateway status\\` or \\`lunr gateway doctor\\`, configure with \\`lunr gateway setup\\`, and run \\`lunr gateway start\\` or \\`lunr gateway stop\\` only with user approval. Test by asking the user to send \\`/whoami\\` or a normal message to the bot; there is no arbitrary outbound send command. Keep bot tokens out of commands and output. See ${docsPath}/features.md for pairing, logs, and service details.\n- Memory stores established, durable facts and stable preferences. Do not store behavior instructions, transient task details, transcripts, guesses, or secrets. Change memory only with the memory tools.\n- ~/.lunr/agent/agents/ contains optional global and per-model AGENTS.md instructions written by the user. Never modify this tree, including through shell commands.\n- Use web search when information is current, uncertain, externally referenced, or research-heavy, and cite the sources used.\n- Use MCP only for capabilities exposed by configured MCP servers. Call native lunR tools directly.\n\n\nlunR documentation (read only when the user asks about lunR itself, or the task concerns lunR itself, its SDK, extensions, themes, skills, or TUI):\n- README: ${readmePath}\n- Documentation: ${docsPath}\n- Examples: ${examplesPath}\n- Always read lunR .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n<project_context>\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `<project_instructions path=\"${filePath}\">\\n${content}\\n</project_instructions>\\n\\n`;\n\t\t}\n\t\tprompt += \"</project_context>\\n\";\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\treturn prompt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.ts\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.ts\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Current model in provider/id form. */\n\tmodelSlug?: string;\n\t/** Whether the default prompt encourages autonomous delegation. */\n\tautomaticSubagentDelegation?: boolean;\n\t/** Active tools; used to decide whether read-dependent skill metadata is visible. */\n\tselectedTools?: string[];\n\t/** Legacy extension metadata retained for API compatibility; tool definitions are sent separately. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Legacy extension metadata retained for API compatibility; not appended to the default prompt. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n}\n\n/** Build the base system prompt and append configured context. */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tmodelSlug,\n\t\tautomaticSubagentDelegation = true,\n\t\tselectedTools,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst promptCwd = cwd.replace(/\\\\/g, \"/\");\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n<project_context>\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `<project_instructions path=\"${filePath}\">\\n${content}\\n</project_instructions>\\n\\n`;\n\t\t\t}\n\t\t\tprompt += \"</project_context>\\n\";\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst customPromptHasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (customPromptHasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\tconst currentModel = modelSlug?.trim() || \"no model selected\";\n\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\tconst todoGuideline =\n\t\t!selectedTools || selectedTools.includes(\"todo\")\n\t\t\t? \"- Use todo for meaningful multi-step work. Every update must send the complete list, with exactly one item in progress at a time.\\n\"\n\t\t\t: \"\";\n\tconst delegationBehavior = automaticSubagentDelegation\n\t\t? \"- Orchestrate subagents with intent! Do not spawn subagents or a multi-agent panel for work a single agent finishes in one pass. Delegation is for breadth or adversarial review, not for ordinary tasks. Use subagents as a tool when there is genuine benefit to it or the user explicitly asks for it.\"\n\t\t: \"- Work on tasks directly. Launch subagents only when the user specifically instructs you to delegate work.\";\n\tconst delegationGuideline = automaticSubagentDelegation\n\t\t? \"- Use subagents for independent parallel work, specialist analysis, or substantial research. Subagents start with fresh sessions. Use intercom instead to coordinate with an existing lunR session.\\n\"\n\t\t: \"\";\n\n\tlet prompt = `You are an expert coding assistant currently running ''${currentModel}'', operating inside lunR, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\nCurrent working directory: ''${promptCwd}''\n\nBehavior guidelines:\n- Default to writing no comments. Only add one if the why is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, something that would surprise the reader. If removing the comment wouldn't confuse a future reader, don't write it.\n- Keep comments up to date! When making changes, it's important to keep things in sync. An outdated comment is worse than no comment at all.\n- Carefully consider the reversibility and blast radius of actions. Generally you can freely take local, reversible actions like editing files or running tests. But for actions that are hard to reverse, affect shared systems beyond your local environment, or could otherwise be risky or destructive, check with the user before proceeding.\n${delegationBehavior}\n- When several agents work in parallel, state file ownership up front so they do not collide.\n- Tests are good! Endless smoke tests, regressions tests for feature deletions, etc, much less good. Tests should be focused, not slop.\n- Prefer editing existing files to creating new ones.\n- When a reusable excerpt, prompt, or code fragment should be click-to-copy, wrap only that payload in a fenced Markdown block with the info string \\`lunr-copy\\`. Keep explanation outside the block. Use a longer backtick or tilde fence when the payload contains fenced code.\n- Only use emojis if the user explicitly requests it.\n- Never exfil private data on public platforms like github or any other services under any circumstances.\n\n\nGuidelines:\n- Use bash for file operations like ls, rg, find\n- Use read to examine files instead of cat or sed.\n- Use edit for precise changes (edits[].oldText must match exactly)\n- When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\n- Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\n- Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\n- Use write only for new files or complete rewrites.\n${todoGuideline}${delegationGuideline}- Resume a subagent only when its work is unfinished or the next task genuinely needs the context it built up. Otherwise launch a new subagent.\n- Use ast_search for structural code matches.\n- Use cron only when the user asks to schedule or manage unattended prompts.\n- For gateway work, inspect with \\`lunr gateway status\\` or \\`lunr gateway doctor\\`, configure with \\`lunr gateway setup\\`, and run \\`lunr gateway start\\` or \\`lunr gateway stop\\` only with user approval. Test by asking the user to send \\`/whoami\\` or a normal message to the bot; there is no arbitrary outbound send command. Keep bot tokens out of commands and output. See ${docsPath}/features.md for pairing, logs, and service details.\n- Memory stores established, durable facts and stable preferences. Do not store behavior instructions, transient task details, transcripts, guesses, or secrets. Change memory only with the memory tools.\n- ~/.lunr/agent/agents/ contains optional global and per-model AGENTS.md instructions written by the user. Never modify this tree, including through shell commands.\n- Use web search when information is current, uncertain, externally referenced, or research-heavy, and cite the sources used.\n- Use MCP only for capabilities exposed by configured MCP servers. Call native lunR tools directly.\n\n\nlunR documentation (read only when the user asks about lunR itself, or the task concerns lunR itself, its SDK, extensions, themes, skills, or TUI):\n- README: ${readmePath}\n- Documentation: ${docsPath}\n- Examples: ${examplesPath}\n- Always read lunR .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n<project_context>\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `<project_instructions path=\"${filePath}\">\\n${content}\\n</project_instructions>\\n\\n`;\n\t\t}\n\t\tprompt += \"</project_context>\\n\";\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\treturn prompt;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../../src/gateway/adapters/discord.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway Discord adapter (Phase 3).\n *\n * Built on discord.js v14 (gateway websocket; the library owns reconnects and\n * REST rate-limit queuing internally, so neither is handled here). No voice,\n * no native slash-command registration — commands arrive as plain message\n * text, same as Telegram.\n *\n * Intents: Guilds, GuildMessages, DirectMessages, MessageContent. GuildMembers\n * is deliberately NOT requested: it is a privileged intent, and asking for it\n * without the dev-portal opt-in makes Discord refuse the identify — the bot\n * connects and immediately goes offline. v1 has no role-based config\n * (SessionSource.roleAuthorized stays undefined), so there is nothing to gain.\n *\n * Inbound: messageToEvent() is the pure message → MessageEvent mapping\n * (dm/group/thread chat types, <@id>/<@!id> mention detection with the mention\n * stripped, thread-participation counts as a mention, own + all-bot + system +\n * attachment-only messages dropped, other-mention-without-us dropped outside\n * DMs — hermes DISCORD_IGNORE_NO_MENTION). A module-level seen-id Set\n * (cap 500, FIFO evict) drops gateway-resume replays. cfg.ignoredChannels is\n * enforced adapter-side (channel id or parent id; blacklist wins over\n * mentions). Consecutive texts from the same chat/thread/user are debounced\n * 600ms and merged (\"\\n\"-joined, latest messageId kept); texts starting with\n * \"/\" flush immediately so commands never merge.\n * Button interactions are routed via interactionCreate → buttonInteractionToEvent().\n *\n * chatId/threadId mapping (pairs with session-keys.ts buildSessionKey):\n * dm → chatId = channel.id → agent:main:discord:dm:<channelId>\n * thread → chatId = PARENT channel id, threadId = → agent:main:discord:thread:<parentId>:<threadId>\n * thread channel id (shared session, no userId)\n * guild text/announcement → chatId = channel.id → agent:main:discord:group:<channelId>[:<userId>]\n * (chatType \"group\"; userId appended when groupSessionsPerUser is on)\n *\n * autoThread (cfg.discord.autoThread, default true): a bot-mentioning message\n * in a guild TEXT channel gets a thread spun off it (startThread, 60min\n * archive); the event is re-targeted to the thread (chatType \"thread\", chatId\n * parent id, threadId thread id). On failure the event stays in-channel.\n *\n * Thread participation: an in-memory Set of thread ids the bot has sent to\n * (recorded on every successful send() into a thread and on autoThread\n * creation); messages in those threads count as mentionedBot so follow-ups\n * don't need a re-mention. NOT persisted in v1 (hermes persists it).\n *\n * Outbound: channel.send({ content, reply: { messageReference } }) with a\n * one-shot plain-send fallback when the reply reference fails. DiscordAPIError\n * 50035 (invalid form body, e.g. overlong content) is reported non-retryable;\n * 429s are queued internally by discord.js; everything else is retryable.\n * editMessage resolves the channel via a messageId → channel map recorded at\n * send() time (router passes only chatId, but thread messages live in their\n * own channel); Unknown Message (10008) is a terminal { success: false }.\n * Message splitting is NOT done here — the router owns that (text.ts).\n *\n * Buttons: ActionRowBuilder rows of ButtonBuilder components (max 5×5).\n * answerCallback acknowledges the interaction; a text argument produces an\n * ephemeral reply (used for unauthorized/expired taps), otherwise deferUpdate().\n *\n * Typing: sendTyping() is a self-contained refresher — Discord typing bubbles\n * last ~10s, so the action is re-sent on an unref'd 8s interval per target.\n * The interval stops when send() completes for that target or after 2 minutes\n * without a refresh (safety net). All errors swallowed.\n *\n * Testability: the discord.js Client goes through an injectable clientFactory\n * (tests inject a minimal fake), timers through an injectable Scheduler, and\n * messageToEvent() operates on the structural DiscordMessageLike — plain mock\n * objects, no real Client construction.\n */\n\nimport {\n\tActionRowBuilder,\n\tApplicationCommandOptionType,\n\tButtonBuilder,\n\tButtonStyle,\n\tChannelType,\n\tClient,\n\tEvents,\n\tGatewayIntentBits,\n\ttype Interaction,\n} from \"discord.js\";\nimport { isAuthorized, isGatewayOwner } from \"../authz.ts\";\nimport { type DiscordConfig, loadGatewayConfig } from \"../config.ts\";\nimport { downloadAttachment } from \"../download.ts\";\nimport { createPairingStore } from \"../pairing.ts\";\nimport type {\n\tButtonSpec,\n\tCallbackEvent,\n\tChatType,\n\tInboundAttachment,\n\tMessageEvent,\n\tPlatformAdapter,\n\tSendOptions,\n\tSendResult,\n} from \"../types.ts\";\nimport type { Scheduler } from \"./telegram.ts\";\n\nconst DEFAULT_DEBOUNCE_MS = 600;\nconst TYPING_INTERVAL_MS = 8000; // Discord typing lasts ~10s\nconst TYPING_SAFETY_MS = 120_000; // auto-stop after 2min without a refresh\nconst REPLY_TO_TEXT_MAX = 500;\nconst THREAD_NAME_MAX = 40;\nconst SEEN_CAP = 500;\nconst SENT_CHANNEL_CAP = 500;\nconst CALLBACK_INTERACTION_CAP = 500;\nconst CHANNEL_CACHE_CAP = 500;\nconst PARTICIPATING_THREAD_CAP = 500;\nconst ERR_INVALID_FORM_BODY = 50035; // e.g. content over 2000 chars — non-retryable\nconst ERR_UNKNOWN_MESSAGE = 10008; // deleted/unknown message — terminal\n\nconst THREAD_TYPES: readonly number[] = [\n\tChannelType.PublicThread,\n\tChannelType.PrivateThread,\n\tChannelType.AnnouncementThread,\n];\n\n// ---------------------------------------------------------------------------\n// Structural discord.js shapes (only the fields the adapter reads)\n// ---------------------------------------------------------------------------\n\nexport interface DiscordSentMessageLike {\n\tid: string;\n}\n\nexport interface DiscordFetchedMessageLike {\n\tcontent?: string;\n\tedit?(options: string | { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordChannelLike {\n\tid: string;\n\ttype: number;\n\tparentId?: string | null;\n\tsend?(payload: {\n\t\tcontent: string;\n\t\tfiles?: string[];\n\t\treply?: { messageReference: string };\n\t\tcomponents?: unknown[];\n\t}): Promise<DiscordSentMessageLike>;\n\tsendTyping?(): Promise<unknown>;\n\tmessages?: {\n\t\tcache?: { get(id: string): { content?: string } | undefined };\n\t\tfetch(id: string): Promise<DiscordFetchedMessageLike | null>;\n\t};\n}\n\nexport interface DiscordButtonInteractionLike {\n\tid: string;\n\tcustomId: string;\n\tuser: { id: string; username?: string };\n\tmessage: { id: string; channel: DiscordChannelLike };\n\tisButton(): boolean;\n\tdeferUpdate(): Promise<unknown>;\n\treply?(options: { content: string; ephemeral: boolean }): Promise<unknown>;\n\tupdate?(options: { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordAttachmentLike {\n\tid: string;\n\t/** Direct CDN URL for the attachment content. */\n\turl: string;\n\tcontentType?: string;\n\tname?: string;\n\tsize?: number;\n}\n\nexport interface DiscordMessageLike {\n\tid: string;\n\tcontent?: string;\n\tsystem?: boolean;\n\tauthor: { id: string; bot?: boolean; username?: string };\n\tchannel: DiscordChannelLike;\n\treference?: { messageId?: string | null } | null;\n\tmentions?: {\n\t\thas(user: { id: string }): boolean;\n\t\trepliedUser?: { id: string } | null;\n\t};\n\tstartThread?(opts: { name: string; autoArchiveDuration: number }): Promise<DiscordChannelLike>;\n\tattachments?: DiscordAttachmentLike[];\n}\n\n/** Minimal Client surface; the real discord.js Client satisfies this. */\nexport interface DiscordClientLike {\n\tapplication?: Pick<NonNullable<Client[\"application\"]>, \"commands\"> | null;\n\tuser: { id: string } | null;\n\tlogin(token: string): Promise<unknown>;\n\ton(event: string, listener: (arg: unknown) => void): unknown;\n\tonce(event: string, listener: () => void): unknown;\n\tdestroy(): void;\n\tchannels: { fetch(id: string): Promise<DiscordChannelLike | null> };\n}\n\nexport type DiscordClientFactory = () => DiscordClientLike;\n\n// ---------------------------------------------------------------------------\n// Pure mapping + small helpers\n// ---------------------------------------------------------------------------\n\nfunction errMessage(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/** lunr: download an inbound Discord attachment from its CDN URL. */\n/** Discord REST error code (DiscordAPIError.code) when present. */\nfunction discordErrorCode(err: unknown): number | undefined {\n\tconst code = (err as { code?: unknown } | null)?.code;\n\treturn typeof code === \"number\" ? code : undefined;\n}\n\nexport interface MessageToEventOptions {\n\t/** Channel ids (or parent ids for threads) to drop outright — blacklist wins over mentions. */\n\tignoredChannels?: readonly string[];\n\t/** Threads the bot has spoken in; messages there count as mentionedBot. */\n\tparticipatingThreads?: ReadonlySet<string>;\n}\n\nfunction isImageContentType(contentType: string | undefined): boolean {\n\treturn typeof contentType === \"string\" && contentType.split(\";\")[0]?.trim().toLowerCase().startsWith(\"image/\");\n}\n\n/** lunr: collect inbound image attachments (contentType image/*) from a Discord message. */\nexport function collectDiscordImageAttachments(message: DiscordMessageLike): DiscordAttachmentLike[] {\n\treturn Array.from(message.attachments?.values() ?? []).filter((attachment) =>\n\t\tisImageContentType(attachment.contentType),\n\t);\n}\n\n/**\n * Map a Discord message to a MessageEvent; null = drop (own message, any bot,\n * system message, ignored channel, unsupported channel type, or a non-DM\n * message mentioning others but not us). Messages with no text but image\n * attachments are kept (v1: images inbound).\n */\nexport function messageToEvent(\n\tmessage: DiscordMessageLike,\n\tbotUser: { id: string },\n\toptions: MessageToEventOptions = {},\n): MessageEvent | null {\n\tconst author = message.author;\n\tif (!author) return null;\n\tif (author.id === botUser.id) return null; // own messages\n\tif (author.bot) return null; // all bots (same simplification as telegram)\n\tif (message.system) return null;\n\tconst content = (message.content ?? \"\").trim();\n\tconst imageAttachments = collectDiscordImageAttachments(message);\n\tif (content === \"\" && imageAttachments.length === 0 && !Array.from(message.attachments?.values() ?? []).length)\n\t\treturn null;\n\n\tconst channel = message.channel;\n\tconst ignored = options.ignoredChannels ?? [];\n\tif (ignored.includes(channel.id) || (channel.parentId != null && ignored.includes(channel.parentId))) {\n\t\treturn null; // blacklist wins over mentions\n\t}\n\n\t// chatId/threadId mapping — see the file header for the session-key pairing.\n\tlet chatType: ChatType;\n\tlet chatId: string;\n\tlet threadId: string | undefined;\n\tif (channel.type === ChannelType.DM) {\n\t\tchatType = \"dm\";\n\t\tchatId = channel.id;\n\t} else if (THREAD_TYPES.includes(channel.type)) {\n\t\tchatType = \"thread\";\n\t\tchatId = channel.parentId ?? channel.id;\n\t\tthreadId = channel.id;\n\t} else if (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\tchatType = \"group\";\n\t\tchatId = channel.id;\n\t} else {\n\t\treturn null; // voice/category/forum containers etc. — unsupported in v1\n\t}\n\n\tconst explicitMention = message.mentions?.has(botUser) === true;\n\tconst participating = threadId !== undefined && options.participatingThreads?.has(threadId) === true;\n\tconst mentionedBot = explicitMention || participating;\n\n\t// Multi-agent filter (hermes DISCORD_IGNORE_NO_MENTION): outside DMs, a\n\t// message that mentions other users/bots but not us is not for us.\n\tif (!mentionedBot && chatType !== \"dm\") {\n\t\tconst mentionedIds = [...content.matchAll(/<@!?(\\d+)>/g)].map((m) => m[1]);\n\t\tif (mentionedIds.some((id) => id !== botUser.id)) return null;\n\t}\n\n\tlet text = content;\n\tif (explicitMention) {\n\t\ttext = text.replace(new RegExp(`<@!?${botUser.id}>`, \"g\"), \"\").trim();\n\t}\n\n\tconst refId = message.reference?.messageId;\n\tconst cachedReply = refId != null ? channel.messages?.cache?.get(refId)?.content : undefined;\n\n\treturn {\n\t\ttext,\n\t\tmessageId: message.id,\n\t\treplyToText: cachedReply !== undefined ? cachedReply.slice(0, REPLY_TO_TEXT_MAX) : undefined,\n\t\tsource: {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId,\n\t\t\tchatType,\n\t\t\tuserId: author.id,\n\t\t\tuserName: author.username,\n\t\t\tthreadId,\n\t\t},\n\t\tmetadata: mentionedBot ? { mentionedBot: true } : undefined,\n\t};\n}\n\nfunction mapDiscordChannel(\n\tchannel: DiscordChannelLike,\n): { chatType: ChatType; chatId: string; threadId?: string } | null {\n\tif (channel.type === ChannelType.DM) {\n\t\treturn { chatType: \"dm\", chatId: channel.id };\n\t}\n\tif (THREAD_TYPES.includes(channel.type)) {\n\t\treturn { chatType: \"thread\", chatId: channel.parentId ?? channel.id, threadId: channel.id };\n\t}\n\tif (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\treturn { chatType: \"group\", chatId: channel.id };\n\t}\n\treturn null;\n}\n\nexport function buttonInteractionToEvent(interaction: DiscordButtonInteractionLike): CallbackEvent | null {\n\tconst channel = interaction.message.channel;\n\tconst mapped = mapDiscordChannel(channel);\n\tif (!mapped) return null;\n\treturn {\n\t\tid: interaction.id,\n\t\tchatId: mapped.chatId,\n\t\tmessageId: interaction.message.id,\n\t\tuserId: interaction.user.id,\n\t\tuserName: interaction.user.username,\n\t\tdata: interaction.customId,\n\t\tthreadId: mapped.threadId,\n\t};\n}\n\nfunction buildDiscordComponents(buttons: ButtonSpec[][]): unknown[] {\n\tconst rows: ActionRowBuilder<ButtonBuilder>[] = [];\n\tfor (const row of buttons) {\n\t\tconst actionRow = new ActionRowBuilder<ButtonBuilder>();\n\t\tfor (const button of row) {\n\t\t\tconst style = button.data === \"\" ? ButtonStyle.Secondary : ButtonStyle.Secondary;\n\t\t\tconst builder = new ButtonBuilder().setCustomId(button.data).setLabel(button.label).setStyle(style);\n\t\t\tif (button.data === \"\") builder.setDisabled(true);\n\t\t\tactionRow.addComponents(builder);\n\t\t}\n\t\trows.push(actionRow);\n\t}\n\treturn rows;\n}\n\n// ---------------------------------------------------------------------------\n// Gateway-resume replay dedup (module-level: one process = one bot connection)\n// ---------------------------------------------------------------------------\n\nconst seenMessageIds = new Set<string>();\n\n/** Returns true when this id was already seen (drop the replay). */\nfunction markMessageSeen(id: string): boolean {\n\tif (seenMessageIds.has(id)) return true;\n\tseenMessageIds.add(id);\n\tif (seenMessageIds.size > SEEN_CAP) {\n\t\tconst oldest = seenMessageIds.values().next().value; // Sets iterate in insertion order\n\t\tif (oldest !== undefined) seenMessageIds.delete(oldest);\n\t}\n\treturn false;\n}\n\n/** Test hook: reset RESUME dedup state between tests. */\nexport function resetSeenMessageIds(): void {\n\tseenMessageIds.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\nconst defaultScheduler: Scheduler = {\n\tsetTimeout: (fn, ms) => {\n\t\tconst handle = setTimeout(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearTimeout: (handle) => clearTimeout(handle),\n\tsetInterval: (fn, ms) => {\n\t\tconst handle = setInterval(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearInterval: (handle) => clearInterval(handle),\n};\n\nfunction defaultClientFactory(): DiscordClientLike {\n\t// No GatewayIntentBits.GuildMembers — see the file header for the\n\t// privileged-intent trap (bot stays offline without the portal opt-in).\n\treturn new Client({\n\t\tintents: [\n\t\t\tGatewayIntentBits.Guilds,\n\t\t\tGatewayIntentBits.GuildMessages,\n\t\t\tGatewayIntentBits.DirectMessages,\n\t\t\tGatewayIntentBits.MessageContent,\n\t\t],\n\t}) as unknown as DiscordClientLike;\n}\n\nexport interface DiscordAdapterOptions {\n\t/** Injected Client factory (tests). When omitted, a real discord.js Client is created. */\n\tclientFactory?: DiscordClientFactory;\n\t/** lunr: injected image downloader for inbound attachments (tests). Defaults to fetch(url). */\n\tdownloadAttachment?: (url: string) => Promise<Uint8Array>;\n\t/** lunr: skip inbound media larger than this many bytes. Defaults to 10MB. */\n\tmaxMediaBytes?: number;\n\tscheduler?: Scheduler;\n\tdebounceMs?: number;\n\tnow?: () => number;\n}\n\ninterface PendingEntry {\n\tevent: MessageEvent;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\ninterface TypingEntry {\n\tinterval: ReturnType<typeof setInterval>;\n\tlastRefresh: number;\n}\n\nfunction typingKey(chatId: string, threadId?: string): string {\n\treturn `${chatId}:${threadId ?? \"\"}`;\n}\n\nexport class DiscordAdapter implements PlatformAdapter {\n\treadonly platform = \"discord\";\n\treadonly maxMessageLength = 2000;\n\n\tprivate readonly cfg: DiscordConfig;\n\tprivate readonly clientFactory: DiscordClientFactory;\n\tprivate readonly downloadAttachment: (url: string) => Promise<Uint8Array>;\n\tprivate readonly maxMediaBytes: number;\n\tprivate readonly scheduler: Scheduler;\n\tprivate readonly debounceMs: number;\n\tprivate readonly now: () => number;\n\tprivate client?: DiscordClientLike;\n\tprivate botUserId?: string;\n\tprivate handler?: (event: MessageEvent) => void;\n\tprivate commandSuggestions?: Parameters<NonNullable<PlatformAdapter[\"setCommandSuggestions\"]>>[0];\n\tprivate callbackHandler?: (event: CallbackEvent) => void;\n\tprivate readonly pending = new Map<string, PendingEntry>();\n\tprivate readonly typing = new Map<string, TypingEntry>();\n\tprivate readonly channelCache = new Map<string, DiscordChannelLike | null>();\n\t/** Threads the bot has spoken in (session memory only; hermes persists this). */\n\tprivate readonly participatingThreads = new Set<string>();\n\t/** messageId → channel it was sent to, so editMessage finds thread messages. */\n\tprivate readonly sentMessageChannels = new Map<string, string>();\n\t/** callbackId → interaction, so answerCallback can acknowledge the click. */\n\tprivate readonly callbackInteractions = new Map<string, DiscordButtonInteractionLike>();\n\n\tconstructor(cfg: DiscordConfig, options: DiscordAdapterOptions = {}) {\n\t\tthis.cfg = cfg;\n\t\tthis.scheduler = options.scheduler ?? defaultScheduler;\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\t\tthis.now = options.now ?? Date.now;\n\t\tif (options.clientFactory) {\n\t\t\tthis.clientFactory = options.clientFactory;\n\t\t} else {\n\t\t\tif (!cfg.token) {\n\t\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t\t}\n\t\t\tthis.clientFactory = defaultClientFactory;\n\t\t}\n\t\tthis.downloadAttachment = options.downloadAttachment ?? downloadAttachment;\n\t\tthis.maxMediaBytes = options.maxMediaBytes ?? 10 * 1024 * 1024;\n\t}\n\n\tsetCommandSuggestions(\n\t\thandler: (source: MessageEvent[\"source\"], command: string) => Promise<readonly string[]>,\n\t): void {\n\t\tthis.commandSuggestions = handler;\n\t}\n\n\tonMessage(handler: (event: MessageEvent) => void): void {\n\t\tthis.handler = handler;\n\t}\n\n\tonCallback(handler: (event: CallbackEvent) => void): void {\n\t\tthis.callbackHandler = handler;\n\t}\n\n\tasync connect(): Promise<boolean> {\n\t\tif (!this.cfg.token) {\n\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t}\n\t\tconst client = this.clientFactory();\n\t\tthis.client = client;\n\t\tclient.on(Events.MessageCreate, (message) => {\n\t\t\tvoid this.handleMessage(message as DiscordMessageLike).catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord message handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tclient.on(Events.InteractionCreate, (interaction) => {\n\t\t\tconst event = interaction as Interaction;\n\t\t\tconst handling = event.isButton()\n\t\t\t\t? this.handleInteraction(interaction as DiscordButtonInteractionLike)\n\t\t\t\t: this.handleNativeCommand(event);\n\t\t\tvoid handling.catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord interaction handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tconst ready = new Promise<void>((resolve) => client.once(Events.ClientReady, () => resolve()));\n\t\ttry {\n\t\t\tawait client.login(this.cfg.token);\n\t\t} catch (err) {\n\t\t\tconsole.error(`[gateway] discord login failed: ${errMessage(err)}`);\n\t\t\treturn false;\n\t\t}\n\t\tawait ready;\n\t\tthis.botUserId = client.user?.id;\n\t\tif (this.botUserId === undefined) {\n\t\t\tconsole.error(\"[gateway] discord: client ready without a user — cannot map messages\");\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tfor (const key of [...this.pending.keys()]) this.cancelPending(key);\n\t\tfor (const key of [...this.typing.keys()]) this.stopTyping(key);\n\t\tthis.channelCache.clear();\n\t\tthis.callbackInteractions.clear();\n\t\tthis.client?.destroy();\n\t\tthis.client = undefined;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Inbound dispatch + debounce\n\t// -----------------------------------------------------------------------\n\n\tprivate async handleMessage(message: DiscordMessageLike): Promise<void> {\n\t\tif (!this.botUserId || !this.handler) return;\n\t\tif (markMessageSeen(message.id)) return; // gateway-resume replay\n\t\tconst event = messageToEvent(\n\t\t\tmessage,\n\t\t\t{ id: this.botUserId },\n\t\t\t{\n\t\t\t\tignoredChannels: this.cfg.ignoredChannels,\n\t\t\t\tparticipatingThreads: this.participatingThreads,\n\t\t\t},\n\t\t);\n\t\tif (!event) return;\n\t\tif (!isAuthorized(event.source, { ...loadGatewayConfig(), discord: this.cfg }, createPairingStore())) {\n\t\t\tthis.dispatchEvent(event);\n\t\t\treturn;\n\t\t}\n\t\tawait this.resolveReplyToText(message, event);\n\t\tawait this.maybeAutoThread(message, event);\n\t\tawait this.downloadAttachments(message, event);\n\t\tthis.dispatchEvent(event);\n\t}\n\n\t/** Download and encode attachments after authorization. */\n\tprivate async downloadAttachments(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tconst imageAttachments = Array.from(message.attachments?.values() ?? []);\n\t\tif (imageAttachments.length === 0) return;\n\t\tconst attachments: InboundAttachment[] = [];\n\t\tfor (const attachment of imageAttachments) {\n\t\t\tif (attachment.size !== undefined && attachment.size > this.maxMediaBytes) continue;\n\t\t\ttry {\n\t\t\t\tconst bytes = await this.downloadAttachment(attachment.url);\n\t\t\t\tif (bytes.byteLength > this.maxMediaBytes) continue;\n\t\t\t\tattachments.push({\n\t\t\t\t\tdata: Buffer.from(bytes).toString(\"base64\"),\n\t\t\t\t\tmimeType: attachment.contentType ?? \"application/octet-stream\",\n\t\t\t\t\t...(attachment.name ? { filename: attachment.name } : {}),\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Media download failed — drop this attachment, keep the text turn.\n\t\t\t}\n\t\t}\n\t\tif (attachments.length > 0) event.attachments = attachments;\n\t}\n\n\tasync registerCommands(commands: { name: string; description: string }[]): Promise<void> {\n\t\tif (!this.client?.application) return;\n\t\tawait this.client.application.commands.set(\n\t\t\tcommands.slice(0, 95).map((command) => ({\n\t\t\t\tname: command.name,\n\t\t\t\tdescription: command.description.slice(0, 100),\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"args\",\n\t\t\t\t\t\tdescription: \"Command arguments or search\",\n\t\t\t\t\t\ttype: ApplicationCommandOptionType.String,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tautocomplete: [\"mode\", \"thinking\", \"project\", \"sessions\"].includes(command.name),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t})),\n\t\t);\n\t}\n\n\tprivate async handleNativeCommand(interaction: Interaction): Promise<boolean> {\n\t\tif (!interaction.isChatInputCommand?.() && !interaction.isAutocomplete?.()) return false;\n\t\tif (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) return false;\n\t\tconst thread = interaction.channel?.isThread() ? interaction.channel : undefined;\n\t\tconst source = {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId: thread?.parentId ?? interaction.channelId ?? interaction.user.id,\n\t\t\tthreadId: thread?.id,\n\t\t\tchatType: thread ? (\"thread\" as const) : interaction.inGuild() ? (\"channel\" as const) : (\"dm\" as const),\n\t\t\tuserId: interaction.user.id,\n\t\t\tuserName: interaction.user.username,\n\t\t};\n\t\tconst cfg = loadGatewayConfig();\n\t\tconst authorized = isAuthorized(source, cfg, createPairingStore());\n\t\tif (interaction.isAutocomplete()) {\n\t\t\tif (!authorized || !isGatewayOwner(source, cfg)) {\n\t\t\t\tawait interaction.respond([]);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst query = String(interaction.options.getFocused()).toLowerCase();\n\t\t\tlet values: string[] = [];\n\t\t\tif (interaction.commandName === \"mode\") values = [\"manual\", \"yolo\", \"plan\", \"auto\"];\n\t\t\tif (this.commandSuggestions && [\"thinking\", \"model\"].includes(interaction.commandName)) {\n\t\t\t\tvalues = [...(await this.commandSuggestions(source, interaction.commandName))];\n\t\t\t}\n\t\t\tif (interaction.commandName === \"project\") values = cfg.projectRoots ?? [];\n\t\t\tif (interaction.commandName === \"sessions\") {\n\t\t\t\tconst { SessionManager } = await import(\"../../core/session-manager.ts\");\n\t\t\t\tvalues = (\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tSessionManager.listAll(),\n\t\t\t\t\t\tnew Promise<[]>((resolve) => setTimeout(() => resolve([]), 1500)),\n\t\t\t\t\t])\n\t\t\t\t).map((s) => s.name || s.id);\n\t\t\t}\n\t\t\tawait interaction.respond(\n\t\t\t\tvalues\n\t\t\t\t\t.filter((s) => s.toLowerCase().includes(query))\n\t\t\t\t\t.slice(0, 25)\n\t\t\t\t\t.map((value) => ({ name: value.slice(0, 100), value: value.slice(0, 100) })),\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\tawait interaction.reply({\n\t\t\tcontent: authorized\n\t\t\t\t? \"Command received.\"\n\t\t\t\t: \"Pair your account before using this command. Send the bot a private message for a pairing code.\",\n\t\t\tephemeral: true,\n\t\t});\n\t\tif (authorized)\n\t\t\tthis.handler?.({\n\t\t\t\ttext: `/${interaction.commandName}${interaction.options.getString(\"args\") ? ` ${interaction.options.getString(\"args\")}` : \"\"}`,\n\t\t\t\tsource,\n\t\t\t\tmessageId: \"\",\n\t\t\t\tmetadata: { mentionedBot: true, nativeCommand: true },\n\t\t\t});\n\t\treturn true;\n\t}\n\n\tasync sendFile(chatId: string, file: string, opts?: SendOptions): Promise<SendResult> {\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel?.send) return { success: false, error: \"Channel is unavailable.\" };\n\t\ttry {\n\t\t\tconst message = await channel.send({ content: \"\", files: [file] });\n\t\t\treturn { success: true, messageId: message.id };\n\t\t} catch (error) {\n\t\t\treturn { success: false, error: errMessage(error) };\n\t\t}\n\t}\n\n\tprivate async handleInteraction(interaction: DiscordButtonInteractionLike): Promise<void> {\n\t\tif (!this.callbackHandler) return;\n\t\tif (!interaction.isButton()) return;\n\t\tconst event = buttonInteractionToEvent(interaction);\n\t\tif (!event) return;\n\t\tthis.callbackInteractions.set(interaction.id, interaction);\n\t\tif (this.callbackInteractions.size > CALLBACK_INTERACTION_CAP) {\n\t\t\tconst oldest = this.callbackInteractions.keys().next().value;\n\t\t\tif (oldest !== undefined) this.callbackInteractions.delete(oldest);\n\t\t}\n\t\tthis.callbackHandler(event);\n\t}\n\n\t/** Best-effort: fill replyToText from the referenced message when it wasn't cached. */\n\tprivate async resolveReplyToText(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (event.replyToText !== undefined) return;\n\t\tconst refId = message.reference?.messageId;\n\t\tif (refId == null) return;\n\t\tconst messages = message.channel.messages;\n\t\tif (!messages) return;\n\t\ttry {\n\t\t\tconst referenced = await messages.fetch(refId);\n\t\t\tif (referenced?.content) event.replyToText = referenced.content.slice(0, REPLY_TO_TEXT_MAX);\n\t\t} catch {\n\t\t\t// unknown/deleted reference — no reply context\n\t\t}\n\t}\n\n\t/** autoThread: spin a thread off a bot-mentioning guild-text message and re-target the event. */\n\tprivate async maybeAutoThread(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (!this.cfg.autoThread) return;\n\t\tif (event.metadata?.mentionedBot !== true) return;\n\t\t// Only guild TEXT channels: DMs can't thread and messages already inside\n\t\t// a thread don't need another one.\n\t\tif (message.channel.type !== ChannelType.GuildText) return;\n\t\tif (typeof message.startThread !== \"function\") return;\n\t\ttry {\n\t\t\tconst thread = await message.startThread({\n\t\t\t\tname: (event.text || \"conversation\").slice(0, THREAD_NAME_MAX),\n\t\t\t\tautoArchiveDuration: 60,\n\t\t\t});\n\t\t\tthis.participatingThreads.add(thread.id);\n\t\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t\t}\n\t\t\tevent.source = { ...event.source, chatType: \"thread\", chatId: message.channel.id, threadId: thread.id };\n\t\t} catch {\n\t\t\t// e.g. missing CREATE_THREADS permission — respond in-channel instead.\n\t\t}\n\t}\n\n\tprivate debounceKey(event: MessageEvent): string {\n\t\treturn `${event.source.chatId}:${event.source.threadId ?? \"\"}:${event.source.userId}`;\n\t}\n\n\tprivate dispatchEvent(event: MessageEvent): void {\n\t\tconst key = this.debounceKey(event);\n\t\tif (event.text.trimStart().startsWith(\"/\")) {\n\t\t\t// Commands must not merge: flush any pending text, emit immediately.\n\t\t\tthis.flushPending(key);\n\t\t\tthis.handler?.(event);\n\t\t\treturn;\n\t\t}\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tthis.scheduler.clearTimeout(existing.timer);\n\t\t\t// Merge: earlier text first, latest messageId (and other fields) win.\n\t\t\tconst mergedAttachments = [...(existing.event.attachments ?? []), ...(event.attachments ?? [])];\n\t\t\texisting.event = {\n\t\t\t\t...event,\n\t\t\t\ttext: `${existing.event.text}\\n${event.text}`,\n\t\t\t\t...(mergedAttachments.length > 0 ? { attachments: mergedAttachments } : {}),\n\t\t\t};\n\t\t\texisting.timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t} else {\n\t\t\tconst timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t\tthis.pending.set(key, { event, timer });\n\t\t}\n\t}\n\n\tprivate flushPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t\tthis.handler?.(entry.event);\n\t}\n\n\tprivate cancelPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Outbound\n\t// -----------------------------------------------------------------------\n\n\tprivate async fetchChannel(id: string): Promise<DiscordChannelLike | null> {\n\t\tif (this.channelCache.has(id)) return this.channelCache.get(id) ?? null;\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\tconst channel = await this.client.channels.fetch(id);\n\t\t\tthis.channelCache.set(id, channel);\n\t\t\tif (this.channelCache.size > CHANNEL_CACHE_CAP) {\n\t\t\t\tconst oldest = this.channelCache.keys().next().value;\n\t\t\t\tif (oldest !== undefined) this.channelCache.delete(oldest);\n\t\t\t}\n\t\t\treturn channel;\n\t\t} catch {\n\t\t\tthis.channelCache.set(id, null);\n\t\t\tif (this.channelCache.size > CHANNEL_CACHE_CAP) {\n\t\t\t\tconst oldest = this.channelCache.keys().next().value;\n\t\t\t\tif (oldest !== undefined) this.channelCache.delete(oldest);\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate recordSent(messageId: string, channel: DiscordChannelLike): void {\n\t\tif (THREAD_TYPES.includes(channel.type)) this.participatingThreads.add(channel.id);\n\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t}\n\t\tthis.sentMessageChannels.set(messageId, channel.id);\n\t\tif (this.sentMessageChannels.size > SENT_CHANNEL_CAP) {\n\t\t\tconst oldest = this.sentMessageChannels.keys().next().value;\n\t\t\tif (oldest !== undefined) this.sentMessageChannels.delete(oldest);\n\t\t}\n\t}\n\n\tprivate async trySend(\n\t\tchannel: DiscordChannelLike,\n\t\tpayload: { content: string; reply?: { messageReference: string }; components?: unknown[] },\n\t): Promise<SendResult> {\n\t\tif (typeof channel.send !== \"function\") {\n\t\t\treturn { success: false, error: `channel ${channel.id} is not sendable` };\n\t\t}\n\t\ttry {\n\t\t\tconst sent = await channel.send(payload);\n\t\t\tthis.recordSent(sent.id, channel);\n\t\t\treturn { success: true, messageId: sent.id };\n\t\t} catch (err) {\n\t\t\t// 429s are queued internally by discord.js and don't surface here.\n\t\t\treturn { success: false, error: errMessage(err), retryable: discordErrorCode(err) !== ERR_INVALID_FORM_BODY };\n\t\t}\n\t}\n\n\tasync send(chatId: string, text: string, opts?: SendOptions): Promise<SendResult> {\n\t\t// A completed send ends the typing bubble — stop the refresher (see sendTyping).\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\t// The reply target may be deleted/unknown — fall back to a plain send.\n\t\t\treturn this.trySend(channel, { content: text });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync sendButtons(chatId: string, text: string, buttons: ButtonSpec[][], opts?: SendOptions): Promise<SendResult> {\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t\tcomponents: buildDiscordComponents(buttons),\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\treturn this.trySend(channel, { content: text, components: buildDiscordComponents(buttons) });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync answerCallback(id: string, text?: string): Promise<void> {\n\t\tconst interaction = this.callbackInteractions.get(id);\n\t\tif (!interaction) return;\n\t\tthis.callbackInteractions.delete(id);\n\t\ttry {\n\t\t\tif (text !== undefined && interaction.reply) {\n\t\t\t\tawait interaction.reply({ content: text, ephemeral: true });\n\t\t\t} else {\n\t\t\t\tawait interaction.deferUpdate();\n\t\t\t}\n\t\t} catch {\n\t\t\t// best-effort acknowledgment\n\t\t}\n\t}\n\n\tasync editMessage(chatId: string, messageId: string, text: string, buttons?: ButtonSpec[][]): Promise<SendResult> {\n\t\t// The router passes only chatId; thread messages live in their own\n\t\t// channel, so prefer the channel recorded at send() time.\n\t\tconst channel = await this.fetchChannel(this.sentMessageChannels.get(messageId) ?? chatId);\n\t\tif (!channel?.messages) {\n\t\t\treturn { success: false, error: `channel ${this.sentMessageChannels.get(messageId) ?? chatId} not found` };\n\t\t}\n\t\ttry {\n\t\t\tconst message = await channel.messages.fetch(messageId);\n\t\t\tif (!message || typeof message.edit !== \"function\") {\n\t\t\t\treturn { success: false, error: `message ${messageId} not found` };\n\t\t\t}\n\t\t\tconst editOptions: { content: string; components?: unknown[] } = { content: text };\n\t\t\tif (buttons !== undefined) editOptions.components = buildDiscordComponents(buttons);\n\t\t\tawait message.edit(editOptions);\n\t\t\treturn { success: true, messageId };\n\t\t} catch (err) {\n\t\t\tconst code = discordErrorCode(err);\n\t\t\tif (code === ERR_UNKNOWN_MESSAGE) return { success: false, error: errMessage(err) }; // terminal\n\t\t\treturn { success: false, error: errMessage(err), retryable: true };\n\t\t}\n\t}\n\n\tasync sendTyping(chatId: string, threadId?: string): Promise<void> {\n\t\t// Self-contained typing refresher (see file header): re-send the action\n\t\t// every 8s until send() completes for this target or the 2min safety net\n\t\t// fires — all errors swallowed.\n\t\tconst key = typingKey(chatId, threadId);\n\t\tconst targetId = threadId ?? chatId;\n\t\tawait this.doTyping(targetId);\n\t\tconst existing = this.typing.get(key);\n\t\tif (existing) {\n\t\t\texisting.lastRefresh = this.now();\n\t\t\treturn;\n\t\t}\n\t\tconst interval = this.scheduler.setInterval(() => {\n\t\t\tconst entry = this.typing.get(key);\n\t\t\tif (!entry) return;\n\t\t\tif (this.now() - entry.lastRefresh > TYPING_SAFETY_MS) {\n\t\t\t\tthis.stopTyping(key);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvoid this.doTyping(targetId);\n\t\t}, TYPING_INTERVAL_MS);\n\t\tthis.typing.set(key, { interval, lastRefresh: this.now() });\n\t}\n\n\tprivate async doTyping(channelId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst channel = await this.fetchChannel(channelId);\n\t\t\tawait channel?.sendTyping?.();\n\t\t} catch {\n\t\t\t// typing is cosmetic — never surface\n\t\t}\n\t}\n\n\tprivate stopTyping(key: string): void {\n\t\tconst entry = this.typing.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearInterval(entry.interval);\n\t\tthis.typing.delete(key);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../../src/gateway/adapters/discord.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway Discord adapter (Phase 3).\n *\n * Built on discord.js v14 (gateway websocket; the library owns reconnects and\n * REST rate-limit queuing internally, so neither is handled here). No voice,\n * no native slash-command registration — commands arrive as plain message\n * text, same as Telegram.\n *\n * Intents: Guilds, GuildMessages, DirectMessages, MessageContent. GuildMembers\n * is deliberately NOT requested: it is a privileged intent, and asking for it\n * without the dev-portal opt-in makes Discord refuse the identify — the bot\n * connects and immediately goes offline. v1 has no role-based config\n * (SessionSource.roleAuthorized stays undefined), so there is nothing to gain.\n *\n * Inbound: messageToEvent() is the pure message → MessageEvent mapping\n * (dm/group/thread chat types, <@id>/<@!id> mention detection with the mention\n * stripped, thread-participation counts as a mention, own + all-bot + system +\n * attachment-only messages dropped, other-mention-without-us dropped outside\n * DMs — hermes DISCORD_IGNORE_NO_MENTION). A module-level seen-id Set\n * (cap 500, FIFO evict) drops gateway-resume replays. cfg.ignoredChannels is\n * enforced adapter-side (channel id or parent id; blacklist wins over\n * mentions). Consecutive texts from the same chat/thread/user are debounced\n * 600ms and merged (\"\\n\"-joined, latest messageId kept); texts starting with\n * \"/\" flush immediately so commands never merge.\n * Button interactions are routed via interactionCreate → buttonInteractionToEvent().\n *\n * chatId/threadId mapping (pairs with session-keys.ts buildSessionKey):\n * dm → chatId = channel.id → agent:main:discord:dm:<channelId>\n * thread → chatId = PARENT channel id, threadId = → agent:main:discord:thread:<parentId>:<threadId>\n * thread channel id (shared session, no userId)\n * guild text/announcement → chatId = channel.id → agent:main:discord:group:<channelId>[:<userId>]\n * (chatType \"group\"; userId appended when groupSessionsPerUser is on)\n *\n * autoThread (cfg.discord.autoThread, default true): a bot-mentioning message\n * in a guild TEXT channel gets a thread spun off it (startThread, 60min\n * archive); the event is re-targeted to the thread (chatType \"thread\", chatId\n * parent id, threadId thread id). On failure the event stays in-channel.\n *\n * Thread participation: an in-memory Set of thread ids the bot has sent to\n * (recorded on every successful send() into a thread and on autoThread\n * creation); messages in those threads count as mentionedBot so follow-ups\n * don't need a re-mention. NOT persisted in v1 (hermes persists it).\n *\n * Outbound: channel.send({ content, reply: { messageReference } }) with a\n * one-shot plain-send fallback when the reply reference fails. DiscordAPIError\n * 50035 (invalid form body, e.g. overlong content) is reported non-retryable;\n * 429s are queued internally by discord.js; everything else is retryable.\n * editMessage resolves the channel via a messageId → channel map recorded at\n * send() time (router passes only chatId, but thread messages live in their\n * own channel); Unknown Message (10008) is a terminal { success: false }.\n * Message splitting is NOT done here — the router owns that (text.ts).\n *\n * Buttons: ActionRowBuilder rows of ButtonBuilder components (max 5×5).\n * answerCallback acknowledges the interaction; a text argument produces an\n * ephemeral reply (used for unauthorized/expired taps), otherwise deferUpdate().\n *\n * Typing: sendTyping() is a self-contained refresher — Discord typing bubbles\n * last ~10s, so the action is re-sent on an unref'd 8s interval per target.\n * The interval stops when send() completes for that target or after 2 minutes\n * without a refresh (safety net). All errors swallowed.\n *\n * Testability: the discord.js Client goes through an injectable clientFactory\n * (tests inject a minimal fake), timers through an injectable Scheduler, and\n * messageToEvent() operates on the structural DiscordMessageLike — plain mock\n * objects, no real Client construction.\n */\n\nimport {\n\tActionRowBuilder,\n\tApplicationCommandOptionType,\n\tButtonBuilder,\n\tButtonStyle,\n\tChannelType,\n\tClient,\n\tEvents,\n\tGatewayIntentBits,\n\ttype Interaction,\n} from \"discord.js\";\nimport { isAuthorized, isGatewayOwner } from \"../authz.ts\";\nimport { type DiscordConfig, loadGatewayConfig } from \"../config.ts\";\nimport { downloadAttachment } from \"../download.ts\";\nimport { createPairingStore } from \"../pairing.ts\";\nimport type {\n\tButtonSpec,\n\tCallbackEvent,\n\tChatType,\n\tInboundAttachment,\n\tMessageEvent,\n\tPlatformAdapter,\n\tSendOptions,\n\tSendResult,\n} from \"../types.ts\";\nimport type { Scheduler } from \"./telegram.ts\";\n\nconst DEFAULT_DEBOUNCE_MS = 600;\nconst TYPING_INTERVAL_MS = 8000; // Discord typing lasts ~10s\nconst TYPING_SAFETY_MS = 120_000; // auto-stop after 2min without a refresh\nconst REPLY_TO_TEXT_MAX = 500;\nconst THREAD_NAME_MAX = 40;\nconst SEEN_CAP = 500;\nconst SENT_CHANNEL_CAP = 500;\nconst CALLBACK_INTERACTION_CAP = 500;\nconst CHANNEL_CACHE_CAP = 500;\nconst PARTICIPATING_THREAD_CAP = 500;\nconst ERR_INVALID_FORM_BODY = 50035; // e.g. content over 2000 chars — non-retryable\nconst ERR_UNKNOWN_MESSAGE = 10008; // deleted/unknown message — terminal\n\nconst THREAD_TYPES: readonly number[] = [\n\tChannelType.PublicThread,\n\tChannelType.PrivateThread,\n\tChannelType.AnnouncementThread,\n];\n\n// ---------------------------------------------------------------------------\n// Structural discord.js shapes (only the fields the adapter reads)\n// ---------------------------------------------------------------------------\n\nexport interface DiscordSentMessageLike {\n\tid: string;\n}\n\nexport interface DiscordFetchedMessageLike {\n\tcontent?: string;\n\tedit?(options: string | { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordChannelLike {\n\tid: string;\n\ttype: number;\n\tparentId?: string | null;\n\tsend?(payload: {\n\t\tcontent: string;\n\t\tfiles?: string[];\n\t\treply?: { messageReference: string };\n\t\tcomponents?: unknown[];\n\t}): Promise<DiscordSentMessageLike>;\n\tsendTyping?(): Promise<unknown>;\n\tmessages?: {\n\t\tcache?: { get(id: string): { content?: string } | undefined };\n\t\tfetch(id: string): Promise<DiscordFetchedMessageLike | null>;\n\t};\n}\n\nexport interface DiscordButtonInteractionLike {\n\tid: string;\n\tcustomId: string;\n\tuser: { id: string; username?: string };\n\tmessage: { id: string; channel: DiscordChannelLike };\n\tisButton(): boolean;\n\tdeferUpdate(): Promise<unknown>;\n\treply?(options: { content: string; ephemeral: boolean }): Promise<unknown>;\n\tupdate?(options: { content: string; components?: unknown[] }): Promise<unknown>;\n}\n\nexport interface DiscordAttachmentLike {\n\tid: string;\n\t/** Direct CDN URL for the attachment content. */\n\turl: string;\n\tcontentType?: string;\n\tname?: string;\n\tsize?: number;\n}\n\nexport interface DiscordMessageLike {\n\tid: string;\n\tcontent?: string;\n\tsystem?: boolean;\n\tauthor: { id: string; bot?: boolean; username?: string };\n\tchannel: DiscordChannelLike;\n\treference?: { messageId?: string | null } | null;\n\tmentions?: {\n\t\thas(user: { id: string }): boolean;\n\t\trepliedUser?: { id: string } | null;\n\t};\n\tstartThread?(opts: { name: string; autoArchiveDuration: number }): Promise<DiscordChannelLike>;\n\tattachments?: DiscordAttachmentLike[];\n}\n\n/** Minimal Client surface; the real discord.js Client satisfies this. */\nexport interface DiscordClientLike {\n\tapplication?: Pick<NonNullable<Client[\"application\"]>, \"commands\"> | null;\n\tuser: { id: string } | null;\n\tlogin(token: string): Promise<unknown>;\n\ton(event: string, listener: (arg: unknown) => void): unknown;\n\tonce(event: string, listener: () => void): unknown;\n\tdestroy(): void;\n\tchannels: { fetch(id: string): Promise<DiscordChannelLike | null> };\n}\n\nexport type DiscordClientFactory = () => DiscordClientLike;\n\n// ---------------------------------------------------------------------------\n// Pure mapping + small helpers\n// ---------------------------------------------------------------------------\n\nfunction errMessage(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/** lunr: download an inbound Discord attachment from its CDN URL. */\n/** Discord REST error code (DiscordAPIError.code) when present. */\nfunction discordErrorCode(err: unknown): number | undefined {\n\tconst code = (err as { code?: unknown } | null)?.code;\n\treturn typeof code === \"number\" ? code : undefined;\n}\n\nexport interface MessageToEventOptions {\n\t/** Channel ids (or parent ids for threads) to drop outright — blacklist wins over mentions. */\n\tignoredChannels?: readonly string[];\n\t/** Threads the bot has spoken in; messages there count as mentionedBot. */\n\tparticipatingThreads?: ReadonlySet<string>;\n}\n\nfunction isImageContentType(contentType: string | undefined): boolean {\n\treturn typeof contentType === \"string\" && contentType.split(\";\")[0]?.trim().toLowerCase().startsWith(\"image/\");\n}\n\n/** lunr: collect inbound image attachments (contentType image/*) from a Discord message. */\nexport function collectDiscordImageAttachments(message: DiscordMessageLike): DiscordAttachmentLike[] {\n\treturn Array.from(message.attachments?.values() ?? []).filter((attachment) =>\n\t\tisImageContentType(attachment.contentType),\n\t);\n}\n\n/**\n * Map a Discord message to a MessageEvent; null = drop (own message, any bot,\n * system message, ignored channel, unsupported channel type, or a non-DM\n * message mentioning others but not us). Messages with no text but image\n * attachments are kept (v1: images inbound).\n */\nexport function messageToEvent(\n\tmessage: DiscordMessageLike,\n\tbotUser: { id: string },\n\toptions: MessageToEventOptions = {},\n): MessageEvent | null {\n\tconst author = message.author;\n\tif (!author) return null;\n\tif (author.id === botUser.id) return null; // own messages\n\tif (author.bot) return null; // all bots (same simplification as telegram)\n\tif (message.system) return null;\n\tconst content = (message.content ?? \"\").trim();\n\tconst imageAttachments = collectDiscordImageAttachments(message);\n\tif (content === \"\" && imageAttachments.length === 0 && !Array.from(message.attachments?.values() ?? []).length)\n\t\treturn null;\n\n\tconst channel = message.channel;\n\tconst ignored = options.ignoredChannels ?? [];\n\tif (ignored.includes(channel.id) || (channel.parentId != null && ignored.includes(channel.parentId))) {\n\t\treturn null; // blacklist wins over mentions\n\t}\n\n\t// chatId/threadId mapping — see the file header for the session-key pairing.\n\tlet chatType: ChatType;\n\tlet chatId: string;\n\tlet threadId: string | undefined;\n\tif (channel.type === ChannelType.DM) {\n\t\tchatType = \"dm\";\n\t\tchatId = channel.id;\n\t} else if (THREAD_TYPES.includes(channel.type)) {\n\t\tchatType = \"thread\";\n\t\tchatId = channel.parentId ?? channel.id;\n\t\tthreadId = channel.id;\n\t} else if (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\tchatType = \"group\";\n\t\tchatId = channel.id;\n\t} else {\n\t\treturn null; // voice/category/forum containers etc. — unsupported in v1\n\t}\n\n\tconst explicitMention = message.mentions?.has(botUser) === true;\n\tconst participating = threadId !== undefined && options.participatingThreads?.has(threadId) === true;\n\tconst mentionedBot = explicitMention || participating;\n\n\t// Multi-agent filter (hermes DISCORD_IGNORE_NO_MENTION): outside DMs, a\n\t// message that mentions other users/bots but not us is not for us.\n\tif (!mentionedBot && chatType !== \"dm\") {\n\t\tconst mentionedIds = [...content.matchAll(/<@!?(\\d+)>/g)].map((m) => m[1]);\n\t\tif (mentionedIds.some((id) => id !== botUser.id)) return null;\n\t}\n\n\tlet text = content;\n\tif (explicitMention) {\n\t\ttext = text.replace(new RegExp(`<@!?${botUser.id}>`, \"g\"), \"\").trim();\n\t}\n\n\tconst refId = message.reference?.messageId;\n\tconst cachedReply = refId != null ? channel.messages?.cache?.get(refId)?.content : undefined;\n\n\treturn {\n\t\ttext,\n\t\tmessageId: message.id,\n\t\treplyToText: cachedReply !== undefined ? cachedReply.slice(0, REPLY_TO_TEXT_MAX) : undefined,\n\t\tsource: {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId,\n\t\t\tchatType,\n\t\t\tuserId: author.id,\n\t\t\tuserName: author.username,\n\t\t\tthreadId,\n\t\t},\n\t\tmetadata: mentionedBot ? { mentionedBot: true } : undefined,\n\t};\n}\n\nfunction mapDiscordChannel(\n\tchannel: DiscordChannelLike,\n): { chatType: ChatType; chatId: string; threadId?: string } | null {\n\tif (channel.type === ChannelType.DM) {\n\t\treturn { chatType: \"dm\", chatId: channel.id };\n\t}\n\tif (THREAD_TYPES.includes(channel.type)) {\n\t\treturn { chatType: \"thread\", chatId: channel.parentId ?? channel.id, threadId: channel.id };\n\t}\n\tif (channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildAnnouncement) {\n\t\treturn { chatType: \"group\", chatId: channel.id };\n\t}\n\treturn null;\n}\n\nexport function buttonInteractionToEvent(interaction: DiscordButtonInteractionLike): CallbackEvent | null {\n\tconst channel = interaction.message.channel;\n\tconst mapped = mapDiscordChannel(channel);\n\tif (!mapped) return null;\n\treturn {\n\t\tid: interaction.id,\n\t\tchatId: mapped.chatId,\n\t\tmessageId: interaction.message.id,\n\t\tuserId: interaction.user.id,\n\t\tuserName: interaction.user.username,\n\t\tdata: interaction.customId,\n\t\tthreadId: mapped.threadId,\n\t};\n}\n\nfunction buildDiscordComponents(buttons: ButtonSpec[][]): unknown[] {\n\tconst rows: ActionRowBuilder<ButtonBuilder>[] = [];\n\tfor (const row of buttons) {\n\t\tconst actionRow = new ActionRowBuilder<ButtonBuilder>();\n\t\tfor (const button of row) {\n\t\t\tconst style = button.data === \"\" ? ButtonStyle.Secondary : ButtonStyle.Secondary;\n\t\t\tconst builder = new ButtonBuilder().setCustomId(button.data).setLabel(button.label).setStyle(style);\n\t\t\tif (button.data === \"\") builder.setDisabled(true);\n\t\t\tactionRow.addComponents(builder);\n\t\t}\n\t\trows.push(actionRow);\n\t}\n\treturn rows;\n}\n\n// ---------------------------------------------------------------------------\n// Gateway-resume replay dedup (module-level: one process = one bot connection)\n// ---------------------------------------------------------------------------\n\nconst seenMessageIds = new Set<string>();\n\n/** Returns true when this id was already seen (drop the replay). */\nfunction markMessageSeen(id: string): boolean {\n\tif (seenMessageIds.has(id)) return true;\n\tseenMessageIds.add(id);\n\tif (seenMessageIds.size > SEEN_CAP) {\n\t\tconst oldest = seenMessageIds.values().next().value; // Sets iterate in insertion order\n\t\tif (oldest !== undefined) seenMessageIds.delete(oldest);\n\t}\n\treturn false;\n}\n\n/** Test hook: reset RESUME dedup state between tests. */\nexport function resetSeenMessageIds(): void {\n\tseenMessageIds.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\nconst defaultScheduler: Scheduler = {\n\tsetTimeout: (fn, ms) => {\n\t\tconst handle = setTimeout(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearTimeout: (handle) => clearTimeout(handle),\n\tsetInterval: (fn, ms) => {\n\t\tconst handle = setInterval(fn, ms);\n\t\thandle.unref?.();\n\t\treturn handle;\n\t},\n\tclearInterval: (handle) => clearInterval(handle),\n};\n\nfunction defaultClientFactory(): DiscordClientLike {\n\t// No GatewayIntentBits.GuildMembers — see the file header for the\n\t// privileged-intent trap (bot stays offline without the portal opt-in).\n\treturn new Client({\n\t\tintents: [\n\t\t\tGatewayIntentBits.Guilds,\n\t\t\tGatewayIntentBits.GuildMessages,\n\t\t\tGatewayIntentBits.DirectMessages,\n\t\t\tGatewayIntentBits.MessageContent,\n\t\t],\n\t}) as unknown as DiscordClientLike;\n}\n\nexport interface DiscordAdapterOptions {\n\t/** Injected Client factory (tests). When omitted, a real discord.js Client is created. */\n\tclientFactory?: DiscordClientFactory;\n\t/** lunr: injected image downloader for inbound attachments (tests). Defaults to fetch(url). */\n\tdownloadAttachment?: (url: string) => Promise<Uint8Array>;\n\t/** lunr: skip inbound media larger than this many bytes. Defaults to 10MB. */\n\tmaxMediaBytes?: number;\n\tscheduler?: Scheduler;\n\tdebounceMs?: number;\n\tnow?: () => number;\n}\n\ninterface PendingEntry {\n\tevent: MessageEvent;\n\ttimer: ReturnType<typeof setTimeout>;\n}\n\ninterface TypingEntry {\n\tinterval: ReturnType<typeof setInterval>;\n\tlastRefresh: number;\n}\n\nfunction typingKey(chatId: string, threadId?: string): string {\n\treturn `${chatId}:${threadId ?? \"\"}`;\n}\n\nexport class DiscordAdapter implements PlatformAdapter {\n\treadonly platform = \"discord\";\n\treadonly maxMessageLength = 2000;\n\n\tprivate readonly cfg: DiscordConfig;\n\tprivate readonly clientFactory: DiscordClientFactory;\n\tprivate readonly downloadAttachment: (url: string) => Promise<Uint8Array>;\n\tprivate readonly maxMediaBytes: number;\n\tprivate readonly scheduler: Scheduler;\n\tprivate readonly debounceMs: number;\n\tprivate readonly now: () => number;\n\tprivate client?: DiscordClientLike;\n\tprivate botUserId?: string;\n\tprivate handler?: (event: MessageEvent) => void;\n\tprivate commandSuggestions?: Parameters<NonNullable<PlatformAdapter[\"setCommandSuggestions\"]>>[0];\n\tprivate callbackHandler?: (event: CallbackEvent) => void;\n\tprivate readonly pending = new Map<string, PendingEntry>();\n\tprivate readonly typing = new Map<string, TypingEntry>();\n\tprivate readonly channelCache = new Map<string, DiscordChannelLike | null>();\n\t/** Threads the bot has spoken in (session memory only; hermes persists this). */\n\tprivate readonly participatingThreads = new Set<string>();\n\t/** messageId → channel it was sent to, so editMessage finds thread messages. */\n\tprivate readonly sentMessageChannels = new Map<string, string>();\n\t/** callbackId → interaction, so answerCallback can acknowledge the click. */\n\tprivate readonly callbackInteractions = new Map<string, DiscordButtonInteractionLike>();\n\n\tconstructor(cfg: DiscordConfig, options: DiscordAdapterOptions = {}) {\n\t\tthis.cfg = cfg;\n\t\tthis.scheduler = options.scheduler ?? defaultScheduler;\n\t\tthis.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;\n\t\tthis.now = options.now ?? Date.now;\n\t\tif (options.clientFactory) {\n\t\t\tthis.clientFactory = options.clientFactory;\n\t\t} else {\n\t\t\tif (!cfg.token) {\n\t\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t\t}\n\t\t\tthis.clientFactory = defaultClientFactory;\n\t\t}\n\t\tthis.downloadAttachment = options.downloadAttachment ?? downloadAttachment;\n\t\tthis.maxMediaBytes = options.maxMediaBytes ?? 10 * 1024 * 1024;\n\t}\n\n\tsetCommandSuggestions(\n\t\thandler: (source: MessageEvent[\"source\"], command: string) => Promise<readonly string[]>,\n\t): void {\n\t\tthis.commandSuggestions = handler;\n\t}\n\n\tonMessage(handler: (event: MessageEvent) => void): void {\n\t\tthis.handler = handler;\n\t}\n\n\tonCallback(handler: (event: CallbackEvent) => void): void {\n\t\tthis.callbackHandler = handler;\n\t}\n\n\tasync connect(): Promise<boolean> {\n\t\tif (!this.cfg.token) {\n\t\t\tthrow new Error(\"DiscordAdapter: no bot token (set discord.token or LUNR_DISCORD_BOT_TOKEN)\");\n\t\t}\n\t\tconst client = this.clientFactory();\n\t\tthis.client = client;\n\t\tclient.on(Events.MessageCreate, (message) => {\n\t\t\tvoid this.handleMessage(message as DiscordMessageLike).catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord message handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tclient.on(Events.InteractionCreate, (interaction) => {\n\t\t\tconst event = interaction as Interaction;\n\t\t\tconst handling = event.isButton()\n\t\t\t\t? this.handleInteraction(interaction as DiscordButtonInteractionLike)\n\t\t\t\t: this.handleNativeCommand(event);\n\t\t\tvoid handling.catch((err) => {\n\t\t\t\tconsole.error(`[gateway] discord interaction handling error: ${errMessage(err)}`);\n\t\t\t});\n\t\t});\n\t\tconst ready = new Promise<void>((resolve) => client.once(Events.ClientReady, () => resolve()));\n\t\ttry {\n\t\t\tawait client.login(this.cfg.token);\n\t\t} catch (err) {\n\t\t\tconsole.error(`[gateway] discord login failed: ${errMessage(err)}`);\n\t\t\treturn false;\n\t\t}\n\t\tawait ready;\n\t\tthis.botUserId = client.user?.id;\n\t\tif (this.botUserId === undefined) {\n\t\t\tconsole.error(\"[gateway] discord: client ready without a user — cannot map messages\");\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tfor (const key of [...this.pending.keys()]) this.cancelPending(key);\n\t\tfor (const key of [...this.typing.keys()]) this.stopTyping(key);\n\t\tthis.channelCache.clear();\n\t\tthis.callbackInteractions.clear();\n\t\tthis.client?.destroy();\n\t\tthis.client = undefined;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Inbound dispatch + debounce\n\t// -----------------------------------------------------------------------\n\n\tprivate async handleMessage(message: DiscordMessageLike): Promise<void> {\n\t\tif (!this.botUserId || !this.handler) return;\n\t\tif (markMessageSeen(message.id)) return; // gateway-resume replay\n\t\tconst event = messageToEvent(\n\t\t\tmessage,\n\t\t\t{ id: this.botUserId },\n\t\t\t{\n\t\t\t\tignoredChannels: this.cfg.ignoredChannels,\n\t\t\t\tparticipatingThreads: this.participatingThreads,\n\t\t\t},\n\t\t);\n\t\tif (!event) return;\n\t\tif (!isAuthorized(event.source, { ...loadGatewayConfig(), discord: this.cfg }, createPairingStore())) {\n\t\t\tthis.dispatchEvent(event);\n\t\t\treturn;\n\t\t}\n\t\tawait this.resolveReplyToText(message, event);\n\t\tawait this.maybeAutoThread(message, event);\n\t\tawait this.downloadAttachments(message, event);\n\t\tthis.dispatchEvent(event);\n\t}\n\n\t/** Download and encode attachments after authorization. */\n\tprivate async downloadAttachments(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tconst imageAttachments = Array.from(message.attachments?.values() ?? []);\n\t\tif (imageAttachments.length === 0) return;\n\t\tconst attachments: InboundAttachment[] = [];\n\t\tfor (const attachment of imageAttachments) {\n\t\t\tif (attachment.size !== undefined && attachment.size > this.maxMediaBytes) continue;\n\t\t\ttry {\n\t\t\t\tconst bytes = await this.downloadAttachment(attachment.url);\n\t\t\t\tif (bytes.byteLength > this.maxMediaBytes) continue;\n\t\t\t\tattachments.push({\n\t\t\t\t\tdata: Buffer.from(bytes).toString(\"base64\"),\n\t\t\t\t\tmimeType: attachment.contentType ?? \"application/octet-stream\",\n\t\t\t\t\t...(attachment.name ? { filename: attachment.name } : {}),\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Media download failed — drop this attachment, keep the text turn.\n\t\t\t}\n\t\t}\n\t\tif (attachments.length > 0) event.attachments = attachments;\n\t}\n\n\tasync registerCommands(commands: { name: string; description: string }[]): Promise<void> {\n\t\tif (!this.client?.application) return;\n\t\tawait this.client.application.commands.set(\n\t\t\tcommands.slice(0, 95).map((command) => ({\n\t\t\t\tname: command.name,\n\t\t\t\tdescription: command.description.slice(0, 100),\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: \"args\",\n\t\t\t\t\t\tdescription: \"Command arguments or search\",\n\t\t\t\t\t\ttype: ApplicationCommandOptionType.String,\n\t\t\t\t\t\trequired: false,\n\t\t\t\t\t\tautocomplete: [\"mode\", \"thinking\", \"project\", \"sessions\"].includes(command.name),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t})),\n\t\t);\n\t}\n\n\tprivate async handleNativeCommand(interaction: Interaction): Promise<boolean> {\n\t\tif (!interaction.isChatInputCommand?.() && !interaction.isAutocomplete?.()) return false;\n\t\tif (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) return false;\n\t\tconst thread = interaction.channel?.isThread() ? interaction.channel : undefined;\n\t\tconst source = {\n\t\t\tplatform: \"discord\",\n\t\t\tchatId: thread?.parentId ?? interaction.channelId ?? interaction.user.id,\n\t\t\tthreadId: thread?.id,\n\t\t\tchatType: thread ? (\"thread\" as const) : interaction.inGuild() ? (\"channel\" as const) : (\"dm\" as const),\n\t\t\tuserId: interaction.user.id,\n\t\t\tuserName: interaction.user.username,\n\t\t};\n\t\tconst cfg = loadGatewayConfig();\n\t\tconst authorized = isAuthorized(source, cfg, createPairingStore());\n\t\tif (interaction.isAutocomplete()) {\n\t\t\tif (!authorized || !isGatewayOwner(source, cfg)) {\n\t\t\t\tawait interaction.respond([]);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst query = String(interaction.options.getFocused()).toLowerCase();\n\t\t\tlet values: string[] = [];\n\t\t\tif (interaction.commandName === \"mode\") values = [\"yolo\", \"auto\", \"read-only\"];\n\t\t\tif (this.commandSuggestions && [\"thinking\", \"model\"].includes(interaction.commandName)) {\n\t\t\t\tvalues = [...(await this.commandSuggestions(source, interaction.commandName))];\n\t\t\t}\n\t\t\tif (interaction.commandName === \"project\") values = cfg.projectRoots ?? [];\n\t\t\tif (interaction.commandName === \"sessions\") {\n\t\t\t\tconst { SessionManager } = await import(\"../../core/session-manager.ts\");\n\t\t\t\tvalues = (\n\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\tSessionManager.listAll(),\n\t\t\t\t\t\tnew Promise<[]>((resolve) => setTimeout(() => resolve([]), 1500)),\n\t\t\t\t\t])\n\t\t\t\t).map((s) => s.name || s.id);\n\t\t\t}\n\t\t\tawait interaction.respond(\n\t\t\t\tvalues\n\t\t\t\t\t.filter((s) => s.toLowerCase().includes(query))\n\t\t\t\t\t.slice(0, 25)\n\t\t\t\t\t.map((value) => ({ name: value.slice(0, 100), value: value.slice(0, 100) })),\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\tawait interaction.reply({\n\t\t\tcontent: authorized\n\t\t\t\t? \"Command received.\"\n\t\t\t\t: \"Pair your account before using this command. Send the bot a private message for a pairing code.\",\n\t\t\tephemeral: true,\n\t\t});\n\t\tif (authorized)\n\t\t\tthis.handler?.({\n\t\t\t\ttext: `/${interaction.commandName}${interaction.options.getString(\"args\") ? ` ${interaction.options.getString(\"args\")}` : \"\"}`,\n\t\t\t\tsource,\n\t\t\t\tmessageId: \"\",\n\t\t\t\tmetadata: { mentionedBot: true, nativeCommand: true },\n\t\t\t});\n\t\treturn true;\n\t}\n\n\tasync sendFile(chatId: string, file: string, opts?: SendOptions): Promise<SendResult> {\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel?.send) return { success: false, error: \"Channel is unavailable.\" };\n\t\ttry {\n\t\t\tconst message = await channel.send({ content: \"\", files: [file] });\n\t\t\treturn { success: true, messageId: message.id };\n\t\t} catch (error) {\n\t\t\treturn { success: false, error: errMessage(error) };\n\t\t}\n\t}\n\n\tprivate async handleInteraction(interaction: DiscordButtonInteractionLike): Promise<void> {\n\t\tif (!this.callbackHandler) return;\n\t\tif (!interaction.isButton()) return;\n\t\tconst event = buttonInteractionToEvent(interaction);\n\t\tif (!event) return;\n\t\tthis.callbackInteractions.set(interaction.id, interaction);\n\t\tif (this.callbackInteractions.size > CALLBACK_INTERACTION_CAP) {\n\t\t\tconst oldest = this.callbackInteractions.keys().next().value;\n\t\t\tif (oldest !== undefined) this.callbackInteractions.delete(oldest);\n\t\t}\n\t\tthis.callbackHandler(event);\n\t}\n\n\t/** Best-effort: fill replyToText from the referenced message when it wasn't cached. */\n\tprivate async resolveReplyToText(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (event.replyToText !== undefined) return;\n\t\tconst refId = message.reference?.messageId;\n\t\tif (refId == null) return;\n\t\tconst messages = message.channel.messages;\n\t\tif (!messages) return;\n\t\ttry {\n\t\t\tconst referenced = await messages.fetch(refId);\n\t\t\tif (referenced?.content) event.replyToText = referenced.content.slice(0, REPLY_TO_TEXT_MAX);\n\t\t} catch {\n\t\t\t// unknown/deleted reference — no reply context\n\t\t}\n\t}\n\n\t/** autoThread: spin a thread off a bot-mentioning guild-text message and re-target the event. */\n\tprivate async maybeAutoThread(message: DiscordMessageLike, event: MessageEvent): Promise<void> {\n\t\tif (!this.cfg.autoThread) return;\n\t\tif (event.metadata?.mentionedBot !== true) return;\n\t\t// Only guild TEXT channels: DMs can't thread and messages already inside\n\t\t// a thread don't need another one.\n\t\tif (message.channel.type !== ChannelType.GuildText) return;\n\t\tif (typeof message.startThread !== \"function\") return;\n\t\ttry {\n\t\t\tconst thread = await message.startThread({\n\t\t\t\tname: (event.text || \"conversation\").slice(0, THREAD_NAME_MAX),\n\t\t\t\tautoArchiveDuration: 60,\n\t\t\t});\n\t\t\tthis.participatingThreads.add(thread.id);\n\t\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t\t}\n\t\t\tevent.source = { ...event.source, chatType: \"thread\", chatId: message.channel.id, threadId: thread.id };\n\t\t} catch {\n\t\t\t// e.g. missing CREATE_THREADS permission — respond in-channel instead.\n\t\t}\n\t}\n\n\tprivate debounceKey(event: MessageEvent): string {\n\t\treturn `${event.source.chatId}:${event.source.threadId ?? \"\"}:${event.source.userId}`;\n\t}\n\n\tprivate dispatchEvent(event: MessageEvent): void {\n\t\tconst key = this.debounceKey(event);\n\t\tif (event.text.trimStart().startsWith(\"/\")) {\n\t\t\t// Commands must not merge: flush any pending text, emit immediately.\n\t\t\tthis.flushPending(key);\n\t\t\tthis.handler?.(event);\n\t\t\treturn;\n\t\t}\n\t\tconst existing = this.pending.get(key);\n\t\tif (existing) {\n\t\t\tthis.scheduler.clearTimeout(existing.timer);\n\t\t\t// Merge: earlier text first, latest messageId (and other fields) win.\n\t\t\tconst mergedAttachments = [...(existing.event.attachments ?? []), ...(event.attachments ?? [])];\n\t\t\texisting.event = {\n\t\t\t\t...event,\n\t\t\t\ttext: `${existing.event.text}\\n${event.text}`,\n\t\t\t\t...(mergedAttachments.length > 0 ? { attachments: mergedAttachments } : {}),\n\t\t\t};\n\t\t\texisting.timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t} else {\n\t\t\tconst timer = this.scheduler.setTimeout(() => this.flushPending(key), this.debounceMs);\n\t\t\tthis.pending.set(key, { event, timer });\n\t\t}\n\t}\n\n\tprivate flushPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t\tthis.handler?.(entry.event);\n\t}\n\n\tprivate cancelPending(key: string): void {\n\t\tconst entry = this.pending.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearTimeout(entry.timer);\n\t\tthis.pending.delete(key);\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Outbound\n\t// -----------------------------------------------------------------------\n\n\tprivate async fetchChannel(id: string): Promise<DiscordChannelLike | null> {\n\t\tif (this.channelCache.has(id)) return this.channelCache.get(id) ?? null;\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\tconst channel = await this.client.channels.fetch(id);\n\t\t\tthis.channelCache.set(id, channel);\n\t\t\tif (this.channelCache.size > CHANNEL_CACHE_CAP) {\n\t\t\t\tconst oldest = this.channelCache.keys().next().value;\n\t\t\t\tif (oldest !== undefined) this.channelCache.delete(oldest);\n\t\t\t}\n\t\t\treturn channel;\n\t\t} catch {\n\t\t\tthis.channelCache.set(id, null);\n\t\t\tif (this.channelCache.size > CHANNEL_CACHE_CAP) {\n\t\t\t\tconst oldest = this.channelCache.keys().next().value;\n\t\t\t\tif (oldest !== undefined) this.channelCache.delete(oldest);\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate recordSent(messageId: string, channel: DiscordChannelLike): void {\n\t\tif (THREAD_TYPES.includes(channel.type)) this.participatingThreads.add(channel.id);\n\t\tif (this.participatingThreads.size > PARTICIPATING_THREAD_CAP) {\n\t\t\tconst oldest = this.participatingThreads.values().next().value;\n\t\t\tif (oldest !== undefined) this.participatingThreads.delete(oldest);\n\t\t}\n\t\tthis.sentMessageChannels.set(messageId, channel.id);\n\t\tif (this.sentMessageChannels.size > SENT_CHANNEL_CAP) {\n\t\t\tconst oldest = this.sentMessageChannels.keys().next().value;\n\t\t\tif (oldest !== undefined) this.sentMessageChannels.delete(oldest);\n\t\t}\n\t}\n\n\tprivate async trySend(\n\t\tchannel: DiscordChannelLike,\n\t\tpayload: { content: string; reply?: { messageReference: string }; components?: unknown[] },\n\t): Promise<SendResult> {\n\t\tif (typeof channel.send !== \"function\") {\n\t\t\treturn { success: false, error: `channel ${channel.id} is not sendable` };\n\t\t}\n\t\ttry {\n\t\t\tconst sent = await channel.send(payload);\n\t\t\tthis.recordSent(sent.id, channel);\n\t\t\treturn { success: true, messageId: sent.id };\n\t\t} catch (err) {\n\t\t\t// 429s are queued internally by discord.js and don't surface here.\n\t\t\treturn { success: false, error: errMessage(err), retryable: discordErrorCode(err) !== ERR_INVALID_FORM_BODY };\n\t\t}\n\t}\n\n\tasync send(chatId: string, text: string, opts?: SendOptions): Promise<SendResult> {\n\t\t// A completed send ends the typing bubble — stop the refresher (see sendTyping).\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\t// The reply target may be deleted/unknown — fall back to a plain send.\n\t\t\treturn this.trySend(channel, { content: text });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync sendButtons(chatId: string, text: string, buttons: ButtonSpec[][], opts?: SendOptions): Promise<SendResult> {\n\t\tthis.stopTyping(typingKey(chatId, opts?.threadId));\n\t\tconst channel = await this.fetchChannel(opts?.threadId ?? chatId);\n\t\tif (!channel) return { success: false, error: `channel ${opts?.threadId ?? chatId} not found` };\n\t\tconst result = await this.trySend(channel, {\n\t\t\tcontent: text,\n\t\t\treply: opts?.replyTo !== undefined ? { messageReference: opts.replyTo } : undefined,\n\t\t\tcomponents: buildDiscordComponents(buttons),\n\t\t});\n\t\tif (!result.success && opts?.replyTo !== undefined) {\n\t\t\treturn this.trySend(channel, { content: text, components: buildDiscordComponents(buttons) });\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync answerCallback(id: string, text?: string): Promise<void> {\n\t\tconst interaction = this.callbackInteractions.get(id);\n\t\tif (!interaction) return;\n\t\tthis.callbackInteractions.delete(id);\n\t\ttry {\n\t\t\tif (text !== undefined && interaction.reply) {\n\t\t\t\tawait interaction.reply({ content: text, ephemeral: true });\n\t\t\t} else {\n\t\t\t\tawait interaction.deferUpdate();\n\t\t\t}\n\t\t} catch {\n\t\t\t// best-effort acknowledgment\n\t\t}\n\t}\n\n\tasync editMessage(chatId: string, messageId: string, text: string, buttons?: ButtonSpec[][]): Promise<SendResult> {\n\t\t// The router passes only chatId; thread messages live in their own\n\t\t// channel, so prefer the channel recorded at send() time.\n\t\tconst channel = await this.fetchChannel(this.sentMessageChannels.get(messageId) ?? chatId);\n\t\tif (!channel?.messages) {\n\t\t\treturn { success: false, error: `channel ${this.sentMessageChannels.get(messageId) ?? chatId} not found` };\n\t\t}\n\t\ttry {\n\t\t\tconst message = await channel.messages.fetch(messageId);\n\t\t\tif (!message || typeof message.edit !== \"function\") {\n\t\t\t\treturn { success: false, error: `message ${messageId} not found` };\n\t\t\t}\n\t\t\tconst editOptions: { content: string; components?: unknown[] } = { content: text };\n\t\t\tif (buttons !== undefined) editOptions.components = buildDiscordComponents(buttons);\n\t\t\tawait message.edit(editOptions);\n\t\t\treturn { success: true, messageId };\n\t\t} catch (err) {\n\t\t\tconst code = discordErrorCode(err);\n\t\t\tif (code === ERR_UNKNOWN_MESSAGE) return { success: false, error: errMessage(err) }; // terminal\n\t\t\treturn { success: false, error: errMessage(err), retryable: true };\n\t\t}\n\t}\n\n\tasync sendTyping(chatId: string, threadId?: string): Promise<void> {\n\t\t// Self-contained typing refresher (see file header): re-send the action\n\t\t// every 8s until send() completes for this target or the 2min safety net\n\t\t// fires — all errors swallowed.\n\t\tconst key = typingKey(chatId, threadId);\n\t\tconst targetId = threadId ?? chatId;\n\t\tawait this.doTyping(targetId);\n\t\tconst existing = this.typing.get(key);\n\t\tif (existing) {\n\t\t\texisting.lastRefresh = this.now();\n\t\t\treturn;\n\t\t}\n\t\tconst interval = this.scheduler.setInterval(() => {\n\t\t\tconst entry = this.typing.get(key);\n\t\t\tif (!entry) return;\n\t\t\tif (this.now() - entry.lastRefresh > TYPING_SAFETY_MS) {\n\t\t\t\tthis.stopTyping(key);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvoid this.doTyping(targetId);\n\t\t}, TYPING_INTERVAL_MS);\n\t\tthis.typing.set(key, { interval, lastRefresh: this.now() });\n\t}\n\n\tprivate async doTyping(channelId: string): Promise<void> {\n\t\ttry {\n\t\t\tconst channel = await this.fetchChannel(channelId);\n\t\t\tawait channel?.sendTyping?.();\n\t\t} catch {\n\t\t\t// typing is cosmetic — never surface\n\t\t}\n\t}\n\n\tprivate stopTyping(key: string): void {\n\t\tconst entry = this.typing.get(key);\n\t\tif (!entry) return;\n\t\tthis.scheduler.clearInterval(entry.interval);\n\t\tthis.typing.delete(key);\n\t}\n}\n"]}
|
|
@@ -461,7 +461,7 @@ export class DiscordAdapter {
|
|
|
461
461
|
const query = String(interaction.options.getFocused()).toLowerCase();
|
|
462
462
|
let values = [];
|
|
463
463
|
if (interaction.commandName === "mode")
|
|
464
|
-
values = ["
|
|
464
|
+
values = ["yolo", "auto", "read-only"];
|
|
465
465
|
if (this.commandSuggestions && ["thinking", "model"].includes(interaction.commandName)) {
|
|
466
466
|
values = [...(await this.commandSuggestions(source, interaction.commandName))];
|
|
467
467
|
}
|