@ashx-j/lunr-dev 0.2.13-dev.5.1 → 0.2.13-dev.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  2. package/dist/builtin-extensions/ashxj-tui.js +4 -4
  3. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  4. package/dist/builtin-extensions/index.d.ts.map +1 -1
  5. package/dist/builtin-extensions/index.js +2 -0
  6. package/dist/builtin-extensions/index.js.map +1 -1
  7. package/dist/builtin-extensions/lunr-settings-tools.d.ts +4 -0
  8. package/dist/builtin-extensions/lunr-settings-tools.d.ts.map +1 -0
  9. package/dist/builtin-extensions/lunr-settings-tools.js +120 -0
  10. package/dist/builtin-extensions/lunr-settings-tools.js.map +1 -0
  11. package/dist/builtin-extensions/pi-prompt-template-model/subagent-runtime.d.ts +2 -2
  12. package/dist/builtin-extensions/pi-prompt-template-model/subagent-runtime.d.ts.map +1 -1
  13. package/dist/builtin-extensions/pi-prompt-template-model/subagent-runtime.js.map +1 -1
  14. package/dist/builtin-extensions/pi-prompt-template-model/subagent-step.d.ts.map +1 -1
  15. package/dist/builtin-extensions/pi-prompt-template-model/subagent-step.js +3 -3
  16. package/dist/builtin-extensions/pi-prompt-template-model/subagent-step.js.map +1 -1
  17. package/dist/builtin-extensions/pi-subagents/src/api/delegation.d.ts +2 -2
  18. package/dist/builtin-extensions/pi-subagents/src/api/delegation.d.ts.map +1 -1
  19. package/dist/builtin-extensions/pi-subagents/src/api/delegation.js +1 -1
  20. package/dist/builtin-extensions/pi-subagents/src/api/delegation.js.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  22. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +2 -1
  23. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  24. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts +5 -11
  25. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
  26. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +12 -15
  27. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
  28. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +3 -3
  29. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  30. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +14 -27
  31. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  32. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.d.ts.map +1 -1
  33. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js +6 -4
  34. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-execution.js.map +1 -1
  35. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-job-tracker.d.ts.map +1 -1
  36. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-job-tracker.js +2 -1
  37. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-job-tracker.js.map +1 -1
  38. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.d.ts.map +1 -1
  39. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.js +6 -4
  40. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-resume.js.map +1 -1
  41. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.d.ts.map +1 -1
  42. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.js +4 -2
  43. package/dist/builtin-extensions/pi-subagents/src/runs/background/async-status.js.map +1 -1
  44. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-clarify.d.ts.map +1 -1
  45. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-clarify.js +3 -35
  46. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-clarify.js.map +1 -1
  47. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.d.ts.map +1 -1
  48. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js +5 -12
  49. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/chain-execution.js.map +1 -1
  50. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  51. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +14 -31
  52. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  53. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts +2 -0
  54. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts.map +1 -1
  55. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js +28 -22
  56. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js.map +1 -1
  57. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.d.ts +1 -0
  58. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.d.ts.map +1 -1
  59. package/dist/builtin-extensions/pi-subagents/src/runs/shared/parallel-utils.js.map +1 -1
  60. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.d.ts.map +1 -1
  61. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js +9 -4
  62. package/dist/builtin-extensions/pi-subagents/src/shared/child-spec.js.map +1 -1
  63. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.d.ts +1 -1
  64. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.d.ts.map +1 -1
  65. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.js +2 -2
  66. package/dist/builtin-extensions/pi-subagents/src/shared/formatters.js.map +1 -1
  67. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +4 -7
  68. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
  69. package/dist/builtin-extensions/pi-subagents/src/shared/types.js +1 -1
  70. package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
  71. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-adapters.d.ts +3 -3
  72. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-adapters.d.ts.map +1 -1
  73. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-adapters.js +7 -6
  74. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-adapters.js.map +1 -1
  75. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-request.d.ts.map +1 -1
  76. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-request.js +3 -3
  77. package/dist/builtin-extensions/pi-subagents/src/slash/delegation-request.js.map +1 -1
  78. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.d.ts +1 -1
  79. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.d.ts.map +1 -1
  80. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js +6 -4
  81. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js.map +1 -1
  82. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts +1 -1
  83. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
  84. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +15 -10
  85. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
  86. package/dist/builtin-extensions/pi-subagents/src/tui/fleet.d.ts.map +1 -1
  87. package/dist/builtin-extensions/pi-subagents/src/tui/fleet.js +3 -2
  88. package/dist/builtin-extensions/pi-subagents/src/tui/fleet.js.map +1 -1
  89. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +2 -2
  90. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  91. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +75 -43
  92. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  93. package/dist/catalog/models.json +1 -1
  94. package/dist/catalog/providers/amazon-bedrock.json +1 -1
  95. package/dist/catalog/providers/anthropic.json +1 -1
  96. package/dist/catalog/providers/cloudflare-ai-gateway.json +1 -1
  97. package/dist/catalog/providers/fireworks.json +1 -1
  98. package/dist/catalog/providers/github-copilot.json +1 -1
  99. package/dist/catalog/providers/google-vertex.json +1 -1
  100. package/dist/catalog/providers/google.json +1 -1
  101. package/dist/catalog/providers/huggingface.json +1 -1
  102. package/dist/catalog/providers/nvidia.json +1 -1
  103. package/dist/catalog/providers/opencode-go.json +1 -1
  104. package/dist/catalog/providers/opencode.json +1 -1
  105. package/dist/catalog/providers/openrouter.json +1 -1
  106. package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
  107. package/dist/catalog/publication.json +3 -3
  108. package/dist/core/agent-session-runtime.d.ts +5 -1
  109. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  110. package/dist/core/agent-session-runtime.js +7 -2
  111. package/dist/core/agent-session-runtime.js.map +1 -1
  112. package/dist/core/agent-session.d.ts +2 -0
  113. package/dist/core/agent-session.d.ts.map +1 -1
  114. package/dist/core/agent-session.js +20 -5
  115. package/dist/core/agent-session.js.map +1 -1
  116. package/dist/core/context-breakdown.d.ts +2 -0
  117. package/dist/core/context-breakdown.d.ts.map +1 -1
  118. package/dist/core/context-breakdown.js +6 -2
  119. package/dist/core/context-breakdown.js.map +1 -1
  120. package/dist/core/injected-prompt.d.ts +3 -3
  121. package/dist/core/injected-prompt.d.ts.map +1 -1
  122. package/dist/core/injected-prompt.js +2 -7
  123. package/dist/core/injected-prompt.js.map +1 -1
  124. package/dist/core/large-subagent-launch.d.ts +13 -0
  125. package/dist/core/large-subagent-launch.d.ts.map +1 -0
  126. package/dist/core/large-subagent-launch.js +54 -0
  127. package/dist/core/large-subagent-launch.js.map +1 -0
  128. package/dist/core/model-instructions.d.ts +24 -0
  129. package/dist/core/model-instructions.d.ts.map +1 -0
  130. package/dist/core/model-instructions.js +123 -0
  131. package/dist/core/model-instructions.js.map +1 -0
  132. package/dist/core/permissions.d.ts +11 -10
  133. package/dist/core/permissions.d.ts.map +1 -1
  134. package/dist/core/permissions.js +61 -45
  135. package/dist/core/permissions.js.map +1 -1
  136. package/dist/core/plan-mode.d.ts.map +1 -1
  137. package/dist/core/plan-mode.js +10 -1
  138. package/dist/core/plan-mode.js.map +1 -1
  139. package/dist/core/resource-loader.d.ts +3 -0
  140. package/dist/core/resource-loader.d.ts.map +1 -1
  141. package/dist/core/resource-loader.js +13 -4
  142. package/dist/core/resource-loader.js.map +1 -1
  143. package/dist/core/runtime-bridges.d.ts +4 -0
  144. package/dist/core/runtime-bridges.d.ts.map +1 -0
  145. package/dist/core/runtime-bridges.js +16 -0
  146. package/dist/core/runtime-bridges.js.map +1 -0
  147. package/dist/core/sdk.d.ts.map +1 -1
  148. package/dist/core/sdk.js +1 -0
  149. package/dist/core/sdk.js.map +1 -1
  150. package/dist/core/session-manager.d.ts.map +1 -1
  151. package/dist/core/session-manager.js +17 -16
  152. package/dist/core/session-manager.js.map +1 -1
  153. package/dist/core/settings-manager.d.ts +13 -0
  154. package/dist/core/settings-manager.d.ts.map +1 -1
  155. package/dist/core/settings-manager.js +32 -0
  156. package/dist/core/settings-manager.js.map +1 -1
  157. package/dist/core/settings-tools-bridge.d.ts +35 -0
  158. package/dist/core/settings-tools-bridge.d.ts.map +1 -0
  159. package/dist/core/settings-tools-bridge.js +56 -0
  160. package/dist/core/settings-tools-bridge.js.map +1 -0
  161. package/dist/core/swarm.d.ts +0 -31
  162. package/dist/core/swarm.d.ts.map +1 -1
  163. package/dist/core/swarm.js +2 -76
  164. package/dist/core/swarm.js.map +1 -1
  165. package/dist/core/system-prompt.d.ts.map +1 -1
  166. package/dist/core/system-prompt.js +1 -1
  167. package/dist/core/system-prompt.js.map +1 -1
  168. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  169. package/dist/gateway/agent-bridge.js +3 -2
  170. package/dist/gateway/agent-bridge.js.map +1 -1
  171. package/dist/gateway/approval.d.ts.map +1 -1
  172. package/dist/gateway/approval.js +4 -3
  173. package/dist/gateway/approval.js.map +1 -1
  174. package/dist/gateway/cron.d.ts.map +1 -1
  175. package/dist/gateway/cron.js +3 -2
  176. package/dist/gateway/cron.js.map +1 -1
  177. package/dist/gateway/router.d.ts +1 -1
  178. package/dist/gateway/router.d.ts.map +1 -1
  179. package/dist/gateway/router.js +1 -1
  180. package/dist/gateway/router.js.map +1 -1
  181. package/dist/main.d.ts.map +1 -1
  182. package/dist/main.js +3 -9
  183. package/dist/main.js.map +1 -1
  184. package/dist/migrations.d.ts.map +1 -1
  185. package/dist/migrations.js +4 -0
  186. package/dist/migrations.js.map +1 -1
  187. package/dist/modes/interactive/components/injected-prompt-message.d.ts +1 -1
  188. package/dist/modes/interactive/components/injected-prompt-message.d.ts.map +1 -1
  189. package/dist/modes/interactive/components/injected-prompt-message.js +1 -1
  190. package/dist/modes/interactive/components/injected-prompt-message.js.map +1 -1
  191. package/dist/modes/interactive/components/settings-selector.d.ts +8 -1
  192. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/settings-selector.js +61 -1
  194. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  195. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  196. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  197. package/dist/modes/interactive/interactive-mode.js +31 -23
  198. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  199. package/dist/startup/interactive-shell.d.ts.map +1 -1
  200. package/dist/startup/interactive-shell.js +2 -1
  201. package/dist/startup/interactive-shell.js.map +1 -1
  202. package/docs/features.md +7 -6
  203. package/docs/quickstart.md +3 -2
  204. package/docs/settings.md +3 -1
  205. package/docs/termux.md +1 -1
  206. package/docs/usage.md +2 -1
  207. package/examples/extensions/subagent/README.md +1 -1
  208. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAU,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI7G,OAAO,EAAE,KAAK,aAAa,EAA2D,MAAM,aAAa,CAAC;AAC1G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,yEAAyE;AACzE,MAAM,WAAW,UAAU;IAC1B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC5C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACvD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,GAAG,EAAE,aAAa,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,MAAM;IACtB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAcD,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6LrD","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /swarm, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded INTO the streaming preview (edit, plus\n * continuation chunks when the preview was truncated) — no preview +\n * final double-send. Without streaming it is split (text.ts) and sent\n * sequentially, the first chunk replying to the triggering message.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\n\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string): Promise<BridgeSession | null>;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// lunr: headless gateway sessions must prompt for mutating tools in manual mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(adapter: PlatformAdapter, event: MessageEvent, message: string): Promise<void> {\n\t\tawait adapter.send(event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: event.messageId,\n\t\t\tthreadId: event.source.threadId,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)} — ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\t// Unknown slash command: DM → normal text, group → ignore.\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runChatCommand(cmd, ctx);\n\t}\n\n\t/** Step 4 delivery: silence filter → split → sequential send.\n\t *\n\t * When a streaming preview message exists (editMessageId), the final text\n\t * is folded INTO it instead of sent as a duplicate: non-truncated previews\n\t * get a final edit (covers silence-marker stripping), truncated previews\n\t * are edited to chunk 1 and the remaining chunks are sent as follow-ups. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\tlet startIndex = 0;\n\t\tif (opts.editMessageId) {\n\t\t\tif (!opts.previewTruncated) {\n\t\t\t\t// Preview already holds the whole text: one final edit, no new message.\n\t\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Preview was truncated: upgrade it to the first full chunk.\n\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]);\n\t\t\tstartIndex = 1;\n\t\t}\n\t\tfor (let index = startIndex; index < chunks.length; index++) {\n\t\t\tconst result = await adapter.send(event.source.chatId, chunks[index], {\n\t\t\t\treplyTo: opts.reply && index === 0 ? event.messageId : undefined,\n\t\t\t\tthreadId: event.source.threadId,\n\t\t\t});\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? \"unknown error\"}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tawait adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, { reply: false });\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, message);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => {});\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,mBAAmB,EAAU,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAI7G,OAAO,EAAE,KAAK,aAAa,EAA2D,MAAM,aAAa,CAAC;AAC1G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,yEAAyE;AACzE,MAAM,WAAW,UAAU;IAC1B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC5C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACvD,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,GAAG,EAAE,aAAa,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,MAAM;IACtB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAcD,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6LrD","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded INTO the streaming preview (edit, plus\n * continuation chunks when the preview was truncated) — no preview +\n * final double-send. Without streaming it is split (text.ts) and sent\n * sequentially, the first chunk replying to the triggering message.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\n\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string): Promise<BridgeSession | null>;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// lunr: headless gateway sessions must prompt for mutating tools in manual mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(adapter: PlatformAdapter, event: MessageEvent, message: string): Promise<void> {\n\t\tawait adapter.send(event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: event.messageId,\n\t\t\tthreadId: event.source.threadId,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)} — ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\t// Unknown slash command: DM → normal text, group → ignore.\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runChatCommand(cmd, ctx);\n\t}\n\n\t/** Step 4 delivery: silence filter → split → sequential send.\n\t *\n\t * When a streaming preview message exists (editMessageId), the final text\n\t * is folded INTO it instead of sent as a duplicate: non-truncated previews\n\t * get a final edit (covers silence-marker stripping), truncated previews\n\t * are edited to chunk 1 and the remaining chunks are sent as follow-ups. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\tlet startIndex = 0;\n\t\tif (opts.editMessageId) {\n\t\t\tif (!opts.previewTruncated) {\n\t\t\t\t// Preview already holds the whole text: one final edit, no new message.\n\t\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Preview was truncated: upgrade it to the first full chunk.\n\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]);\n\t\t\tstartIndex = 1;\n\t\t}\n\t\tfor (let index = startIndex; index < chunks.length; index++) {\n\t\t\tconst result = await adapter.send(event.source.chatId, chunks[index], {\n\t\t\t\treplyTo: opts.reply && index === 0 ? event.messageId : undefined,\n\t\t\t\tthreadId: event.source.threadId,\n\t\t\t});\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? \"unknown error\"}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tawait adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, { reply: false });\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, message);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => {});\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  * 2. authorization (authz.ts); denied DMs get a pairing code when the
8
8
  * behavior is "pair" and the user isn't rate-limited
9
9
  * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,
10
- * /context, /swarm, /compact, /thinking, /stop, /status, /help, /whoami)
10
+ * /context, /compact, /thinking, /stop, /status, /help, /whoami)
11
11
  * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is
12
12
  * enabled and the adapter supports edit; the final text is
13
13
  * silence-filtered and folded INTO the streaming preview (edit, plus
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /swarm, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded INTO the streaming preview (edit, plus\n * continuation chunks when the preview was truncated) — no preview +\n * final double-send. Without streaming it is split (text.ts) and sent\n * sequentially, the first chunk replying to the triggering message.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\n\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string): Promise<BridgeSession | null>;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// lunr: headless gateway sessions must prompt for mutating tools in manual mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(adapter: PlatformAdapter, event: MessageEvent, message: string): Promise<void> {\n\t\tawait adapter.send(event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: event.messageId,\n\t\t\tthreadId: event.source.threadId,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)} — ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\t// Unknown slash command: DM → normal text, group → ignore.\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runChatCommand(cmd, ctx);\n\t}\n\n\t/** Step 4 delivery: silence filter → split → sequential send.\n\t *\n\t * When a streaming preview message exists (editMessageId), the final text\n\t * is folded INTO it instead of sent as a duplicate: non-truncated previews\n\t * get a final edit (covers silence-marker stripping), truncated previews\n\t * are edited to chunk 1 and the remaining chunks are sent as follow-ups. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\tlet startIndex = 0;\n\t\tif (opts.editMessageId) {\n\t\t\tif (!opts.previewTruncated) {\n\t\t\t\t// Preview already holds the whole text: one final edit, no new message.\n\t\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Preview was truncated: upgrade it to the first full chunk.\n\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]);\n\t\t\tstartIndex = 1;\n\t\t}\n\t\tfor (let index = startIndex; index < chunks.length; index++) {\n\t\t\tconst result = await adapter.send(event.source.chatId, chunks[index], {\n\t\t\t\treplyTo: opts.reply && index === 0 ? event.messageId : undefined,\n\t\t\t\tthreadId: event.source.threadId,\n\t\t\t});\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? \"unknown error\"}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tawait adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, { reply: false });\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, message);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => {});\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/gateway/router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway router.\n *\n * Every inbound MessageEvent flows through handleEvent:\n * 1. group gating (allowedChats / freeResponseChats / requireMention,\n * with adapter-supplied metadata.mentionedBot)\n * 2. authorization (authz.ts); denied DMs get a pairing code when the\n * behavior is \"pair\" and the user isn't rate-limited\n * 3. command registry (/new, /undo, /redo, /model, /sessions, /title,\n * /context, /compact, /thinking, /stop, /status, /help, /whoami)\n * 4. normal path: bridge.runTurn with a StreamConsumer when streaming is\n * enabled and the adapter supports edit; the final text is\n * silence-filtered and folded INTO the streaming preview (edit, plus\n * continuation chunks when the preview was truncated) — no preview +\n * final double-send. Without streaming it is split (text.ts) and sent\n * sequentially, the first chunk replying to the triggering message.\n * Errors surface as a compact \"⚠ <one-line>\" — never a stack trace.\n */\n\nimport { existsSync } from \"node:fs\";\n\nimport { type BridgeSession, type BridgeSessionStatus, QUEUED, type TurnCallbacks } from \"./agent-bridge.ts\";\nimport { registerGatewayApprovalHandler, runWithApprovalContext } from \"./approval.ts\";\nimport { isAuthorized } from \"./authz.ts\";\nimport { CHAT_COMMANDS, runChatCommand, sendCommandReply } from \"./commands.ts\";\nimport { type GatewayConfig, gatewayConfigPath, loadGatewayConfig, platformConfigFor } from \"./config.ts\";\nimport type { PairingStore } from \"./pairing.ts\";\nimport { buildSessionKey } from \"./session-keys.ts\";\nimport { applySilenceFilter, StreamConsumer } from \"./stream.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\n/** Structural bridge shape (AgentBridge satisfies it; tests fake it). */\nexport interface BridgeLike {\n\trunTurn(key: string, event: MessageEvent, callbacks: TurnCallbacks): Promise<string>;\n\tabort(key: string): Promise<void> | void;\n\treset(key: string): void | Promise<void>;\n\tgetStatus(key: string): BridgeSessionStatus;\n\tgetSession(key: string): Promise<BridgeSession | null>;\n\tswitchSession(key: string, sessionFile: string): Promise<void>;\n\tundo(key: string): Promise<{ userText: string }>;\n\tredo(key: string): Promise<void>;\n}\n\nexport interface RouterDeps {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\tpairing: PairingStore;\n\tbridge: BridgeLike;\n\t/** Reload gateway.json on every inbound event so CLI config edits (e.g. pair approve) are picked up by the running daemon. */\n\treloadConfig?: boolean;\n}\n\nexport interface Router {\n\thandleEvent(event: MessageEvent): Promise<void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction formatPairingCode(code: string): string {\n\treturn code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nfunction mentionedBot(event: MessageEvent): boolean {\n\treturn event.metadata?.mentionedBot === true;\n}\n\nexport function createRouter(deps: RouterDeps): Router {\n\tconst { adapters, cfg: initialCfg, pairing, bridge, reloadConfig } = deps;\n\n\t// lunr: headless gateway sessions must prompt for mutating tools in manual mode.\n\tregisterGatewayApprovalHandler();\n\n\t/** Reload gateway.json on every inbound event so CLI edits (e.g. pair approve) take effect immediately. */\n\tfunction freshCfg(): GatewayConfig {\n\t\tif (!reloadConfig) return initialCfg;\n\t\ttry {\n\t\t\tif (existsSync(gatewayConfigPath())) {\n\t\t\t\treturn loadGatewayConfig();\n\t\t\t}\n\t\t} catch {\n\t\t\t// fall through to initial config\n\t\t}\n\t\treturn initialCfg;\n\t}\n\n\tasync function sendError(adapter: PlatformAdapter, event: MessageEvent, message: string): Promise<void> {\n\t\tawait adapter.send(event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: event.messageId,\n\t\t\tthreadId: event.source.threadId,\n\t\t});\n\t}\n\n\t/** Step 1: group-chat early gating. Returns true when the event must be dropped. */\n\tfunction isGroupGated(event: MessageEvent, cfg: GatewayConfig): boolean {\n\t\tconst { source } = event;\n\t\tif (source.chatType === \"dm\") return false;\n\t\tconst platformCfg = platformConfigFor(cfg, source.platform);\n\t\tif (!platformCfg) return true;\n\t\tconst freeResponse = platformCfg.freeResponseChats.includes(source.chatId);\n\t\tif (\n\t\t\tplatformCfg.allowedChats.length > 0 &&\n\t\t\t!platformCfg.allowedChats.includes(source.chatId) &&\n\t\t\t!freeResponse &&\n\t\t\t!mentionedBot(event)\n\t\t) {\n\t\t\treturn true;\n\t\t}\n\t\tif (platformCfg.requireMention && !freeResponse && !mentionedBot(event)) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Step 2: authorization; handles the denied-DM pairing flow. Returns true when denied. */\n\tasync function isDenied(adapter: PlatformAdapter, event: MessageEvent, cfg: GatewayConfig): Promise<boolean> {\n\t\tif (isAuthorized(event.source, cfg, pairing)) return false;\n\t\tconst { source } = event;\n\t\tif (source.chatType !== \"dm\") return true; // denied group: silent\n\t\tif (cfg.unauthorizedDmBehavior !== \"pair\") return true;\n\t\tconst code = pairing.issueCode(source.platform, source.userId);\n\t\tif (code === null) return true; // rate-limited or pending list full: stay silent\n\t\tawait adapter.send(\n\t\t\tsource.chatId,\n\t\t\t`Your lunR pairing code: ${formatPairingCode(code)} — ask the owner to run: lunr gateway pair approve ${source.platform} ${formatPairingCode(code)}`,\n\t\t\t{ replyTo: event.messageId, threadId: source.threadId },\n\t\t);\n\t\treturn true;\n\t}\n\n\t/** Step 3: command registry. Returns true when the event was consumed. */\n\tasync function handleSlash(adapter: PlatformAdapter, event: MessageEvent, key: string): Promise<boolean> {\n\t\tconst text = event.text.trim();\n\t\tif (!text.startsWith(\"/\")) return false;\n\t\tconst firstToken = text.split(/\\s+/, 1)[0].toLowerCase();\n\t\tconst commandWord = firstToken.split(\"@\")[0].slice(1);\n\t\tconst args = text.slice(firstToken.length).trim();\n\t\tconst cmd = CHAT_COMMANDS.find((c) => c.name === commandWord || c.aliases?.includes(commandWord));\n\t\tif (!cmd) {\n\t\t\t// Unknown slash command: DM → normal text, group → ignore.\n\t\t\treturn event.source.chatType !== \"dm\";\n\t\t}\n\t\tconst ctx = {\n\t\t\tevent,\n\t\t\tkey,\n\t\t\tadapter,\n\t\t\tbridge,\n\t\t\targs,\n\t\t\treply: (message: string) => sendCommandReply(adapter, event, message),\n\t\t};\n\t\treturn runChatCommand(cmd, ctx);\n\t}\n\n\t/** Step 4 delivery: silence filter → split → sequential send.\n\t *\n\t * When a streaming preview message exists (editMessageId), the final text\n\t * is folded INTO it instead of sent as a duplicate: non-truncated previews\n\t * get a final edit (covers silence-marker stripping), truncated previews\n\t * are edited to chunk 1 and the remaining chunks are sent as follow-ups. */\n\tasync function deliver(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\ttext: string,\n\t\topts: { reply: boolean; editMessageId?: string; previewTruncated?: boolean },\n\t): Promise<void> {\n\t\tconst filtered = applySilenceFilter(text);\n\t\tif (filtered === null) return;\n\t\tconst chunks = splitMessage(filtered, adapter.maxMessageLength);\n\t\tlet startIndex = 0;\n\t\tif (opts.editMessageId) {\n\t\t\tif (!opts.previewTruncated) {\n\t\t\t\t// Preview already holds the whole text: one final edit, no new message.\n\t\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, filtered);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Preview was truncated: upgrade it to the first full chunk.\n\t\t\tawait adapter.editMessage(event.source.chatId, opts.editMessageId, chunks[0]);\n\t\t\tstartIndex = 1;\n\t\t}\n\t\tfor (let index = startIndex; index < chunks.length; index++) {\n\t\t\tconst result = await adapter.send(event.source.chatId, chunks[index], {\n\t\t\t\treplyTo: opts.reply && index === 0 ? event.messageId : undefined,\n\t\t\t\tthreadId: event.source.threadId,\n\t\t\t});\n\t\t\tif (!result.success) {\n\t\t\t\tconsole.error(`[gateway] send failed on ${adapter.platform}: ${result.error ?? \"unknown error\"}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync function runTurn(\n\t\tadapter: PlatformAdapter,\n\t\tevent: MessageEvent,\n\t\tkey: string,\n\t\tcfg: GatewayConfig,\n\t): Promise<void> {\n\t\tconst streaming = cfg.streaming.enabled && typeof adapter.editMessage === \"function\";\n\t\tlet consumer: StreamConsumer | undefined;\n\t\tif (streaming) {\n\t\t\tconsumer = new StreamConsumer({\n\t\t\t\tsendInitial: async (text) => {\n\t\t\t\t\tconst result = await adapter.send(event.source.chatId, text, {\n\t\t\t\t\t\treplyTo: event.messageId,\n\t\t\t\t\t\tthreadId: event.source.threadId,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.success ? (result.messageId ?? null) : null;\n\t\t\t\t},\n\t\t\t\tedit: async (messageId, text) => {\n\t\t\t\t\tawait adapter.editMessage(event.source.chatId, messageId, text);\n\t\t\t\t},\n\t\t\t\tintervalMs: cfg.streaming.editIntervalMs,\n\t\t\t\tthreshold: cfg.streaming.bufferThreshold,\n\t\t\t\tmaxPreview: adapter.maxMessageLength,\n\t\t\t});\n\t\t}\n\n\t\tconst callbacks: TurnCallbacks = {\n\t\t\tonDelta: consumer ? (delta) => consumer.push(delta) : undefined,\n\t\t\tonFollowUpResult: (text) => {\n\t\t\t\tvoid deliver(adapter, event, text, { reply: false });\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tvoid sendError(adapter, event, message);\n\t\t\t},\n\t\t};\n\n\t\tconst result = await runWithApprovalContext({ key, adapter, source: event.source }, () =>\n\t\t\tbridge.runTurn(key, event, callbacks),\n\t\t);\n\t\tif (result === QUEUED) return; // queued behind a running turn: no reply\n\t\tif (consumer) await consumer.finalize();\n\t\tawait deliver(adapter, event, result, {\n\t\t\treply: !consumer,\n\t\t\teditMessageId: consumer?.sentMessageId ?? undefined,\n\t\t\tpreviewTruncated: consumer?.truncated ?? false,\n\t\t});\n\t}\n\n\treturn {\n\t\tasync handleEvent(event: MessageEvent): Promise<void> {\n\t\t\tconst adapter = adapters.get(event.source.platform);\n\t\t\tif (!adapter) return;\n\t\t\tconst cfg = freshCfg();\n\t\t\ttry {\n\t\t\t\tif (isGroupGated(event, cfg)) return;\n\t\t\t\tif (await isDenied(adapter, event, cfg)) return;\n\t\t\t\tconst key = buildSessionKey(event.source, { groupSessionsPerUser: cfg.groupSessionsPerUser });\n\t\t\t\tif (await handleSlash(adapter, event, key)) return;\n\t\t\t\tawait adapter.sendTyping(event.source.chatId, event.source.threadId).catch(() => {});\n\t\t\t\tawait runTurn(adapter, event, key, cfg);\n\t\t\t} catch (err) {\n\t\t\t\tawait sendError(adapter, event, err instanceof Error ? err.message : String(err)).catch(() => {});\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgCH,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,4BAA4B,CAAC;AAyBvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAa9E;;;GAGG;AACH,wBAAsB,cAAc,CACnC,MAAM,GAAE,MAAM,CAAC,UAA0B,EACzC,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACtD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0C7B;AA2aD,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACvC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBA2e/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@ashx-j/lunr-ai\";\nimport chalk from \"chalk\";\nimport { lightBuiltinExtensions, loadAllBuiltinExtensions } from \"./builtin-extensions/index.ts\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.ts\";\nimport { processFileArguments } from \"./cli/file-processor.ts\";\nimport { buildInitialMessage } from \"./cli/initial-message.ts\";\nimport { handleInstallCli } from \"./cli/install-cli.ts\";\nimport { listModels } from \"./cli/list-models.ts\";\nimport { createProjectTrustContext } from \"./cli/project-trust.ts\";\nimport { selectSession } from \"./cli/session-picker.ts\";\nimport { shouldRunFirstTimeSetup, showFirstTimeSetup, showStartupSelector } from \"./cli/startup-ui.ts\";\nimport { handleUpdateCli } from \"./cli/update-cli.ts\";\nimport {\n\tAPP_NAME,\n\tappendDebugLog,\n\tENV_SESSION_DIR,\n\texpandTildePath,\n\tgetAgentDir,\n\tgetSessionsDir,\n\tVERSION,\n} from \"./config.ts\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.ts\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.ts\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.ts\";\nimport { registerCustomizeBridge } from \"./core/customize.ts\";\nimport type { InlineExtension, ProjectTrustContext } from \"./core/extensions/types.ts\";\nimport { applyHttpProxySettings, configureHttpDispatcher } from \"./core/http-dispatcher.ts\";\nimport { registerMemoryCapBridge } from \"./core/memory-cap.ts\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.ts\";\nimport type { ModelRuntime } from \"./core/model-runtime.ts\";\nimport { getModelTiersBridge, registerModelTierBridge } from \"./core/model-tiers.ts\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.ts\";\nimport { type AppMode, resolveProjectTrusted } from \"./core/project-trust.ts\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.ts\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.ts\";\nimport { assertValidSessionId, SessionManager } from \"./core/session-manager.ts\";\nimport { pruneOldSessions } from \"./core/session-retention.ts\";\nimport { SettingsManager } from \"./core/settings-manager.ts\";\nimport { applyInheritedSubagentPermissions } from \"./core/subagent-permission-inherit.ts\";\nimport { printTimings, resetTimings, time } from \"./core/timings.ts\";\nimport { hasTrustRequiringProjectResources, ProjectTrustStore } from \"./core/trust-manager.ts\";\nimport { registerUsageServiceBridge } from \"./core/usage-service.ts\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.ts\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.ts\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.ts\";\nimport type { InteractiveStartupShell } from \"./startup/interactive-shell.ts\";\nimport { markStartupMilestone } from \"./startup/startup-milestones.ts\";\nimport { isLocalPath, normalizePath, resolvePath } from \"./utils/paths.ts\";\n\nconst EXTENSION_LOAD_FAILURE_HINT = `Hint: Start without extensions using \"${APP_NAME} -ne\".`;\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nconst PIPED_STDIN_FIRST_CHUNK_MS = 50;\nconst PIPED_STDIN_IDLE_MS = 2000;\n\n/**\n * Read piped stdin. A non-TTY that never emits data (Windows console, leftover\n * redirected handle) must not block interactive start forever.\n */\nexport async function readPipedStdin(\n\tstream: NodeJS.ReadStream = process.stdin,\n\toptions: { firstChunkMs?: number; idleMs?: number } = {},\n): Promise<string | undefined> {\n\tif (stream.isTTY) {\n\t\treturn undefined;\n\t}\n\n\tconst firstChunkMs = options.firstChunkMs ?? PIPED_STDIN_FIRST_CHUNK_MS;\n\tconst idleMs = options.idleMs ?? PIPED_STDIN_IDLE_MS;\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tlet settled = false;\n\t\tlet sawData = false;\n\t\tlet idleTimer: ReturnType<typeof setTimeout> | undefined;\n\t\tconst firstChunkTimer = setTimeout(() => {\n\t\t\tif (!sawData) finish(undefined);\n\t\t}, firstChunkMs);\n\n\t\tconst finish = (value: string | undefined) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(firstChunkTimer);\n\t\t\tif (idleTimer) clearTimeout(idleTimer);\n\t\t\tstream.off(\"data\", onData);\n\t\t\tstream.off(\"end\", onEnd);\n\t\t\tstream.off(\"error\", onEnd);\n\t\t\tresolve(value);\n\t\t};\n\n\t\tconst onEnd = () => finish(data.trim() || undefined);\n\t\tconst onData = (chunk: string | Buffer) => {\n\t\t\tsawData = true;\n\t\t\tdata += typeof chunk === \"string\" ? chunk : chunk.toString(\"utf8\");\n\t\t\tif (idleTimer) clearTimeout(idleTimer);\n\t\t\tidleTimer = setTimeout(onEnd, idleMs);\n\t\t};\n\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", onData);\n\t\tstream.on(\"end\", onEnd);\n\t\tstream.on(\"error\", onEnd);\n\t\tstream.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean, stdoutIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\t// PI_STARTUP_BENCHMARK=1 times interactive first paint even when stdin/stdout\n\t// are not TTYs (repo npx, CI, redirected logs).\n\tif (isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK) && !parsed.print) {\n\t\treturn \"interactive\";\n\t}\n\tif (parsed.print || !stdinIsTTY || !stdoutIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nfunction isPlainRuntimeMetadataCommand(parsed: Args): boolean {\n\treturn !parsed.print && parsed.mode === undefined && (parsed.help === true || parsed.listModels !== undefined);\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function findLocalSessionByExactId(\n\tsessionId: string,\n\tcwd: string,\n\tsessionDir?: string,\n): Promise<{ type: \"local\"; path: string } | undefined> {\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch = localSessions.find((s) => s.id === sessionId);\n\treturn localMatch ? { type: \"local\", path: localMatch.path } : undefined;\n}\n\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, resolve it before handing it to the session manager.\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: resolvePath(sessionArg, cwd) };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch =\n\t\tlocalSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatch) {\n\t\treturn { type: \"local\", path: localMatch.path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll(sessionDir);\n\tconst globalMatch =\n\t\tallSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatch) {\n\t\treturn { type: \"global\", path: globalMatch.path, cwd: globalMatch.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string, startupShell?: InteractiveStartupShell): Promise<boolean> {\n\tif (startupShell) {\n\t\treturn startupShell.confirm(\"Confirm\", message);\n\t}\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction validateSessionIdFlags(parsed: Args): void {\n\tif (parsed.sessionId === undefined) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n\n\ttry {\n\t\tassertValidSessionId(parsed.sessionId);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction openSessionOrExit(path: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.open(path, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string, sessionId?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n\tstartupShell?: InteractiveStartupShell,\n): Promise<SessionManager> {\n\tif (parsed.noSession || parsed.help || parsed.listModels !== undefined) {\n\t\treturn SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);\n\t}\n\n\tif (parsed.fork) {\n\t\tif (parsed.sessionId) {\n\t\t\tconst existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\t\tif (existingTarget) {\n\t\t\t\tconsole.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn openSessionOrExit(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\", startupShell);\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\ttry {\n\t\t\tconst currentSessionsLoader = (onProgress?: Parameters<typeof SessionManager.list>[2]) =>\n\t\t\t\tSessionManager.list(cwd, sessionDir, onProgress);\n\t\t\tconst allSessionsLoader = (onProgress?: Parameters<typeof SessionManager.listAll>[1]) =>\n\t\t\t\tSessionManager.listAll(sessionDir, onProgress);\n\t\t\tconst selectedPath = startupShell\n\t\t\t\t? await startupShell.selectSession(currentSessionsLoader, allSessionsLoader)\n\t\t\t\t: await selectSession(currentSessionsLoader, allSessionsLoader, settingsManager);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\tif (parsed.sessionId) {\n\t\tconst existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\tif (existingSession) {\n\t\t\treturn SessionManager.open(existingSession.path, sessionDir);\n\t\t}\n\t\tconsole.error(\n\t\t\tchalk.yellow(\n\t\t\t\t`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRuntime: ModelRuntime,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tcliThinking: parsed.thinking,\n\t\t\tmodelRuntime,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRuntime.getModel(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set as a non-persistent runtime override\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\tif (parsed.excludeTools) {\n\t\toptions.excludeTools = [...parsed.excludeTools];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));\n}\n\nfunction createStartupShellTrustContext(cwd: string, startupShell: InteractiveStartupShell): ProjectTrustContext {\n\treturn {\n\t\tcwd,\n\t\tmode: \"tui\",\n\t\thasUI: true,\n\t\tui: {\n\t\t\tselect: (title, options) =>\n\t\t\t\tstartupShell.select(\n\t\t\t\t\ttitle,\n\t\t\t\t\toptions.map((option) => ({ label: option, value: option })),\n\t\t\t\t),\n\t\t\tconfirm: (title, message) => startupShell.confirm(title, message),\n\t\t\tinput: (title, placeholder) => startupShell.input(title, placeholder),\n\t\t\tnotify: () => {},\n\t\t},\n\t};\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n\tstartupShell?: InteractiveStartupShell,\n): Promise<string | undefined> {\n\tconst options = [\n\t\t{ label: \"Continue\", value: issue.fallbackCwd },\n\t\t{ label: \"Cancel\", value: undefined },\n\t];\n\treturn startupShell\n\t\t? startupShell.select(formatMissingSessionCwdPrompt(issue), options)\n\t\t: showStartupSelector(settingsManager, formatMissingSessionCwdPrompt(issue), options);\n}\n\nexport interface MainOptions {\n\textensionFactories?: InlineExtension[];\n\tstartupShell?: InteractiveStartupShell;\n}\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst offlineMode = args.includes(\"--offline\") || isTruthyEnvFlag(process.env.PI_OFFLINE);\n\tif (offlineMode) {\n\t\tprocess.env.PI_OFFLINE = \"1\";\n\t}\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tapplyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher();\n\n\tif (await handleInstallCli(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleUpdateCli(args)) {\n\t\treturn;\n\t}\n\n\tif (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) {\n\t\tconst exitCode = process.exitCode ?? 0;\n\t\tprocess.exit(exitCode);\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args, { extensionFactories: options?.extensionFactories })) {\n\t\treturn;\n\t}\n\n\tif (args[0] === \"gateway\") {\n\t\tconst { handleGatewayCommand } = await import(\"./gateway/command.ts\");\n\t\tif (await handleGatewayCommand(args)) return;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst { exportFromFile } = await import(\"./core/export-html/index.ts\");\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY, process.stdout.isTTY);\n\tconst interactiveModeImport =\n\t\tappMode === \"interactive\" ? import(\"./modes/interactive/interactive-mode.ts\") : undefined;\n\tconst startupShell = appMode === \"interactive\" ? options?.startupShell : undefined;\n\tif (options?.startupShell && !startupShell) options.startupShell.stop();\n\t// lunr: parent-delegated children inherit plan or auto before any tool call.\n\t// Non-child print/json stays fail-closed (module default manual, no handler).\n\tapplyInheritedSubagentPermissions();\n\tconst shouldTakeOverStdout = appMode !== \"interactive\" && !isPlainRuntimeMetadataCommand(parsed);\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\tvalidateSessionIdFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd);\n\ttime(\"runMigrations\");\n\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Register the model-tier bridge before extensions load so pi-subagents can read\n\t// tier settings when building its tool description. Re-pointed to the runtime\n\t// settings manager below once services exist.\n\tregisterModelTierBridge(startupSettingsManager);\n\t// lunr: same for the simple-pi-memory character cap — extensions read it via the\n\t// bridge at load/runtime; re-pointed to the runtime settings manager below.\n\tregisterMemoryCapBridge(startupSettingsManager);\n\t// lunr: TUI customize settings (spinner, dividers, rail, prompt symbol) read by\n\t// ashxj-spinners and ashxj-tui via the bridge; re-pointed to runtime below.\n\tregisterCustomizeBridge(startupSettingsManager);\n\n\t// Experimental first-time setup: theme choice and analytics opt-in.\n\t// Runs before any runtime services are created so the chosen settings apply everywhere.\n\tif (appMode === \"interactive\" && !parsed.help && parsed.listModels === undefined && shouldRunFirstTimeSetup()) {\n\t\tstartupShell?.pause();\n\t\tawait showFirstTimeSetup(startupSettingsManager);\n\t\tstartupShell?.resume();\n\t\ttime(\"firstTimeSetup\");\n\t}\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\t(parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager, startupShell);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(\n\t\t\t\tmissingSessionCwdIssue,\n\t\t\t\tstartupSettingsManager,\n\t\t\t\tstartupShell,\n\t\t\t);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\tif (parsed.name !== undefined) {\n\t\tconst name = parsed.name.trim();\n\t\tif (!name) {\n\t\t\tconsole.error(chalk.red(\"Error: --name requires a non-empty value\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tsessionManager.appendSessionInfo(name);\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst runSessionRetention = async () => {\n\t\ttry {\n\t\t\tconst retentionDays = startupSettingsManager.getSessionRetentionDays();\n\t\t\tif (retentionDays <= 0) return;\n\t\t\tconst activeSessionFile = sessionManager.getSessionFile();\n\t\t\tconst { deleted } = await pruneOldSessions(getSessionsDir(), retentionDays, {\n\t\t\t\texcludeFile: activeSessionFile,\n\t\t\t});\n\t\t\tif (sessionDir) {\n\t\t\t\tconst extra = await pruneOldSessions(sessionDir, retentionDays, { excludeFile: activeSessionFile });\n\t\t\t\tdeleted.push(...extra.deleted);\n\t\t\t}\n\t\t\tif (deleted.length > 0) {\n\t\t\t\tappendDebugLog(\n\t\t\t\t\t`session-retention: deleted ${deleted.length} file(s):\\n${deleted.map((path) => ` ${path}`).join(\"\\n\")}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tappendDebugLog(`session-retention: prune failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t};\n\n\tconst trustStore = new ProjectTrustStore(agentDir);\n\tconst sessionCwd = sessionManager.getCwd();\n\tconst autoTrustOnReloadCwd =\n\t\tparsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd)\n\t\t\t? sessionCwd\n\t\t\t: undefined;\n\tconst trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? \"print\" : appMode;\n\tconst projectTrustByCwd = new Map<string, boolean>();\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\tconst builtinRoster = appMode === \"interactive\" ? [...lightBuiltinExtensions] : await loadAllBuiltinExtensions();\n\tconst rememberAttachedFactories = (factories: InlineExtension[]) => {\n\t\tconst seen = new Set(\n\t\t\tbuiltinRoster.map((entry) => (typeof entry === \"function\" ? undefined : entry.name)).filter(Boolean),\n\t\t);\n\t\tfor (const factory of factories) {\n\t\t\tconst name = typeof factory === \"function\" ? undefined : factory.name;\n\t\t\tif (name && seen.has(name)) continue;\n\t\t\tif (name) seen.add(name);\n\t\t\tbuiltinRoster.push(factory);\n\t\t}\n\t};\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t\tprojectTrustContext,\n\t}) => {\n\t\tconst isInitialRuntime = sessionStartEvent === undefined;\n\t\tconst projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\t\tconst cachedProjectTrust = projectTrustByCwd.get(cwd);\n\t\tconst hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);\n\t\tconst shouldResolveProjectTrust =\n\t\t\tparsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources;\n\t\tconst projectTrusted = shouldResolveProjectTrust\n\t\t\t? false\n\t\t\t: (cachedProjectTrust ??\n\t\t\t\tparsed.projectTrustOverride ??\n\t\t\t\t(!hasTrustRequiringResources || trustStore.get(cwd) === true));\n\t\tconst runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });\n\t\tconst skipMissingPackageInstall = isInitialRuntime && appMode === \"interactive\";\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tsettingsManager: runtimeSettingsManager,\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderReloadOptions: shouldResolveProjectTrust\n\t\t\t\t? {\n\t\t\t\t\t\tskipMissingPackageInstall,\n\t\t\t\t\t\tresolveProjectTrust: async ({ extensionsResult }) => {\n\t\t\t\t\t\t\tconst trusted = await resolveProjectTrusted({\n\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\ttrustStore,\n\t\t\t\t\t\t\t\ttrustOverride: parsed.projectTrustOverride,\n\t\t\t\t\t\t\t\tdefaultProjectTrust: startupSettingsManager.getDefaultProjectTrust(),\n\t\t\t\t\t\t\t\textensionsResult,\n\t\t\t\t\t\t\t\tprojectTrustContext:\n\t\t\t\t\t\t\t\t\tprojectTrustContext ??\n\t\t\t\t\t\t\t\t\t(isInitialRuntime && startupShell\n\t\t\t\t\t\t\t\t\t\t? createStartupShellTrustContext(cwd, startupShell)\n\t\t\t\t\t\t\t\t\t\t: createProjectTrustContext({\n\t\t\t\t\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\t\t\t\t\tmode: isInitialRuntime ? trustPromptMode : appMode,\n\t\t\t\t\t\t\t\t\t\t\t\tsettingsManager: startupSettingsManager,\n\t\t\t\t\t\t\t\t\t\t\t\thasUI: isInitialRuntime && trustPromptMode === \"interactive\",\n\t\t\t\t\t\t\t\t\t\t\t})),\n\t\t\t\t\t\t\t\tonExtensionError: (message) => projectTrustDiagnostics.push({ type: \"warning\", message }),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tprojectTrustByCwd.set(cwd, trusted);\n\t\t\t\t\t\t\treturn trusted;\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: skipMissingPackageInstall\n\t\t\t\t\t? { skipMissingPackageInstall }\n\t\t\t\t\t: undefined,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates,\n\t\t\t\tnoThemes: parsed.noThemes,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\tappendSystemPrompt: parsed.appendSystemPrompt,\n\t\t\t\textensionFactories: [...builtinRoster, ...(options?.extensionFactories ?? [])],\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...projectTrustDiagnostics,\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRuntime) : [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRuntime,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Cache-only: do not wait on GitHub / provider /v1/models before first paint.\n\t\t\t\tawait modelRuntime.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey, {\n\t\t\t\t\tallowNetwork: false,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\texcludeTools: sessionOptions.excludeTools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t});\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\tif (startupShell?.isExitRequested) {\n\t\tawait runtime.dispose();\n\t\treturn;\n\t}\n\tmarkStartupMilestone(\"runtime_hydrated\");\n\ttime(\"createAgentSessionRuntime\");\n\tif (appMode !== \"interactive\") await runSessionRetention();\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\tapplyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());\n\n\t// Point the model-tier bridge at the live runtime settings manager so /settings\n\t// changes take effect without a restart.\n\tregisterModelTierBridge(settingsManager);\n\tgetModelTiersBridge()?.setParentThinkingProvider(() => session.thinkingLevel);\n\tregisterMemoryCapBridge(settingsManager);\n\tregisterCustomizeBridge(settingsManager);\n\tregisterUsageServiceBridge(modelRuntime, settingsManager);\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRuntime, searchPattern);\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC.\n\t// Startup benchmark must stay interactive even if a leftover redirected handle has bytes.\n\tlet stdinContent: string | undefined;\n\tconst startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);\n\tif (appMode !== \"rpc\" && !startupBenchmark) {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\tif (appMode === \"interactive\" && !startupShell && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\ttime(\"resolveModelScope\");\n\tif (!(appMode === \"interactive\" && startupShell)) reportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tconst extensionHint = runtime.diagnostics.some((diagnostic) =>\n\t\t\tdiagnostic.message.includes(\"Failed to load extension\"),\n\t\t)\n\t\t\t? `\\n${EXTENSION_LOAD_FAILURE_HINT}`\n\t\t\t: \"\";\n\t\tif (startupShell) {\n\t\t\tawait startupShell.fail(\n\t\t\t\t`${runtime.diagnostics.map((diagnostic) => diagnostic.message).join(\"\\n\")}${extensionHint}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (extensionHint) console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: PI_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tconst { runRpcMode } = await import(\"./modes/rpc/rpc-mode.ts\");\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tconst [{ InteractiveMode }, { loadDeferredBuiltinExtensionsResult }] = await Promise.all([\n\t\t\tinteractiveModeImport!,\n\t\t\timport(\"./builtin-extensions/index.ts\"),\n\t\t]);\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tautoTrustOnReloadCwd,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t\tdeprecationWarnings: startupShell ? deprecationWarnings : undefined,\n\t\t\tstartupDiagnostics: startupShell\n\t\t\t\t? runtime.diagnostics.filter((diagnostic) => diagnostic.type !== \"error\")\n\t\t\t\t: undefined,\n\t\t\tstartupShellBinding: startupShell?.binding(),\n\t\t\tdeferredMaintenance: runSessionRetention,\n\t\t\tdeferredBuiltinFactories: parsed.noExtensions ? undefined : loadDeferredBuiltinExtensionsResult,\n\t\t\tonDeferredBuiltinsAttached: rememberAttachedFactories,\n\t\t});\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\tawait interactiveMode.waitForPromptBarrier();\n\t\t\ttime(\"promptBarrier\");\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tprintTimings();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tawait interactiveMode.init();\n\t\ttime(\"interactiveMode.init\");\n\t\tprintTimings();\n\t\tawait interactiveMode.run();\n\t} else {\n\t\tconst { runPrintMode } = await import(\"./modes/print-mode.ts\");\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgCH,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,4BAA4B,CAAC;AAyBvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAa9E;;;GAGG;AACH,wBAAsB,cAAc,CACnC,MAAM,GAAE,MAAM,CAAC,UAA0B,EACzC,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACtD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0C7B;AA2aD,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACvC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBAoe/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@ashx-j/lunr-ai\";\nimport chalk from \"chalk\";\nimport { lightBuiltinExtensions, loadAllBuiltinExtensions } from \"./builtin-extensions/index.ts\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.ts\";\nimport { processFileArguments } from \"./cli/file-processor.ts\";\nimport { buildInitialMessage } from \"./cli/initial-message.ts\";\nimport { handleInstallCli } from \"./cli/install-cli.ts\";\nimport { listModels } from \"./cli/list-models.ts\";\nimport { createProjectTrustContext } from \"./cli/project-trust.ts\";\nimport { selectSession } from \"./cli/session-picker.ts\";\nimport { shouldRunFirstTimeSetup, showFirstTimeSetup, showStartupSelector } from \"./cli/startup-ui.ts\";\nimport { handleUpdateCli } from \"./cli/update-cli.ts\";\nimport {\n\tAPP_NAME,\n\tappendDebugLog,\n\tENV_SESSION_DIR,\n\texpandTildePath,\n\tgetAgentDir,\n\tgetSessionsDir,\n\tVERSION,\n} from \"./config.ts\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.ts\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.ts\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.ts\";\nimport { registerCustomizeBridge } from \"./core/customize.ts\";\nimport type { InlineExtension, ProjectTrustContext } from \"./core/extensions/types.ts\";\nimport { applyHttpProxySettings, configureHttpDispatcher } from \"./core/http-dispatcher.ts\";\nimport { registerMemoryCapBridge } from \"./core/memory-cap.ts\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.ts\";\nimport type { ModelRuntime } from \"./core/model-runtime.ts\";\nimport { registerModelTierBridge } from \"./core/model-tiers.ts\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.ts\";\nimport { type AppMode, resolveProjectTrusted } from \"./core/project-trust.ts\";\nimport { bindRuntimeBridges } from \"./core/runtime-bridges.ts\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.ts\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.ts\";\nimport { assertValidSessionId, SessionManager } from \"./core/session-manager.ts\";\nimport { pruneOldSessions } from \"./core/session-retention.ts\";\nimport { SettingsManager } from \"./core/settings-manager.ts\";\nimport { applyInheritedSubagentPermissions } from \"./core/subagent-permission-inherit.ts\";\nimport { printTimings, resetTimings, time } from \"./core/timings.ts\";\nimport { hasTrustRequiringProjectResources, ProjectTrustStore } from \"./core/trust-manager.ts\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.ts\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.ts\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.ts\";\nimport type { InteractiveStartupShell } from \"./startup/interactive-shell.ts\";\nimport { markStartupMilestone } from \"./startup/startup-milestones.ts\";\nimport { isLocalPath, normalizePath, resolvePath } from \"./utils/paths.ts\";\n\nconst EXTENSION_LOAD_FAILURE_HINT = `Hint: Start without extensions using \"${APP_NAME} -ne\".`;\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nconst PIPED_STDIN_FIRST_CHUNK_MS = 50;\nconst PIPED_STDIN_IDLE_MS = 2000;\n\n/**\n * Read piped stdin. A non-TTY that never emits data (Windows console, leftover\n * redirected handle) must not block interactive start forever.\n */\nexport async function readPipedStdin(\n\tstream: NodeJS.ReadStream = process.stdin,\n\toptions: { firstChunkMs?: number; idleMs?: number } = {},\n): Promise<string | undefined> {\n\tif (stream.isTTY) {\n\t\treturn undefined;\n\t}\n\n\tconst firstChunkMs = options.firstChunkMs ?? PIPED_STDIN_FIRST_CHUNK_MS;\n\tconst idleMs = options.idleMs ?? PIPED_STDIN_IDLE_MS;\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tlet settled = false;\n\t\tlet sawData = false;\n\t\tlet idleTimer: ReturnType<typeof setTimeout> | undefined;\n\t\tconst firstChunkTimer = setTimeout(() => {\n\t\t\tif (!sawData) finish(undefined);\n\t\t}, firstChunkMs);\n\n\t\tconst finish = (value: string | undefined) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(firstChunkTimer);\n\t\t\tif (idleTimer) clearTimeout(idleTimer);\n\t\t\tstream.off(\"data\", onData);\n\t\t\tstream.off(\"end\", onEnd);\n\t\t\tstream.off(\"error\", onEnd);\n\t\t\tresolve(value);\n\t\t};\n\n\t\tconst onEnd = () => finish(data.trim() || undefined);\n\t\tconst onData = (chunk: string | Buffer) => {\n\t\t\tsawData = true;\n\t\t\tdata += typeof chunk === \"string\" ? chunk : chunk.toString(\"utf8\");\n\t\t\tif (idleTimer) clearTimeout(idleTimer);\n\t\t\tidleTimer = setTimeout(onEnd, idleMs);\n\t\t};\n\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", onData);\n\t\tstream.on(\"end\", onEnd);\n\t\tstream.on(\"error\", onEnd);\n\t\tstream.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean, stdoutIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\t// PI_STARTUP_BENCHMARK=1 times interactive first paint even when stdin/stdout\n\t// are not TTYs (repo npx, CI, redirected logs).\n\tif (isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK) && !parsed.print) {\n\t\treturn \"interactive\";\n\t}\n\tif (parsed.print || !stdinIsTTY || !stdoutIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nfunction isPlainRuntimeMetadataCommand(parsed: Args): boolean {\n\treturn !parsed.print && parsed.mode === undefined && (parsed.help === true || parsed.listModels !== undefined);\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function findLocalSessionByExactId(\n\tsessionId: string,\n\tcwd: string,\n\tsessionDir?: string,\n): Promise<{ type: \"local\"; path: string } | undefined> {\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch = localSessions.find((s) => s.id === sessionId);\n\treturn localMatch ? { type: \"local\", path: localMatch.path } : undefined;\n}\n\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, resolve it before handing it to the session manager.\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: resolvePath(sessionArg, cwd) };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatch =\n\t\tlocalSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatch) {\n\t\treturn { type: \"local\", path: localMatch.path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll(sessionDir);\n\tconst globalMatch =\n\t\tallSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatch) {\n\t\treturn { type: \"global\", path: globalMatch.path, cwd: globalMatch.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string, startupShell?: InteractiveStartupShell): Promise<boolean> {\n\tif (startupShell) {\n\t\treturn startupShell.confirm(\"Confirm\", message);\n\t}\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction validateSessionIdFlags(parsed: Args): void {\n\tif (parsed.sessionId === undefined) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n\n\ttry {\n\t\tassertValidSessionId(parsed.sessionId);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction openSessionOrExit(path: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.open(path, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string, sessionId?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n\tstartupShell?: InteractiveStartupShell,\n): Promise<SessionManager> {\n\tif (parsed.noSession || parsed.help || parsed.listModels !== undefined) {\n\t\treturn SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);\n\t}\n\n\tif (parsed.fork) {\n\t\tif (parsed.sessionId) {\n\t\t\tconst existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\t\tif (existingTarget) {\n\t\t\t\tconsole.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn openSessionOrExit(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\", startupShell);\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\ttry {\n\t\t\tconst currentSessionsLoader = (onProgress?: Parameters<typeof SessionManager.list>[2]) =>\n\t\t\t\tSessionManager.list(cwd, sessionDir, onProgress);\n\t\t\tconst allSessionsLoader = (onProgress?: Parameters<typeof SessionManager.listAll>[1]) =>\n\t\t\t\tSessionManager.listAll(sessionDir, onProgress);\n\t\t\tconst selectedPath = startupShell\n\t\t\t\t? await startupShell.selectSession(currentSessionsLoader, allSessionsLoader)\n\t\t\t\t: await selectSession(currentSessionsLoader, allSessionsLoader, settingsManager);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\tif (parsed.sessionId) {\n\t\tconst existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);\n\t\tif (existingSession) {\n\t\t\treturn SessionManager.open(existingSession.path, sessionDir);\n\t\t}\n\t\tconsole.error(\n\t\t\tchalk.yellow(\n\t\t\t\t`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`,\n\t\t\t),\n\t\t);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRuntime: ModelRuntime,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tcliThinking: parsed.thinking,\n\t\t\tmodelRuntime,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRuntime.getModel(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set as a non-persistent runtime override\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\tif (parsed.excludeTools) {\n\t\toptions.excludeTools = [...parsed.excludeTools];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));\n}\n\nfunction createStartupShellTrustContext(cwd: string, startupShell: InteractiveStartupShell): ProjectTrustContext {\n\treturn {\n\t\tcwd,\n\t\tmode: \"tui\",\n\t\thasUI: true,\n\t\tui: {\n\t\t\tselect: (title, options) =>\n\t\t\t\tstartupShell.select(\n\t\t\t\t\ttitle,\n\t\t\t\t\toptions.map((option) => ({ label: option, value: option })),\n\t\t\t\t),\n\t\t\tconfirm: (title, message) => startupShell.confirm(title, message),\n\t\t\tinput: (title, placeholder) => startupShell.input(title, placeholder),\n\t\t\tnotify: () => {},\n\t\t},\n\t};\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n\tstartupShell?: InteractiveStartupShell,\n): Promise<string | undefined> {\n\tconst options = [\n\t\t{ label: \"Continue\", value: issue.fallbackCwd },\n\t\t{ label: \"Cancel\", value: undefined },\n\t];\n\treturn startupShell\n\t\t? startupShell.select(formatMissingSessionCwdPrompt(issue), options)\n\t\t: showStartupSelector(settingsManager, formatMissingSessionCwdPrompt(issue), options);\n}\n\nexport interface MainOptions {\n\textensionFactories?: InlineExtension[];\n\tstartupShell?: InteractiveStartupShell;\n}\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst offlineMode = args.includes(\"--offline\") || isTruthyEnvFlag(process.env.PI_OFFLINE);\n\tif (offlineMode) {\n\t\tprocess.env.PI_OFFLINE = \"1\";\n\t}\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tapplyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher();\n\n\tif (await handleInstallCli(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleUpdateCli(args)) {\n\t\treturn;\n\t}\n\n\tif (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) {\n\t\tconst exitCode = process.exitCode ?? 0;\n\t\tprocess.exit(exitCode);\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args, { extensionFactories: options?.extensionFactories })) {\n\t\treturn;\n\t}\n\n\tif (args[0] === \"gateway\") {\n\t\tconst { handleGatewayCommand } = await import(\"./gateway/command.ts\");\n\t\tif (await handleGatewayCommand(args)) return;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst { exportFromFile } = await import(\"./core/export-html/index.ts\");\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY, process.stdout.isTTY);\n\tconst interactiveModeImport =\n\t\tappMode === \"interactive\" ? import(\"./modes/interactive/interactive-mode.ts\") : undefined;\n\tconst startupShell = appMode === \"interactive\" ? options?.startupShell : undefined;\n\tif (options?.startupShell && !startupShell) options.startupShell.stop();\n\t// lunr: parent-delegated children inherit plan or auto before any tool call.\n\t// Non-child print/json stays fail-closed (module default manual, no handler).\n\tapplyInheritedSubagentPermissions();\n\tconst shouldTakeOverStdout = appMode !== \"interactive\" && !isPlainRuntimeMetadataCommand(parsed);\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\tvalidateSessionIdFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd);\n\ttime(\"runMigrations\");\n\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Register the model-tier bridge before extensions load so pi-subagents can read\n\t// tier settings when building its tool description. Re-pointed to the runtime\n\t// settings manager below once services exist.\n\tregisterModelTierBridge(startupSettingsManager);\n\t// lunr: same for the simple-pi-memory character cap — extensions read it via the\n\t// bridge at load/runtime; re-pointed to the runtime settings manager below.\n\tregisterMemoryCapBridge(startupSettingsManager);\n\t// lunr: TUI customize settings (spinner, dividers, rail, prompt symbol) read by\n\t// ashxj-spinners and ashxj-tui via the bridge; re-pointed to runtime below.\n\tregisterCustomizeBridge(startupSettingsManager);\n\n\t// Experimental first-time setup: theme choice and analytics opt-in.\n\t// Runs before any runtime services are created so the chosen settings apply everywhere.\n\tif (appMode === \"interactive\" && !parsed.help && parsed.listModels === undefined && shouldRunFirstTimeSetup()) {\n\t\tstartupShell?.pause();\n\t\tawait showFirstTimeSetup(startupSettingsManager);\n\t\tstartupShell?.resume();\n\t\ttime(\"firstTimeSetup\");\n\t}\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\t(parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager, startupShell);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(\n\t\t\t\tmissingSessionCwdIssue,\n\t\t\t\tstartupSettingsManager,\n\t\t\t\tstartupShell,\n\t\t\t);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\tif (parsed.name !== undefined) {\n\t\tconst name = parsed.name.trim();\n\t\tif (!name) {\n\t\t\tconsole.error(chalk.red(\"Error: --name requires a non-empty value\"));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tsessionManager.appendSessionInfo(name);\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst runSessionRetention = async () => {\n\t\ttry {\n\t\t\tconst retentionDays = startupSettingsManager.getSessionRetentionDays();\n\t\t\tif (retentionDays <= 0) return;\n\t\t\tconst activeSessionFile = sessionManager.getSessionFile();\n\t\t\tconst { deleted } = await pruneOldSessions(getSessionsDir(), retentionDays, {\n\t\t\t\texcludeFile: activeSessionFile,\n\t\t\t});\n\t\t\tif (sessionDir) {\n\t\t\t\tconst extra = await pruneOldSessions(sessionDir, retentionDays, { excludeFile: activeSessionFile });\n\t\t\t\tdeleted.push(...extra.deleted);\n\t\t\t}\n\t\t\tif (deleted.length > 0) {\n\t\t\t\tappendDebugLog(\n\t\t\t\t\t`session-retention: deleted ${deleted.length} file(s):\\n${deleted.map((path) => ` ${path}`).join(\"\\n\")}`,\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tappendDebugLog(`session-retention: prune failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\t};\n\n\tconst trustStore = new ProjectTrustStore(agentDir);\n\tconst sessionCwd = sessionManager.getCwd();\n\tconst autoTrustOnReloadCwd =\n\t\tparsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd)\n\t\t\t? sessionCwd\n\t\t\t: undefined;\n\tconst trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? \"print\" : appMode;\n\tconst projectTrustByCwd = new Map<string, boolean>();\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\tconst builtinRoster = appMode === \"interactive\" ? [...lightBuiltinExtensions] : await loadAllBuiltinExtensions();\n\tconst rememberAttachedFactories = (factories: InlineExtension[]) => {\n\t\tconst seen = new Set(\n\t\t\tbuiltinRoster.map((entry) => (typeof entry === \"function\" ? undefined : entry.name)).filter(Boolean),\n\t\t);\n\t\tfor (const factory of factories) {\n\t\t\tconst name = typeof factory === \"function\" ? undefined : factory.name;\n\t\t\tif (name && seen.has(name)) continue;\n\t\t\tif (name) seen.add(name);\n\t\t\tbuiltinRoster.push(factory);\n\t\t}\n\t};\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t\tprojectTrustContext,\n\t}) => {\n\t\tconst isInitialRuntime = sessionStartEvent === undefined;\n\t\tconst projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\t\tconst cachedProjectTrust = projectTrustByCwd.get(cwd);\n\t\tconst hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);\n\t\tconst shouldResolveProjectTrust =\n\t\t\tparsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources;\n\t\tconst projectTrusted = shouldResolveProjectTrust\n\t\t\t? false\n\t\t\t: (cachedProjectTrust ??\n\t\t\t\tparsed.projectTrustOverride ??\n\t\t\t\t(!hasTrustRequiringResources || trustStore.get(cwd) === true));\n\t\tconst runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });\n\t\tconst skipMissingPackageInstall = isInitialRuntime && appMode === \"interactive\";\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tsettingsManager: runtimeSettingsManager,\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderReloadOptions: shouldResolveProjectTrust\n\t\t\t\t? {\n\t\t\t\t\t\tskipMissingPackageInstall,\n\t\t\t\t\t\tresolveProjectTrust: async ({ extensionsResult }) => {\n\t\t\t\t\t\t\tconst trusted = await resolveProjectTrusted({\n\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\ttrustStore,\n\t\t\t\t\t\t\t\ttrustOverride: parsed.projectTrustOverride,\n\t\t\t\t\t\t\t\tdefaultProjectTrust: startupSettingsManager.getDefaultProjectTrust(),\n\t\t\t\t\t\t\t\textensionsResult,\n\t\t\t\t\t\t\t\tprojectTrustContext:\n\t\t\t\t\t\t\t\t\tprojectTrustContext ??\n\t\t\t\t\t\t\t\t\t(isInitialRuntime && startupShell\n\t\t\t\t\t\t\t\t\t\t? createStartupShellTrustContext(cwd, startupShell)\n\t\t\t\t\t\t\t\t\t\t: createProjectTrustContext({\n\t\t\t\t\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\t\t\t\t\tmode: isInitialRuntime ? trustPromptMode : appMode,\n\t\t\t\t\t\t\t\t\t\t\t\tsettingsManager: startupSettingsManager,\n\t\t\t\t\t\t\t\t\t\t\t\thasUI: isInitialRuntime && trustPromptMode === \"interactive\",\n\t\t\t\t\t\t\t\t\t\t\t})),\n\t\t\t\t\t\t\t\tonExtensionError: (message) => projectTrustDiagnostics.push({ type: \"warning\", message }),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tprojectTrustByCwd.set(cwd, trusted);\n\t\t\t\t\t\t\treturn trusted;\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: skipMissingPackageInstall\n\t\t\t\t\t? { skipMissingPackageInstall }\n\t\t\t\t\t: undefined,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates,\n\t\t\t\tnoThemes: parsed.noThemes,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\tappendSystemPrompt: parsed.appendSystemPrompt,\n\t\t\t\textensionFactories: [...builtinRoster, ...(options?.extensionFactories ?? [])],\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...projectTrustDiagnostics,\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRuntime) : [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRuntime,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Cache-only: do not wait on GitHub / provider /v1/models before first paint.\n\t\t\t\tawait modelRuntime.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey, {\n\t\t\t\t\tallowNetwork: false,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\texcludeTools: sessionOptions.excludeTools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t});\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t\tonRuntimeApplied: bindRuntimeBridges,\n\t});\n\tif (startupShell?.isExitRequested) {\n\t\tawait runtime.dispose();\n\t\treturn;\n\t}\n\tmarkStartupMilestone(\"runtime_hydrated\");\n\ttime(\"createAgentSessionRuntime\");\n\tif (appMode !== \"interactive\") await runSessionRetention();\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRuntime, resourceLoader } = services;\n\tapplyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);\n\tconfigureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRuntime, searchPattern);\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC.\n\t// Startup benchmark must stay interactive even if a leftover redirected handle has bytes.\n\tlet stdinContent: string | undefined;\n\tconst startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);\n\tif (appMode !== \"rpc\" && !startupBenchmark) {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\tif (appMode === \"interactive\" && !startupShell && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\ttime(\"resolveModelScope\");\n\tif (!(appMode === \"interactive\" && startupShell)) reportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tconst extensionHint = runtime.diagnostics.some((diagnostic) =>\n\t\t\tdiagnostic.message.includes(\"Failed to load extension\"),\n\t\t)\n\t\t\t? `\\n${EXTENSION_LOAD_FAILURE_HINT}`\n\t\t\t: \"\";\n\t\tif (startupShell) {\n\t\t\tawait startupShell.fail(\n\t\t\t\t`${runtime.diagnostics.map((diagnostic) => diagnostic.message).join(\"\\n\")}${extensionHint}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tif (extensionHint) console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: PI_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tconst { runRpcMode } = await import(\"./modes/rpc/rpc-mode.ts\");\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tconst [{ InteractiveMode }, { loadDeferredBuiltinExtensionsResult }] = await Promise.all([\n\t\t\tinteractiveModeImport!,\n\t\t\timport(\"./builtin-extensions/index.ts\"),\n\t\t]);\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tautoTrustOnReloadCwd,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t\tdeprecationWarnings: startupShell ? deprecationWarnings : undefined,\n\t\t\tstartupDiagnostics: startupShell\n\t\t\t\t? runtime.diagnostics.filter((diagnostic) => diagnostic.type !== \"error\")\n\t\t\t\t: undefined,\n\t\t\tstartupShellBinding: startupShell?.binding(),\n\t\t\tdeferredMaintenance: runSessionRetention,\n\t\t\tdeferredBuiltinFactories: parsed.noExtensions ? undefined : loadDeferredBuiltinExtensionsResult,\n\t\t\tonDeferredBuiltinsAttached: rememberAttachedFactories,\n\t\t});\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\tawait interactiveMode.waitForPromptBarrier();\n\t\t\ttime(\"promptBarrier\");\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tprintTimings();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tawait interactiveMode.init();\n\t\ttime(\"interactiveMode.init\");\n\t\tprintTimings();\n\t\tawait interactiveMode.run();\n\t} else {\n\t\tconst { runPrintMode } = await import(\"./modes/print-mode.ts\");\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
package/dist/main.js CHANGED
@@ -25,9 +25,10 @@ import { registerCustomizeBridge } from "./core/customize.js";
25
25
  import { applyHttpProxySettings, configureHttpDispatcher } from "./core/http-dispatcher.js";
26
26
  import { registerMemoryCapBridge } from "./core/memory-cap.js";
27
27
  import { resolveCliModel, resolveModelScope } from "./core/model-resolver.js";
28
- import { getModelTiersBridge, registerModelTierBridge } from "./core/model-tiers.js";
28
+ import { registerModelTierBridge } from "./core/model-tiers.js";
29
29
  import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
30
30
  import { resolveProjectTrusted } from "./core/project-trust.js";
31
+ import { bindRuntimeBridges } from "./core/runtime-bridges.js";
31
32
  import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
32
33
  import { assertValidSessionId, SessionManager } from "./core/session-manager.js";
33
34
  import { pruneOldSessions } from "./core/session-retention.js";
@@ -35,7 +36,6 @@ import { SettingsManager } from "./core/settings-manager.js";
35
36
  import { applyInheritedSubagentPermissions } from "./core/subagent-permission-inherit.js";
36
37
  import { printTimings, resetTimings, time } from "./core/timings.js";
37
38
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.js";
38
- import { registerUsageServiceBridge } from "./core/usage-service.js";
39
39
  import { runMigrations, showDeprecationWarnings } from "./migrations.js";
40
40
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
41
41
  import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
@@ -719,6 +719,7 @@ export async function main(args, options) {
719
719
  cwd: sessionManager.getCwd(),
720
720
  agentDir,
721
721
  sessionManager,
722
+ onRuntimeApplied: bindRuntimeBridges,
722
723
  });
723
724
  if (startupShell?.isExitRequested) {
724
725
  await runtime.dispose();
@@ -732,13 +733,6 @@ export async function main(args, options) {
732
733
  const { settingsManager, modelRuntime, resourceLoader } = services;
733
734
  applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);
734
735
  configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
735
- // Point the model-tier bridge at the live runtime settings manager so /settings
736
- // changes take effect without a restart.
737
- registerModelTierBridge(settingsManager);
738
- getModelTiersBridge()?.setParentThinkingProvider(() => session.thinkingLevel);
739
- registerMemoryCapBridge(settingsManager);
740
- registerCustomizeBridge(settingsManager);
741
- registerUsageServiceBridge(modelRuntime, settingsManager);
742
736
  if (parsed.help) {
743
737
  const extensionFlags = resourceLoader
744
738
  .getExtensions()