@ashx-j/lunr 0.2.7 → 0.2.9

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 (164) hide show
  1. package/dist/builtin-extensions/ashxj-spinners.d.ts +7 -0
  2. package/dist/builtin-extensions/ashxj-spinners.d.ts.map +1 -1
  3. package/dist/builtin-extensions/ashxj-spinners.js +40 -3
  4. package/dist/builtin-extensions/ashxj-spinners.js.map +1 -1
  5. package/dist/builtin-extensions/ashxj-tui.d.ts +4 -0
  6. package/dist/builtin-extensions/ashxj-tui.d.ts.map +1 -1
  7. package/dist/builtin-extensions/ashxj-tui.js +44 -0
  8. package/dist/builtin-extensions/ashxj-tui.js.map +1 -1
  9. package/dist/builtin-extensions/lunr-behavior.d.ts +2 -1
  10. package/dist/builtin-extensions/lunr-behavior.d.ts.map +1 -1
  11. package/dist/builtin-extensions/lunr-behavior.js +26 -14
  12. package/dist/builtin-extensions/lunr-behavior.js.map +1 -1
  13. package/dist/builtin-extensions/lunr-todos.d.ts +4 -7
  14. package/dist/builtin-extensions/lunr-todos.d.ts.map +1 -1
  15. package/dist/builtin-extensions/lunr-todos.js +18 -10
  16. package/dist/builtin-extensions/lunr-todos.js.map +1 -1
  17. package/dist/builtin-extensions/pi-subagents/src/agents/agent-management.d.ts.map +1 -1
  18. package/dist/builtin-extensions/pi-subagents/src/agents/agent-management.js +3 -3
  19. package/dist/builtin-extensions/pi-subagents/src/agents/agent-management.js.map +1 -1
  20. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.d.ts.map +1 -1
  21. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.js +2 -2
  22. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.js.map +1 -1
  23. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  24. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +1 -7
  25. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  26. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.d.ts.map +1 -1
  27. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js +2 -2
  28. package/dist/builtin-extensions/pi-subagents/src/extension/schemas.js.map +1 -1
  29. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +2 -2
  30. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  31. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +3 -4
  32. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  33. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
  34. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +21 -0
  35. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
  36. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  37. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +6 -18
  38. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  39. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.d.ts.map +1 -1
  40. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js +13 -5
  41. package/dist/builtin-extensions/pi-subagents/src/runs/shared/model-fallback.js.map +1 -1
  42. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.d.ts +21 -0
  43. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.d.ts.map +1 -0
  44. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.js +22 -0
  45. package/dist/builtin-extensions/pi-subagents/src/shared/lunr-child-context.js.map +1 -0
  46. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +1 -0
  47. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
  48. package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
  49. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.d.ts.map +1 -1
  50. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js +2 -1
  51. package/dist/builtin-extensions/pi-subagents/src/slash/prompt-workflows.js.map +1 -1
  52. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.d.ts.map +1 -1
  53. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js +10 -18
  54. package/dist/builtin-extensions/pi-subagents/src/slash/slash-commands.js.map +1 -1
  55. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +3 -0
  56. package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
  57. package/dist/builtin-extensions/pi-subagents/src/tui/render.js +9 -11
  58. package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
  59. package/dist/builtin-extensions/pi-web-access/render-search-error.d.ts.map +1 -1
  60. package/dist/builtin-extensions/pi-web-access/render-search-error.js +1 -1
  61. package/dist/builtin-extensions/pi-web-access/render-search-error.js.map +1 -1
  62. package/dist/catalog/models.json +1 -1
  63. package/dist/catalog/providers/github-copilot.json +1 -1
  64. package/dist/catalog/providers/google-vertex.json +1 -1
  65. package/dist/catalog/providers/google.json +1 -1
  66. package/dist/catalog/providers/nvidia.json +1 -1
  67. package/dist/catalog/providers/opencode-go.json +1 -1
  68. package/dist/catalog/providers/opencode.json +1 -1
  69. package/dist/catalog/providers/openrouter.json +1 -1
  70. package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
  71. package/dist/catalog/publication.json +3 -3
  72. package/dist/cli/update-cli.d.ts +16 -0
  73. package/dist/cli/update-cli.d.ts.map +1 -0
  74. package/dist/cli/update-cli.js +83 -0
  75. package/dist/cli/update-cli.js.map +1 -0
  76. package/dist/cli.d.ts.map +1 -1
  77. package/dist/cli.js +6 -0
  78. package/dist/cli.js.map +1 -1
  79. package/dist/core/behavior-preset.d.ts +60 -0
  80. package/dist/core/behavior-preset.d.ts.map +1 -0
  81. package/dist/core/behavior-preset.js +257 -0
  82. package/dist/core/behavior-preset.js.map +1 -0
  83. package/dist/core/customize.d.ts +3 -0
  84. package/dist/core/customize.d.ts.map +1 -1
  85. package/dist/core/customize.js +9 -0
  86. package/dist/core/customize.js.map +1 -1
  87. package/dist/core/footer-data-provider.d.ts +12 -1
  88. package/dist/core/footer-data-provider.d.ts.map +1 -1
  89. package/dist/core/footer-data-provider.js +70 -5
  90. package/dist/core/footer-data-provider.js.map +1 -1
  91. package/dist/core/keybindings.d.ts +2 -2
  92. package/dist/core/keybindings.d.ts.map +1 -1
  93. package/dist/core/keybindings.js +1 -1
  94. package/dist/core/keybindings.js.map +1 -1
  95. package/dist/core/model-tiers.d.ts +9 -0
  96. package/dist/core/model-tiers.d.ts.map +1 -1
  97. package/dist/core/model-tiers.js +16 -0
  98. package/dist/core/model-tiers.js.map +1 -1
  99. package/dist/core/rollback.d.ts +1 -1
  100. package/dist/core/rollback.d.ts.map +1 -1
  101. package/dist/core/rollback.js +1 -1
  102. package/dist/core/rollback.js.map +1 -1
  103. package/dist/core/settings-manager.d.ts +23 -0
  104. package/dist/core/settings-manager.d.ts.map +1 -1
  105. package/dist/core/settings-manager.js +66 -0
  106. package/dist/core/settings-manager.js.map +1 -1
  107. package/dist/core/slash-commands.d.ts.map +1 -1
  108. package/dist/core/slash-commands.js +2 -1
  109. package/dist/core/slash-commands.js.map +1 -1
  110. package/dist/core/tools/bash.d.ts.map +1 -1
  111. package/dist/core/tools/bash.js +1 -3
  112. package/dist/core/tools/bash.js.map +1 -1
  113. package/dist/core/update-check.d.ts +32 -0
  114. package/dist/core/update-check.d.ts.map +1 -0
  115. package/dist/core/update-check.js +112 -0
  116. package/dist/core/update-check.js.map +1 -0
  117. package/dist/core/usage-service.d.ts +24 -1
  118. package/dist/core/usage-service.d.ts.map +1 -1
  119. package/dist/core/usage-service.js +91 -14
  120. package/dist/core/usage-service.js.map +1 -1
  121. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  122. package/dist/gateway/agent-bridge.js +5 -1
  123. package/dist/gateway/agent-bridge.js.map +1 -1
  124. package/dist/gateway/cron.d.ts.map +1 -1
  125. package/dist/gateway/cron.js +5 -1
  126. package/dist/gateway/cron.js.map +1 -1
  127. package/dist/main.d.ts.map +1 -1
  128. package/dist/main.js +13 -1
  129. package/dist/main.js.map +1 -1
  130. package/dist/modes/interactive/components/assistant-message.d.ts +10 -1
  131. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/assistant-message.js +154 -68
  133. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  134. package/dist/modes/interactive/components/settings-selector.d.ts +13 -0
  135. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  136. package/dist/modes/interactive/components/settings-selector.js +157 -10
  137. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  138. package/dist/modes/interactive/components/thinking-summary.d.ts +4 -4
  139. package/dist/modes/interactive/components/thinking-summary.d.ts.map +1 -1
  140. package/dist/modes/interactive/components/thinking-summary.js +7 -5
  141. package/dist/modes/interactive/components/thinking-summary.js.map +1 -1
  142. package/dist/modes/interactive/components/thinking-tail.d.ts.map +1 -1
  143. package/dist/modes/interactive/components/thinking-tail.js +5 -3
  144. package/dist/modes/interactive/components/thinking-tail.js.map +1 -1
  145. package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  146. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  147. package/dist/modes/interactive/components/tool-execution.js +9 -0
  148. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  149. package/dist/modes/interactive/interactive-mode.d.ts +6 -1
  150. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  151. package/dist/modes/interactive/interactive-mode.js +124 -43
  152. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  153. package/docs/keybindings.md +1 -1
  154. package/docs/providers.md +2 -0
  155. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  156. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  157. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  158. package/examples/extensions/gondolin/package-lock.json +2 -2
  159. package/examples/extensions/gondolin/package.json +1 -1
  160. package/examples/extensions/sandbox/package-lock.json +2 -2
  161. package/examples/extensions/sandbox/package.json +1 -1
  162. package/examples/extensions/with-deps/package-lock.json +2 -2
  163. package/examples/extensions/with-deps/package.json +1 -1
  164. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/gateway/cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,aAAa,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAIlD,KAAK,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEhF,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAExG,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,GAAG,EAAE,aAAa,CAAC;IACnB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC,kHAAkH;IAClH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;CAChC;AA+JD,sDAAsD;AACtD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,cAAc,CAqClH;AA8CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG;IAAE,IAAI,IAAI,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA0DlG","sourcesContent":["/**\n * lunR: gateway cron runner + delivery (Phase 4 of the cron/gateway roadmap).\n *\n * startGatewayCron() runs the core/cron scheduler inside the `lunr gateway`\n * daemon:\n *\n * - runJob: each fire gets a FRESH headless agent session (createAgentSession\n * with SessionManager.inMemory — cron runs never write session files and\n * never touch the per-chat gateway sessions; the files under\n * <agentDir>/cron/output/ are the audit trail). The shared fire-guard\n * (core/cron/fire-guard) brackets the turn so the `cron` tool refuses to\n * run inside it (jobs cannot schedule jobs).\n * - deliverResult: replaces the `@lunr/cron-delivery` bridge on globalThis\n * with a platform deliverer. Targets come from job.deliver\n * (comma-separated): \"local\" (no-op — the output file is already written),\n * \"origin\" (the chat that created the job, homeChannel fallback), a bare\n * platform name (that platform's homeChannel), or\n * \"<platform>:<chatId>[:<threadId>]\" (explicit). Content is wrapped as\n * \"☾ Cron: <name>\" and split to the adapter's maxMessageLength.\n *\n * The bridge is re-installed after every cron session creation because loading\n * the builtin extensions (lunr-cron) re-registers its local-notify bridge per\n * session; delivery re-reads the bridge on every call, matching lunr-cron.\n *\n * Fallback models (2026-08-02): settings.json `cronFallbackModels`\n * (\"provider/modelId\" entries) are tried IN ORDER when a fire fails — timeout\n * or any error. Each attempt gets a fresh headless session and\n * jobTimeoutMs/candidates of the budget; a successful fallback run prefixes\n * \"[fell back to provider/modelId]\" to the output. TUI fires are unaffected\n * (they use the live session's model).\n */\n\nimport type { Model } from \"@ashx-j/lunr-ai\";\nimport { beginCronFire, endCronFire } from \"../core/cron/fire-guard.ts\";\nimport type { CronJob, CronJobOrigin } from \"../core/cron/jobs.ts\";\nimport { setCronDeliverValidator } from \"../core/cron/jobs.ts\";\nimport { startScheduler } from \"../core/cron/scheduler.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { type GatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { PlatformAdapter } from \"./types.ts\";\n\nconst DELIVERY_BRIDGE_SYMBOL = Symbol.for(\"@lunr/cron-delivery\");\n\ntype DeliveryBridge = (job: CronJob, content: string) => Promise<string | null>;\n\n/** Test seam / default: one fresh headless session per cron fire. */\nexport interface CronModelRef {\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport type CronSessionFactory = (job: CronJob, modelOverride?: CronModelRef) => Promise<BridgeSession>;\n\nexport interface GatewayCronOptions {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\t/** Tick interval; default 60s. */\n\tintervalMs?: number;\n\t/** Session factory for cron fires; defaults to a real headless agent session. */\n\tsessionFactory?: CronSessionFactory;\n\t/** Test seam: fallback models. When omitted, read fresh from settings.json `cronFallbackModels` on every fire. */\n\tfallbackModels?: CronModelRef[];\n}\n\nconst KNOWN_PLATFORMS = [\"telegram\", \"discord\"] as const;\n\n/** Default factory: fresh in-memory headless session, mirroring agent-bridge's wiring. */\nasync function defaultCronSessionFactory(job: CronJob, modelOverride?: CronModelRef): Promise<BridgeSession> {\n\tconst [\n\t\t{ loadAllBuiltinExtensions },\n\t\t{ getAgentDir },\n\t\t{ registerCustomizeBridge },\n\t\t{ registerMemoryCapBridge },\n\t\t{ registerModelTierBridge },\n\t\t{ createAgentSessionFromServices, createAgentSessionServices },\n\t\t{ SessionManager },\n\t\t{ SettingsManager },\n\t] = await Promise.all([\n\t\timport(\"../builtin-extensions/index.ts\"),\n\t\timport(\"../config.ts\"),\n\t\timport(\"../core/customize.ts\"),\n\t\timport(\"../core/memory-cap.ts\"),\n\t\timport(\"../core/model-tiers.ts\"),\n\t\timport(\"../core/agent-session-services.ts\"),\n\t\timport(\"../core/session-manager.ts\"),\n\t\timport(\"../core/settings-manager.ts\"),\n\t]);\n\tconst cwd = job.workdir ?? process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tregisterModelTierBridge(settingsManager);\n\tregisterMemoryCapBridge(settingsManager);\n\tregisterCustomizeBridge(settingsManager);\n\n\t// In-memory on purpose: cron runs must not persist session files or pollute\n\t// the per-chat gateway sessions — <agentDir>/cron/output/ is the audit trail.\n\tconst sessionManager = SessionManager.inMemory(cwd);\n\n\t// Services-first (mirrors main.ts): extension-registered providers (e.g.\n\t// ollama-cloud) must land in the shared ModelRuntime BEFORE session\n\t// creation — otherwise findInitialModel can't resolve the user's default\n\t// model and silently falls back to an arbitrary catalog provider.\n\tconst services = await createAgentSessionServices({\n\t\tcwd,\n\t\tagentDir,\n\t\tsettingsManager,\n\t\tresourceLoaderOptions: { extensionFactories: await loadAllBuiltinExtensions() },\n\t});\n\t// Fallback-model attempts pin the session model explicitly; an unresolvable\n\t// or unauthenticated override fails the attempt so the next fallback runs.\n\tlet model: Model<any> | undefined;\n\tif (modelOverride) {\n\t\tconst resolved = services.modelRuntime.getModel(modelOverride.provider, modelOverride.modelId);\n\t\tif (!resolved) throw new Error(`model not found: ${modelOverride.provider}/${modelOverride.modelId}`);\n\t\tif (!services.modelRuntime.hasConfiguredAuth(resolved.provider)) {\n\t\t\tthrow new Error(`no configured auth for provider: ${modelOverride.provider}`);\n\t\t}\n\t\tmodel = resolved;\n\t}\n\tconst { session } = await createAgentSessionFromServices({ services, sessionManager, model });\n\tawait session.bindExtensions({\n\t\tmode: \"print\",\n\t\tonError: (err) => console.error(`[gateway cron] extension error (${err.extensionPath}): ${err.error}`),\n\t});\n\treturn session;\n}\n\n/** Final assistant text, print-mode style; throws on error/aborted stop. */\nfunction extractFinalText(session: BridgeSession): string {\n\tconst messages = session.state.messages;\n\tconst last = messages[messages.length - 1];\n\tif (last?.role !== \"assistant\") return \"\";\n\tconst assistant = last as {\n\t\tstopReason?: string;\n\t\terrorMessage?: string;\n\t\tcontent?: Array<{ type: string; text?: string }>;\n\t};\n\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") {\n\t\tthrow new Error(assistant.errorMessage || `Request ${assistant.stopReason}`);\n\t}\n\tlet text = \"\";\n\tfor (const content of assistant.content ?? []) {\n\t\tif (content.type === \"text\") text += content.text ?? \"\";\n\t}\n\treturn text;\n}\n\ninterface ResolvedTarget {\n\tplatform: string;\n\tchatId: string;\n\tthreadId?: string;\n}\n\nconst EXPLICIT_TARGET_RE = /^([a-z0-9_-]+):([^:]+)(?::([^:]+))?$/i;\n\n/** Resolve one deliver target to a concrete destination. Returns an error string on failure. */\nfunction resolveTarget(target: string, job: CronJob, cfg: GatewayConfig): ResolvedTarget | string {\n\tif (target === \"origin\") {\n\t\tconst origin = job.origin;\n\t\tif (origin?.platform && origin.chatId) {\n\t\t\treturn { platform: origin.platform, chatId: origin.chatId, threadId: origin.threadId };\n\t\t}\n\t\t// Missing/incomplete origin → fall back to a homeChannel: the origin's\n\t\t// platform when known, otherwise the first platform that has one.\n\t\tconst platforms = origin?.platform ? [origin.platform] : KNOWN_PLATFORMS;\n\t\tfor (const platform of platforms) {\n\t\t\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\t\t\tif (home) return { platform, chatId: home };\n\t\t}\n\t\treturn `deliver target \"origin\": job has no origin and no homeChannel is configured`;\n\t}\n\n\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\tif (platformConfigFor(cfg, platform) === undefined) {\n\t\treturn `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t}\n\tif (explicit) {\n\t\treturn { platform, chatId: explicit[2], threadId: explicit[3] };\n\t}\n\t// Bare platform name → its homeChannel.\n\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\tif (!home) return `no homeChannel configured for ${platform}`;\n\treturn { platform, chatId: home };\n}\n\nfunction createDeliverValidator(\n\tcfg: GatewayConfig,\n): (deliver: string, origin?: CronJobOrigin | null) => string | undefined {\n\treturn (deliver, origin) => {\n\t\tconst targets = deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue;\n\t\t\tif (target === \"origin\") continue; // delivery time resolves/fails with a sensible error\n\t\t\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\t\t\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\t\t\tconst platformCfg = platformConfigFor(cfg, platform);\n\t\t\tif (!platformCfg) return `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t\t\tif (explicit) {\n\t\t\t\tconst chatId = explicit[2];\n\t\t\t\tconst allowed = new Set(\n\t\t\t\t\t[\n\t\t\t\t\t\tplatformCfg.homeChannel,\n\t\t\t\t\t\t...(platformCfg.allowedChats ?? []),\n\t\t\t\t\t\t...(origin?.platform === platform ? [origin.chatId] : []),\n\t\t\t\t\t].filter((x): x is string => !!x),\n\t\t\t\t);\n\t\t\t\tif (!allowed.has(chatId)) {\n\t\t\t\t\treturn `deliver target \"${target}\" is not an allowed chat for ${platform}`;\n\t\t\t\t}\n\t\t\t} else if (!platformCfg.homeChannel) {\n\t\t\t\treturn `no homeChannel configured for ${platform}`;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t};\n}\n\n/** Wrap the run output with a compact cron header. */\nexport function wrapCronContent(job: CronJob, content: string): string {\n\treturn `☾ Cron: ${job.name}\\n———\\n${content}`;\n}\n\n/**\n * The platform deliverer installed at the `@lunr/cron-delivery` bridge.\n * Every target is attempted; the first error is returned (null on success).\n */\nexport function createPlatformDeliverer(adapters: Map<string, PlatformAdapter>, cfg: GatewayConfig): DeliveryBridge {\n\treturn async (job, content) => {\n\t\tconst targets = job.deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tlet firstError: string | null = null;\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue; // output file already written by the scheduler\n\t\t\tconst resolved = resolveTarget(target, job, cfg);\n\t\t\tif (typeof resolved === \"string\") {\n\t\t\t\tfirstError ??= resolved;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst adapter = adapters.get(resolved.platform);\n\t\t\tif (!adapter) {\n\t\t\t\tfirstError ??= `no adapter connected for ${resolved.platform}`;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst wrapped = wrapCronContent(job, content);\n\t\t\tfor (const chunk of splitMessage(wrapped, adapter.maxMessageLength)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await adapter.send(resolved.chatId, chunk, {\n\t\t\t\t\t\tthreadId: resolved.threadId,\n\t\t\t\t\t});\n\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${result.error ?? \"unknown error\"}`;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${err instanceof Error ? err.message : String(err)}`;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn firstError;\n\t};\n}\n\n/** Race one prompt attempt against its share of the job budget. */\nfunction withAttemptTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\treturn Promise.race([\n\t\tpromise,\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tconst timer = setTimeout(() => reject(new Error(`attempt timed out after ${ms}ms`)), ms);\n\t\t\t// If the attempt settles first this promise is discarded; the timer must not keep the process alive.\n\t\t\ttimer.unref?.();\n\t\t}),\n\t]);\n}\n\n/**\n * Read settings.json `cronFallbackModels` (\"provider/modelId\" entries).\n * Fresh read per fire so edits apply without a daemon restart; invalid\n * entries are skipped with a log line. Never throws.\n */\nasync function readCronFallbackModels(): Promise<CronModelRef[]> {\n\ttry {\n\t\tconst [{ getAgentDir }, { SettingsManager }] = await Promise.all([\n\t\t\timport(\"../config.ts\"),\n\t\t\timport(\"../core/settings-manager.ts\"),\n\t\t]);\n\t\tconst settingsManager = SettingsManager.create(process.cwd(), getAgentDir(), { projectTrusted: false });\n\t\tconst out: CronModelRef[] = [];\n\t\tfor (const entry of settingsManager.getCronFallbackModels()) {\n\t\t\tconst idx = entry.indexOf(\"/\");\n\t\t\tif (idx <= 0 || idx === entry.length - 1) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[gateway cron] ignoring invalid cronFallbackModels entry \"${entry}\" (want \"provider/modelId\")`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tout.push({ provider: entry.slice(0, idx), modelId: entry.slice(idx + 1) });\n\t\t}\n\t\treturn out;\n\t} catch (err) {\n\t\tconsole.error(\n\t\t\t`[gateway cron] failed to read cronFallbackModels: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\n/**\n * Start the cron scheduler inside the gateway daemon. Starts even with zero\n * stored jobs — jobs can be created later from chats. stop() halts the loop.\n */\nexport function startGatewayCron(options: GatewayCronOptions): { stop(): void; intervalMs: number } {\n\tconst { adapters, cfg } = options;\n\tconst sessionFactory = options.sessionFactory ?? defaultCronSessionFactory;\n\tconst platformDeliverer = createPlatformDeliverer(adapters, cfg);\n\n\t// Explicit so runJob can split it into per-attempt budgets that sum to (at\n\t// most) the scheduler's outer race (core/cron/scheduler.ts runSchedulerTick).\n\tconst jobTimeoutMs = 5 * 60 * 1000;\n\n\t// Reject deliver targets that are not local, origin, a configured home channel,\n\t// or an explicit chat in the platform allowlist / the job's origin.\n\tsetCronDeliverValidator(createDeliverValidator(cfg));\n\n\t// Replace the local-notify bridge lunr-cron registered with the platform deliverer.\n\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\n\tconst runJob = async (prompt: string, job: CronJob): Promise<string> => {\n\t\tconst fallbacks = options.fallbackModels ?? (await readCronFallbackModels());\n\t\tconst candidates: Array<CronModelRef | undefined> = [undefined, ...fallbacks];\n\t\tconst attemptTimeoutMs = Math.floor(jobTimeoutMs / candidates.length);\n\t\tconst errors: string[] = [];\n\t\tbeginCronFire();\n\t\ttry {\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tconst label = candidate ? `${candidate.provider}/${candidate.modelId}` : \"default model\";\n\t\t\t\tlet session: BridgeSession | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tsession = await sessionFactory(job, candidate);\n\t\t\t\t\t// Loading the builtin extensions re-registered lunr-cron's local-notify\n\t\t\t\t\t// bridge; restore the platform deliverer before any delivery re-reads it.\n\t\t\t\t\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\t\t\t\t\tawait withAttemptTimeout(session.prompt(prompt, { source: \"extension\" }), attemptTimeoutMs);\n\t\t\t\t\tconst text = extractFinalText(session);\n\t\t\t\t\t// The marker lands in the output file (the audit trail); [SILENT] as\n\t\t\t\t\t// the last line still suppresses delivery (scheduler isSilent check).\n\t\t\t\t\treturn candidate ? `[fell back to ${label}]\\n${text}` : text;\n\t\t\t\t} catch (err) {\n\t\t\t\t\terrors.push(`${label}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t} finally {\n\t\t\t\t\tsession?.dispose?.();\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new Error(errors.join(\" | \") || \"no model candidates\");\n\t\t} finally {\n\t\t\tendCronFire();\n\t\t}\n\t};\n\n\tconst deliverResult = async (job: CronJob, content: string): Promise<void> => {\n\t\t// Re-read the bridge on every delivery (same pattern as lunr-cron).\n\t\tconst bridge = (globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] as DeliveryBridge | undefined;\n\t\tif (!bridge) throw new Error(\"no cron delivery bridge registered\");\n\t\tconst err = await bridge(job, content);\n\t\tif (err) throw new Error(err);\n\t};\n\n\tconst scheduler = startScheduler({ runJob, deliverResult, intervalMs: options.intervalMs, jobTimeoutMs });\n\treturn { stop: () => scheduler.stop(), intervalMs: options.intervalMs ?? 60_000 };\n}\n"]}
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/gateway/cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,aAAa,CAAC;AAEpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAIlD,KAAK,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEhF,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;AAExG,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC,GAAG,EAAE,aAAa,CAAC;IACnB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC,kHAAkH;IAClH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;CAChC;AAoKD,sDAAsD;AACtD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,cAAc,CAqClH;AA8CD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG;IAAE,IAAI,IAAI,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA0DlG","sourcesContent":["/**\n * lunR: gateway cron runner + delivery (Phase 4 of the cron/gateway roadmap).\n *\n * startGatewayCron() runs the core/cron scheduler inside the `lunr gateway`\n * daemon:\n *\n * - runJob: each fire gets a FRESH headless agent session (createAgentSession\n * with SessionManager.inMemory — cron runs never write session files and\n * never touch the per-chat gateway sessions; the files under\n * <agentDir>/cron/output/ are the audit trail). The shared fire-guard\n * (core/cron/fire-guard) brackets the turn so the `cron` tool refuses to\n * run inside it (jobs cannot schedule jobs).\n * - deliverResult: replaces the `@lunr/cron-delivery` bridge on globalThis\n * with a platform deliverer. Targets come from job.deliver\n * (comma-separated): \"local\" (no-op — the output file is already written),\n * \"origin\" (the chat that created the job, homeChannel fallback), a bare\n * platform name (that platform's homeChannel), or\n * \"<platform>:<chatId>[:<threadId>]\" (explicit). Content is wrapped as\n * \"☾ Cron: <name>\" and split to the adapter's maxMessageLength.\n *\n * The bridge is re-installed after every cron session creation because loading\n * the builtin extensions (lunr-cron) re-registers its local-notify bridge per\n * session; delivery re-reads the bridge on every call, matching lunr-cron.\n *\n * Fallback models (2026-08-02): settings.json `cronFallbackModels`\n * (\"provider/modelId\" entries) are tried IN ORDER when a fire fails — timeout\n * or any error. Each attempt gets a fresh headless session and\n * jobTimeoutMs/candidates of the budget; a successful fallback run prefixes\n * \"[fell back to provider/modelId]\" to the output. TUI fires are unaffected\n * (they use the live session's model).\n */\n\nimport type { Model } from \"@ashx-j/lunr-ai\";\nimport { beginCronFire, endCronFire } from \"../core/cron/fire-guard.ts\";\nimport type { CronJob, CronJobOrigin } from \"../core/cron/jobs.ts\";\nimport { setCronDeliverValidator } from \"../core/cron/jobs.ts\";\nimport { startScheduler } from \"../core/cron/scheduler.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { type GatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { PlatformAdapter } from \"./types.ts\";\n\nconst DELIVERY_BRIDGE_SYMBOL = Symbol.for(\"@lunr/cron-delivery\");\n\ntype DeliveryBridge = (job: CronJob, content: string) => Promise<string | null>;\n\n/** Test seam / default: one fresh headless session per cron fire. */\nexport interface CronModelRef {\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport type CronSessionFactory = (job: CronJob, modelOverride?: CronModelRef) => Promise<BridgeSession>;\n\nexport interface GatewayCronOptions {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\t/** Tick interval; default 60s. */\n\tintervalMs?: number;\n\t/** Session factory for cron fires; defaults to a real headless agent session. */\n\tsessionFactory?: CronSessionFactory;\n\t/** Test seam: fallback models. When omitted, read fresh from settings.json `cronFallbackModels` on every fire. */\n\tfallbackModels?: CronModelRef[];\n}\n\nconst KNOWN_PLATFORMS = [\"telegram\", \"discord\"] as const;\n\n/** Default factory: fresh in-memory headless session, mirroring agent-bridge's wiring. */\nasync function defaultCronSessionFactory(job: CronJob, modelOverride?: CronModelRef): Promise<BridgeSession> {\n\tconst [\n\t\t{ loadAllBuiltinExtensions },\n\t\t{ getAgentDir },\n\t\t{ registerCustomizeBridge },\n\t\t{ reconcileBehaviorPreset, registerBehaviorPresetBridge },\n\t\t{ registerMemoryCapBridge },\n\t\t{ getModelTiersBridge, registerModelTierBridge },\n\t\t{ createAgentSessionFromServices, createAgentSessionServices },\n\t\t{ SessionManager },\n\t\t{ SettingsManager },\n\t] = await Promise.all([\n\t\timport(\"../builtin-extensions/index.ts\"),\n\t\timport(\"../config.ts\"),\n\t\timport(\"../core/customize.ts\"),\n\t\timport(\"../core/behavior-preset.ts\"),\n\t\timport(\"../core/memory-cap.ts\"),\n\t\timport(\"../core/model-tiers.ts\"),\n\t\timport(\"../core/agent-session-services.ts\"),\n\t\timport(\"../core/session-manager.ts\"),\n\t\timport(\"../core/settings-manager.ts\"),\n\t]);\n\tconst cwd = job.workdir ?? process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tregisterModelTierBridge(settingsManager);\n\tregisterMemoryCapBridge(settingsManager);\n\tregisterBehaviorPresetBridge(settingsManager);\n\treconcileBehaviorPreset(settingsManager, agentDir);\n\tregisterCustomizeBridge(settingsManager);\n\n\t// In-memory on purpose: cron runs must not persist session files or pollute\n\t// the per-chat gateway sessions — <agentDir>/cron/output/ is the audit trail.\n\tconst sessionManager = SessionManager.inMemory(cwd);\n\n\t// Services-first (mirrors main.ts): extension-registered providers (e.g.\n\t// ollama-cloud) must land in the shared ModelRuntime BEFORE session\n\t// creation — otherwise findInitialModel can't resolve the user's default\n\t// model and silently falls back to an arbitrary catalog provider.\n\tconst services = await createAgentSessionServices({\n\t\tcwd,\n\t\tagentDir,\n\t\tsettingsManager,\n\t\tresourceLoaderOptions: { extensionFactories: await loadAllBuiltinExtensions() },\n\t});\n\t// Fallback-model attempts pin the session model explicitly; an unresolvable\n\t// or unauthenticated override fails the attempt so the next fallback runs.\n\tlet model: Model<any> | undefined;\n\tif (modelOverride) {\n\t\tconst resolved = services.modelRuntime.getModel(modelOverride.provider, modelOverride.modelId);\n\t\tif (!resolved) throw new Error(`model not found: ${modelOverride.provider}/${modelOverride.modelId}`);\n\t\tif (!services.modelRuntime.hasConfiguredAuth(resolved.provider)) {\n\t\t\tthrow new Error(`no configured auth for provider: ${modelOverride.provider}`);\n\t\t}\n\t\tmodel = resolved;\n\t}\n\tconst { session } = await createAgentSessionFromServices({ services, sessionManager, model });\n\tgetModelTiersBridge()?.setParentThinkingProvider(() => session.thinkingLevel);\n\tawait session.bindExtensions({\n\t\tmode: \"print\",\n\t\tonError: (err) => console.error(`[gateway cron] extension error (${err.extensionPath}): ${err.error}`),\n\t});\n\treturn session;\n}\n\n/** Final assistant text, print-mode style; throws on error/aborted stop. */\nfunction extractFinalText(session: BridgeSession): string {\n\tconst messages = session.state.messages;\n\tconst last = messages[messages.length - 1];\n\tif (last?.role !== \"assistant\") return \"\";\n\tconst assistant = last as {\n\t\tstopReason?: string;\n\t\terrorMessage?: string;\n\t\tcontent?: Array<{ type: string; text?: string }>;\n\t};\n\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") {\n\t\tthrow new Error(assistant.errorMessage || `Request ${assistant.stopReason}`);\n\t}\n\tlet text = \"\";\n\tfor (const content of assistant.content ?? []) {\n\t\tif (content.type === \"text\") text += content.text ?? \"\";\n\t}\n\treturn text;\n}\n\ninterface ResolvedTarget {\n\tplatform: string;\n\tchatId: string;\n\tthreadId?: string;\n}\n\nconst EXPLICIT_TARGET_RE = /^([a-z0-9_-]+):([^:]+)(?::([^:]+))?$/i;\n\n/** Resolve one deliver target to a concrete destination. Returns an error string on failure. */\nfunction resolveTarget(target: string, job: CronJob, cfg: GatewayConfig): ResolvedTarget | string {\n\tif (target === \"origin\") {\n\t\tconst origin = job.origin;\n\t\tif (origin?.platform && origin.chatId) {\n\t\t\treturn { platform: origin.platform, chatId: origin.chatId, threadId: origin.threadId };\n\t\t}\n\t\t// Missing/incomplete origin → fall back to a homeChannel: the origin's\n\t\t// platform when known, otherwise the first platform that has one.\n\t\tconst platforms = origin?.platform ? [origin.platform] : KNOWN_PLATFORMS;\n\t\tfor (const platform of platforms) {\n\t\t\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\t\t\tif (home) return { platform, chatId: home };\n\t\t}\n\t\treturn `deliver target \"origin\": job has no origin and no homeChannel is configured`;\n\t}\n\n\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\tif (platformConfigFor(cfg, platform) === undefined) {\n\t\treturn `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t}\n\tif (explicit) {\n\t\treturn { platform, chatId: explicit[2], threadId: explicit[3] };\n\t}\n\t// Bare platform name → its homeChannel.\n\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\tif (!home) return `no homeChannel configured for ${platform}`;\n\treturn { platform, chatId: home };\n}\n\nfunction createDeliverValidator(\n\tcfg: GatewayConfig,\n): (deliver: string, origin?: CronJobOrigin | null) => string | undefined {\n\treturn (deliver, origin) => {\n\t\tconst targets = deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue;\n\t\t\tif (target === \"origin\") continue; // delivery time resolves/fails with a sensible error\n\t\t\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\t\t\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\t\t\tconst platformCfg = platformConfigFor(cfg, platform);\n\t\t\tif (!platformCfg) return `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t\t\tif (explicit) {\n\t\t\t\tconst chatId = explicit[2];\n\t\t\t\tconst allowed = new Set(\n\t\t\t\t\t[\n\t\t\t\t\t\tplatformCfg.homeChannel,\n\t\t\t\t\t\t...(platformCfg.allowedChats ?? []),\n\t\t\t\t\t\t...(origin?.platform === platform ? [origin.chatId] : []),\n\t\t\t\t\t].filter((x): x is string => !!x),\n\t\t\t\t);\n\t\t\t\tif (!allowed.has(chatId)) {\n\t\t\t\t\treturn `deliver target \"${target}\" is not an allowed chat for ${platform}`;\n\t\t\t\t}\n\t\t\t} else if (!platformCfg.homeChannel) {\n\t\t\t\treturn `no homeChannel configured for ${platform}`;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t};\n}\n\n/** Wrap the run output with a compact cron header. */\nexport function wrapCronContent(job: CronJob, content: string): string {\n\treturn `☾ Cron: ${job.name}\\n———\\n${content}`;\n}\n\n/**\n * The platform deliverer installed at the `@lunr/cron-delivery` bridge.\n * Every target is attempted; the first error is returned (null on success).\n */\nexport function createPlatformDeliverer(adapters: Map<string, PlatformAdapter>, cfg: GatewayConfig): DeliveryBridge {\n\treturn async (job, content) => {\n\t\tconst targets = job.deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tlet firstError: string | null = null;\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue; // output file already written by the scheduler\n\t\t\tconst resolved = resolveTarget(target, job, cfg);\n\t\t\tif (typeof resolved === \"string\") {\n\t\t\t\tfirstError ??= resolved;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst adapter = adapters.get(resolved.platform);\n\t\t\tif (!adapter) {\n\t\t\t\tfirstError ??= `no adapter connected for ${resolved.platform}`;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst wrapped = wrapCronContent(job, content);\n\t\t\tfor (const chunk of splitMessage(wrapped, adapter.maxMessageLength)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await adapter.send(resolved.chatId, chunk, {\n\t\t\t\t\t\tthreadId: resolved.threadId,\n\t\t\t\t\t});\n\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${result.error ?? \"unknown error\"}`;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${err instanceof Error ? err.message : String(err)}`;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn firstError;\n\t};\n}\n\n/** Race one prompt attempt against its share of the job budget. */\nfunction withAttemptTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\treturn Promise.race([\n\t\tpromise,\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tconst timer = setTimeout(() => reject(new Error(`attempt timed out after ${ms}ms`)), ms);\n\t\t\t// If the attempt settles first this promise is discarded; the timer must not keep the process alive.\n\t\t\ttimer.unref?.();\n\t\t}),\n\t]);\n}\n\n/**\n * Read settings.json `cronFallbackModels` (\"provider/modelId\" entries).\n * Fresh read per fire so edits apply without a daemon restart; invalid\n * entries are skipped with a log line. Never throws.\n */\nasync function readCronFallbackModels(): Promise<CronModelRef[]> {\n\ttry {\n\t\tconst [{ getAgentDir }, { SettingsManager }] = await Promise.all([\n\t\t\timport(\"../config.ts\"),\n\t\t\timport(\"../core/settings-manager.ts\"),\n\t\t]);\n\t\tconst settingsManager = SettingsManager.create(process.cwd(), getAgentDir(), { projectTrusted: false });\n\t\tconst out: CronModelRef[] = [];\n\t\tfor (const entry of settingsManager.getCronFallbackModels()) {\n\t\t\tconst idx = entry.indexOf(\"/\");\n\t\t\tif (idx <= 0 || idx === entry.length - 1) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[gateway cron] ignoring invalid cronFallbackModels entry \"${entry}\" (want \"provider/modelId\")`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tout.push({ provider: entry.slice(0, idx), modelId: entry.slice(idx + 1) });\n\t\t}\n\t\treturn out;\n\t} catch (err) {\n\t\tconsole.error(\n\t\t\t`[gateway cron] failed to read cronFallbackModels: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\n/**\n * Start the cron scheduler inside the gateway daemon. Starts even with zero\n * stored jobs — jobs can be created later from chats. stop() halts the loop.\n */\nexport function startGatewayCron(options: GatewayCronOptions): { stop(): void; intervalMs: number } {\n\tconst { adapters, cfg } = options;\n\tconst sessionFactory = options.sessionFactory ?? defaultCronSessionFactory;\n\tconst platformDeliverer = createPlatformDeliverer(adapters, cfg);\n\n\t// Explicit so runJob can split it into per-attempt budgets that sum to (at\n\t// most) the scheduler's outer race (core/cron/scheduler.ts runSchedulerTick).\n\tconst jobTimeoutMs = 5 * 60 * 1000;\n\n\t// Reject deliver targets that are not local, origin, a configured home channel,\n\t// or an explicit chat in the platform allowlist / the job's origin.\n\tsetCronDeliverValidator(createDeliverValidator(cfg));\n\n\t// Replace the local-notify bridge lunr-cron registered with the platform deliverer.\n\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\n\tconst runJob = async (prompt: string, job: CronJob): Promise<string> => {\n\t\tconst fallbacks = options.fallbackModels ?? (await readCronFallbackModels());\n\t\tconst candidates: Array<CronModelRef | undefined> = [undefined, ...fallbacks];\n\t\tconst attemptTimeoutMs = Math.floor(jobTimeoutMs / candidates.length);\n\t\tconst errors: string[] = [];\n\t\tbeginCronFire();\n\t\ttry {\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tconst label = candidate ? `${candidate.provider}/${candidate.modelId}` : \"default model\";\n\t\t\t\tlet session: BridgeSession | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tsession = await sessionFactory(job, candidate);\n\t\t\t\t\t// Loading the builtin extensions re-registered lunr-cron's local-notify\n\t\t\t\t\t// bridge; restore the platform deliverer before any delivery re-reads it.\n\t\t\t\t\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\t\t\t\t\tawait withAttemptTimeout(session.prompt(prompt, { source: \"extension\" }), attemptTimeoutMs);\n\t\t\t\t\tconst text = extractFinalText(session);\n\t\t\t\t\t// The marker lands in the output file (the audit trail); [SILENT] as\n\t\t\t\t\t// the last line still suppresses delivery (scheduler isSilent check).\n\t\t\t\t\treturn candidate ? `[fell back to ${label}]\\n${text}` : text;\n\t\t\t\t} catch (err) {\n\t\t\t\t\terrors.push(`${label}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t} finally {\n\t\t\t\t\tsession?.dispose?.();\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new Error(errors.join(\" | \") || \"no model candidates\");\n\t\t} finally {\n\t\t\tendCronFire();\n\t\t}\n\t};\n\n\tconst deliverResult = async (job: CronJob, content: string): Promise<void> => {\n\t\t// Re-read the bridge on every delivery (same pattern as lunr-cron).\n\t\tconst bridge = (globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] as DeliveryBridge | undefined;\n\t\tif (!bridge) throw new Error(\"no cron delivery bridge registered\");\n\t\tconst err = await bridge(job, content);\n\t\tif (err) throw new Error(err);\n\t};\n\n\tconst scheduler = startScheduler({ runJob, deliverResult, intervalMs: options.intervalMs, jobTimeoutMs });\n\treturn { stop: () => scheduler.stop(), intervalMs: options.intervalMs ?? 60_000 };\n}\n"]}
@@ -38,10 +38,11 @@ const DELIVERY_BRIDGE_SYMBOL = Symbol.for("@lunr/cron-delivery");
38
38
  const KNOWN_PLATFORMS = ["telegram", "discord"];
39
39
  /** Default factory: fresh in-memory headless session, mirroring agent-bridge's wiring. */
40
40
  async function defaultCronSessionFactory(job, modelOverride) {
41
- const [{ loadAllBuiltinExtensions }, { getAgentDir }, { registerCustomizeBridge }, { registerMemoryCapBridge }, { registerModelTierBridge }, { createAgentSessionFromServices, createAgentSessionServices }, { SessionManager }, { SettingsManager },] = await Promise.all([
41
+ const [{ loadAllBuiltinExtensions }, { getAgentDir }, { registerCustomizeBridge }, { reconcileBehaviorPreset, registerBehaviorPresetBridge }, { registerMemoryCapBridge }, { getModelTiersBridge, registerModelTierBridge }, { createAgentSessionFromServices, createAgentSessionServices }, { SessionManager }, { SettingsManager },] = await Promise.all([
42
42
  import("../builtin-extensions/index.js"),
43
43
  import("../config.js"),
44
44
  import("../core/customize.js"),
45
+ import("../core/behavior-preset.js"),
45
46
  import("../core/memory-cap.js"),
46
47
  import("../core/model-tiers.js"),
47
48
  import("../core/agent-session-services.js"),
@@ -53,6 +54,8 @@ async function defaultCronSessionFactory(job, modelOverride) {
53
54
  const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
54
55
  registerModelTierBridge(settingsManager);
55
56
  registerMemoryCapBridge(settingsManager);
57
+ registerBehaviorPresetBridge(settingsManager);
58
+ reconcileBehaviorPreset(settingsManager, agentDir);
56
59
  registerCustomizeBridge(settingsManager);
57
60
  // In-memory on purpose: cron runs must not persist session files or pollute
58
61
  // the per-chat gateway sessions — <agentDir>/cron/output/ is the audit trail.
@@ -80,6 +83,7 @@ async function defaultCronSessionFactory(job, modelOverride) {
80
83
  model = resolved;
81
84
  }
82
85
  const { session } = await createAgentSessionFromServices({ services, sessionManager, model });
86
+ getModelTiersBridge()?.setParentThinkingProvider(() => session.thinkingLevel);
83
87
  await session.bindExtensions({
84
88
  mode: "print",
85
89
  onError: (err) => console.error(`[gateway cron] extension error (${err.extensionPath}): ${err.error}`),
@@ -1 +1 @@
1
- {"version":3,"file":"cron.js","sourceRoot":"","sources":["../../src/gateway/cron.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway cron runner + delivery (Phase 4 of the cron/gateway roadmap).\n *\n * startGatewayCron() runs the core/cron scheduler inside the `lunr gateway`\n * daemon:\n *\n * - runJob: each fire gets a FRESH headless agent session (createAgentSession\n * with SessionManager.inMemory — cron runs never write session files and\n * never touch the per-chat gateway sessions; the files under\n * <agentDir>/cron/output/ are the audit trail). The shared fire-guard\n * (core/cron/fire-guard) brackets the turn so the `cron` tool refuses to\n * run inside it (jobs cannot schedule jobs).\n * - deliverResult: replaces the `@lunr/cron-delivery` bridge on globalThis\n * with a platform deliverer. Targets come from job.deliver\n * (comma-separated): \"local\" (no-op — the output file is already written),\n * \"origin\" (the chat that created the job, homeChannel fallback), a bare\n * platform name (that platform's homeChannel), or\n * \"<platform>:<chatId>[:<threadId>]\" (explicit). Content is wrapped as\n * \"☾ Cron: <name>\" and split to the adapter's maxMessageLength.\n *\n * The bridge is re-installed after every cron session creation because loading\n * the builtin extensions (lunr-cron) re-registers its local-notify bridge per\n * session; delivery re-reads the bridge on every call, matching lunr-cron.\n *\n * Fallback models (2026-08-02): settings.json `cronFallbackModels`\n * (\"provider/modelId\" entries) are tried IN ORDER when a fire fails — timeout\n * or any error. Each attempt gets a fresh headless session and\n * jobTimeoutMs/candidates of the budget; a successful fallback run prefixes\n * \"[fell back to provider/modelId]\" to the output. TUI fires are unaffected\n * (they use the live session's model).\n */\n\nimport type { Model } from \"@ashx-j/lunr-ai\";\nimport { beginCronFire, endCronFire } from \"../core/cron/fire-guard.ts\";\nimport type { CronJob, CronJobOrigin } from \"../core/cron/jobs.ts\";\nimport { setCronDeliverValidator } from \"../core/cron/jobs.ts\";\nimport { startScheduler } from \"../core/cron/scheduler.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { type GatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { PlatformAdapter } from \"./types.ts\";\n\nconst DELIVERY_BRIDGE_SYMBOL = Symbol.for(\"@lunr/cron-delivery\");\n\ntype DeliveryBridge = (job: CronJob, content: string) => Promise<string | null>;\n\n/** Test seam / default: one fresh headless session per cron fire. */\nexport interface CronModelRef {\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport type CronSessionFactory = (job: CronJob, modelOverride?: CronModelRef) => Promise<BridgeSession>;\n\nexport interface GatewayCronOptions {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\t/** Tick interval; default 60s. */\n\tintervalMs?: number;\n\t/** Session factory for cron fires; defaults to a real headless agent session. */\n\tsessionFactory?: CronSessionFactory;\n\t/** Test seam: fallback models. When omitted, read fresh from settings.json `cronFallbackModels` on every fire. */\n\tfallbackModels?: CronModelRef[];\n}\n\nconst KNOWN_PLATFORMS = [\"telegram\", \"discord\"] as const;\n\n/** Default factory: fresh in-memory headless session, mirroring agent-bridge's wiring. */\nasync function defaultCronSessionFactory(job: CronJob, modelOverride?: CronModelRef): Promise<BridgeSession> {\n\tconst [\n\t\t{ loadAllBuiltinExtensions },\n\t\t{ getAgentDir },\n\t\t{ registerCustomizeBridge },\n\t\t{ registerMemoryCapBridge },\n\t\t{ registerModelTierBridge },\n\t\t{ createAgentSessionFromServices, createAgentSessionServices },\n\t\t{ SessionManager },\n\t\t{ SettingsManager },\n\t] = await Promise.all([\n\t\timport(\"../builtin-extensions/index.ts\"),\n\t\timport(\"../config.ts\"),\n\t\timport(\"../core/customize.ts\"),\n\t\timport(\"../core/memory-cap.ts\"),\n\t\timport(\"../core/model-tiers.ts\"),\n\t\timport(\"../core/agent-session-services.ts\"),\n\t\timport(\"../core/session-manager.ts\"),\n\t\timport(\"../core/settings-manager.ts\"),\n\t]);\n\tconst cwd = job.workdir ?? process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tregisterModelTierBridge(settingsManager);\n\tregisterMemoryCapBridge(settingsManager);\n\tregisterCustomizeBridge(settingsManager);\n\n\t// In-memory on purpose: cron runs must not persist session files or pollute\n\t// the per-chat gateway sessions — <agentDir>/cron/output/ is the audit trail.\n\tconst sessionManager = SessionManager.inMemory(cwd);\n\n\t// Services-first (mirrors main.ts): extension-registered providers (e.g.\n\t// ollama-cloud) must land in the shared ModelRuntime BEFORE session\n\t// creation — otherwise findInitialModel can't resolve the user's default\n\t// model and silently falls back to an arbitrary catalog provider.\n\tconst services = await createAgentSessionServices({\n\t\tcwd,\n\t\tagentDir,\n\t\tsettingsManager,\n\t\tresourceLoaderOptions: { extensionFactories: await loadAllBuiltinExtensions() },\n\t});\n\t// Fallback-model attempts pin the session model explicitly; an unresolvable\n\t// or unauthenticated override fails the attempt so the next fallback runs.\n\tlet model: Model<any> | undefined;\n\tif (modelOverride) {\n\t\tconst resolved = services.modelRuntime.getModel(modelOverride.provider, modelOverride.modelId);\n\t\tif (!resolved) throw new Error(`model not found: ${modelOverride.provider}/${modelOverride.modelId}`);\n\t\tif (!services.modelRuntime.hasConfiguredAuth(resolved.provider)) {\n\t\t\tthrow new Error(`no configured auth for provider: ${modelOverride.provider}`);\n\t\t}\n\t\tmodel = resolved;\n\t}\n\tconst { session } = await createAgentSessionFromServices({ services, sessionManager, model });\n\tawait session.bindExtensions({\n\t\tmode: \"print\",\n\t\tonError: (err) => console.error(`[gateway cron] extension error (${err.extensionPath}): ${err.error}`),\n\t});\n\treturn session;\n}\n\n/** Final assistant text, print-mode style; throws on error/aborted stop. */\nfunction extractFinalText(session: BridgeSession): string {\n\tconst messages = session.state.messages;\n\tconst last = messages[messages.length - 1];\n\tif (last?.role !== \"assistant\") return \"\";\n\tconst assistant = last as {\n\t\tstopReason?: string;\n\t\terrorMessage?: string;\n\t\tcontent?: Array<{ type: string; text?: string }>;\n\t};\n\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") {\n\t\tthrow new Error(assistant.errorMessage || `Request ${assistant.stopReason}`);\n\t}\n\tlet text = \"\";\n\tfor (const content of assistant.content ?? []) {\n\t\tif (content.type === \"text\") text += content.text ?? \"\";\n\t}\n\treturn text;\n}\n\ninterface ResolvedTarget {\n\tplatform: string;\n\tchatId: string;\n\tthreadId?: string;\n}\n\nconst EXPLICIT_TARGET_RE = /^([a-z0-9_-]+):([^:]+)(?::([^:]+))?$/i;\n\n/** Resolve one deliver target to a concrete destination. Returns an error string on failure. */\nfunction resolveTarget(target: string, job: CronJob, cfg: GatewayConfig): ResolvedTarget | string {\n\tif (target === \"origin\") {\n\t\tconst origin = job.origin;\n\t\tif (origin?.platform && origin.chatId) {\n\t\t\treturn { platform: origin.platform, chatId: origin.chatId, threadId: origin.threadId };\n\t\t}\n\t\t// Missing/incomplete origin → fall back to a homeChannel: the origin's\n\t\t// platform when known, otherwise the first platform that has one.\n\t\tconst platforms = origin?.platform ? [origin.platform] : KNOWN_PLATFORMS;\n\t\tfor (const platform of platforms) {\n\t\t\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\t\t\tif (home) return { platform, chatId: home };\n\t\t}\n\t\treturn `deliver target \"origin\": job has no origin and no homeChannel is configured`;\n\t}\n\n\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\tif (platformConfigFor(cfg, platform) === undefined) {\n\t\treturn `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t}\n\tif (explicit) {\n\t\treturn { platform, chatId: explicit[2], threadId: explicit[3] };\n\t}\n\t// Bare platform name → its homeChannel.\n\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\tif (!home) return `no homeChannel configured for ${platform}`;\n\treturn { platform, chatId: home };\n}\n\nfunction createDeliverValidator(\n\tcfg: GatewayConfig,\n): (deliver: string, origin?: CronJobOrigin | null) => string | undefined {\n\treturn (deliver, origin) => {\n\t\tconst targets = deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue;\n\t\t\tif (target === \"origin\") continue; // delivery time resolves/fails with a sensible error\n\t\t\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\t\t\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\t\t\tconst platformCfg = platformConfigFor(cfg, platform);\n\t\t\tif (!platformCfg) return `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t\t\tif (explicit) {\n\t\t\t\tconst chatId = explicit[2];\n\t\t\t\tconst allowed = new Set(\n\t\t\t\t\t[\n\t\t\t\t\t\tplatformCfg.homeChannel,\n\t\t\t\t\t\t...(platformCfg.allowedChats ?? []),\n\t\t\t\t\t\t...(origin?.platform === platform ? [origin.chatId] : []),\n\t\t\t\t\t].filter((x): x is string => !!x),\n\t\t\t\t);\n\t\t\t\tif (!allowed.has(chatId)) {\n\t\t\t\t\treturn `deliver target \"${target}\" is not an allowed chat for ${platform}`;\n\t\t\t\t}\n\t\t\t} else if (!platformCfg.homeChannel) {\n\t\t\t\treturn `no homeChannel configured for ${platform}`;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t};\n}\n\n/** Wrap the run output with a compact cron header. */\nexport function wrapCronContent(job: CronJob, content: string): string {\n\treturn `☾ Cron: ${job.name}\\n———\\n${content}`;\n}\n\n/**\n * The platform deliverer installed at the `@lunr/cron-delivery` bridge.\n * Every target is attempted; the first error is returned (null on success).\n */\nexport function createPlatformDeliverer(adapters: Map<string, PlatformAdapter>, cfg: GatewayConfig): DeliveryBridge {\n\treturn async (job, content) => {\n\t\tconst targets = job.deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tlet firstError: string | null = null;\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue; // output file already written by the scheduler\n\t\t\tconst resolved = resolveTarget(target, job, cfg);\n\t\t\tif (typeof resolved === \"string\") {\n\t\t\t\tfirstError ??= resolved;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst adapter = adapters.get(resolved.platform);\n\t\t\tif (!adapter) {\n\t\t\t\tfirstError ??= `no adapter connected for ${resolved.platform}`;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst wrapped = wrapCronContent(job, content);\n\t\t\tfor (const chunk of splitMessage(wrapped, adapter.maxMessageLength)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await adapter.send(resolved.chatId, chunk, {\n\t\t\t\t\t\tthreadId: resolved.threadId,\n\t\t\t\t\t});\n\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${result.error ?? \"unknown error\"}`;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${err instanceof Error ? err.message : String(err)}`;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn firstError;\n\t};\n}\n\n/** Race one prompt attempt against its share of the job budget. */\nfunction withAttemptTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\treturn Promise.race([\n\t\tpromise,\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tconst timer = setTimeout(() => reject(new Error(`attempt timed out after ${ms}ms`)), ms);\n\t\t\t// If the attempt settles first this promise is discarded; the timer must not keep the process alive.\n\t\t\ttimer.unref?.();\n\t\t}),\n\t]);\n}\n\n/**\n * Read settings.json `cronFallbackModels` (\"provider/modelId\" entries).\n * Fresh read per fire so edits apply without a daemon restart; invalid\n * entries are skipped with a log line. Never throws.\n */\nasync function readCronFallbackModels(): Promise<CronModelRef[]> {\n\ttry {\n\t\tconst [{ getAgentDir }, { SettingsManager }] = await Promise.all([\n\t\t\timport(\"../config.ts\"),\n\t\t\timport(\"../core/settings-manager.ts\"),\n\t\t]);\n\t\tconst settingsManager = SettingsManager.create(process.cwd(), getAgentDir(), { projectTrusted: false });\n\t\tconst out: CronModelRef[] = [];\n\t\tfor (const entry of settingsManager.getCronFallbackModels()) {\n\t\t\tconst idx = entry.indexOf(\"/\");\n\t\t\tif (idx <= 0 || idx === entry.length - 1) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[gateway cron] ignoring invalid cronFallbackModels entry \"${entry}\" (want \"provider/modelId\")`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tout.push({ provider: entry.slice(0, idx), modelId: entry.slice(idx + 1) });\n\t\t}\n\t\treturn out;\n\t} catch (err) {\n\t\tconsole.error(\n\t\t\t`[gateway cron] failed to read cronFallbackModels: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\n/**\n * Start the cron scheduler inside the gateway daemon. Starts even with zero\n * stored jobs — jobs can be created later from chats. stop() halts the loop.\n */\nexport function startGatewayCron(options: GatewayCronOptions): { stop(): void; intervalMs: number } {\n\tconst { adapters, cfg } = options;\n\tconst sessionFactory = options.sessionFactory ?? defaultCronSessionFactory;\n\tconst platformDeliverer = createPlatformDeliverer(adapters, cfg);\n\n\t// Explicit so runJob can split it into per-attempt budgets that sum to (at\n\t// most) the scheduler's outer race (core/cron/scheduler.ts runSchedulerTick).\n\tconst jobTimeoutMs = 5 * 60 * 1000;\n\n\t// Reject deliver targets that are not local, origin, a configured home channel,\n\t// or an explicit chat in the platform allowlist / the job's origin.\n\tsetCronDeliverValidator(createDeliverValidator(cfg));\n\n\t// Replace the local-notify bridge lunr-cron registered with the platform deliverer.\n\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\n\tconst runJob = async (prompt: string, job: CronJob): Promise<string> => {\n\t\tconst fallbacks = options.fallbackModels ?? (await readCronFallbackModels());\n\t\tconst candidates: Array<CronModelRef | undefined> = [undefined, ...fallbacks];\n\t\tconst attemptTimeoutMs = Math.floor(jobTimeoutMs / candidates.length);\n\t\tconst errors: string[] = [];\n\t\tbeginCronFire();\n\t\ttry {\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tconst label = candidate ? `${candidate.provider}/${candidate.modelId}` : \"default model\";\n\t\t\t\tlet session: BridgeSession | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tsession = await sessionFactory(job, candidate);\n\t\t\t\t\t// Loading the builtin extensions re-registered lunr-cron's local-notify\n\t\t\t\t\t// bridge; restore the platform deliverer before any delivery re-reads it.\n\t\t\t\t\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\t\t\t\t\tawait withAttemptTimeout(session.prompt(prompt, { source: \"extension\" }), attemptTimeoutMs);\n\t\t\t\t\tconst text = extractFinalText(session);\n\t\t\t\t\t// The marker lands in the output file (the audit trail); [SILENT] as\n\t\t\t\t\t// the last line still suppresses delivery (scheduler isSilent check).\n\t\t\t\t\treturn candidate ? `[fell back to ${label}]\\n${text}` : text;\n\t\t\t\t} catch (err) {\n\t\t\t\t\terrors.push(`${label}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t} finally {\n\t\t\t\t\tsession?.dispose?.();\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new Error(errors.join(\" | \") || \"no model candidates\");\n\t\t} finally {\n\t\t\tendCronFire();\n\t\t}\n\t};\n\n\tconst deliverResult = async (job: CronJob, content: string): Promise<void> => {\n\t\t// Re-read the bridge on every delivery (same pattern as lunr-cron).\n\t\tconst bridge = (globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] as DeliveryBridge | undefined;\n\t\tif (!bridge) throw new Error(\"no cron delivery bridge registered\");\n\t\tconst err = await bridge(job, content);\n\t\tif (err) throw new Error(err);\n\t};\n\n\tconst scheduler = startScheduler({ runJob, deliverResult, intervalMs: options.intervalMs, jobTimeoutMs });\n\treturn { stop: () => scheduler.stop(), intervalMs: options.intervalMs ?? 60_000 };\n}\n"]}
1
+ {"version":3,"file":"cron.js","sourceRoot":"","sources":["../../src/gateway/cron.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * lunR: gateway cron runner + delivery (Phase 4 of the cron/gateway roadmap).\n *\n * startGatewayCron() runs the core/cron scheduler inside the `lunr gateway`\n * daemon:\n *\n * - runJob: each fire gets a FRESH headless agent session (createAgentSession\n * with SessionManager.inMemory — cron runs never write session files and\n * never touch the per-chat gateway sessions; the files under\n * <agentDir>/cron/output/ are the audit trail). The shared fire-guard\n * (core/cron/fire-guard) brackets the turn so the `cron` tool refuses to\n * run inside it (jobs cannot schedule jobs).\n * - deliverResult: replaces the `@lunr/cron-delivery` bridge on globalThis\n * with a platform deliverer. Targets come from job.deliver\n * (comma-separated): \"local\" (no-op — the output file is already written),\n * \"origin\" (the chat that created the job, homeChannel fallback), a bare\n * platform name (that platform's homeChannel), or\n * \"<platform>:<chatId>[:<threadId>]\" (explicit). Content is wrapped as\n * \"☾ Cron: <name>\" and split to the adapter's maxMessageLength.\n *\n * The bridge is re-installed after every cron session creation because loading\n * the builtin extensions (lunr-cron) re-registers its local-notify bridge per\n * session; delivery re-reads the bridge on every call, matching lunr-cron.\n *\n * Fallback models (2026-08-02): settings.json `cronFallbackModels`\n * (\"provider/modelId\" entries) are tried IN ORDER when a fire fails — timeout\n * or any error. Each attempt gets a fresh headless session and\n * jobTimeoutMs/candidates of the budget; a successful fallback run prefixes\n * \"[fell back to provider/modelId]\" to the output. TUI fires are unaffected\n * (they use the live session's model).\n */\n\nimport type { Model } from \"@ashx-j/lunr-ai\";\nimport { beginCronFire, endCronFire } from \"../core/cron/fire-guard.ts\";\nimport type { CronJob, CronJobOrigin } from \"../core/cron/jobs.ts\";\nimport { setCronDeliverValidator } from \"../core/cron/jobs.ts\";\nimport { startScheduler } from \"../core/cron/scheduler.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { type GatewayConfig, platformConfigFor } from \"./config.ts\";\nimport { splitMessage } from \"./text.ts\";\nimport type { PlatformAdapter } from \"./types.ts\";\n\nconst DELIVERY_BRIDGE_SYMBOL = Symbol.for(\"@lunr/cron-delivery\");\n\ntype DeliveryBridge = (job: CronJob, content: string) => Promise<string | null>;\n\n/** Test seam / default: one fresh headless session per cron fire. */\nexport interface CronModelRef {\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport type CronSessionFactory = (job: CronJob, modelOverride?: CronModelRef) => Promise<BridgeSession>;\n\nexport interface GatewayCronOptions {\n\tadapters: Map<string, PlatformAdapter>;\n\tcfg: GatewayConfig;\n\t/** Tick interval; default 60s. */\n\tintervalMs?: number;\n\t/** Session factory for cron fires; defaults to a real headless agent session. */\n\tsessionFactory?: CronSessionFactory;\n\t/** Test seam: fallback models. When omitted, read fresh from settings.json `cronFallbackModels` on every fire. */\n\tfallbackModels?: CronModelRef[];\n}\n\nconst KNOWN_PLATFORMS = [\"telegram\", \"discord\"] as const;\n\n/** Default factory: fresh in-memory headless session, mirroring agent-bridge's wiring. */\nasync function defaultCronSessionFactory(job: CronJob, modelOverride?: CronModelRef): Promise<BridgeSession> {\n\tconst [\n\t\t{ loadAllBuiltinExtensions },\n\t\t{ getAgentDir },\n\t\t{ registerCustomizeBridge },\n\t\t{ reconcileBehaviorPreset, registerBehaviorPresetBridge },\n\t\t{ registerMemoryCapBridge },\n\t\t{ getModelTiersBridge, registerModelTierBridge },\n\t\t{ createAgentSessionFromServices, createAgentSessionServices },\n\t\t{ SessionManager },\n\t\t{ SettingsManager },\n\t] = await Promise.all([\n\t\timport(\"../builtin-extensions/index.ts\"),\n\t\timport(\"../config.ts\"),\n\t\timport(\"../core/customize.ts\"),\n\t\timport(\"../core/behavior-preset.ts\"),\n\t\timport(\"../core/memory-cap.ts\"),\n\t\timport(\"../core/model-tiers.ts\"),\n\t\timport(\"../core/agent-session-services.ts\"),\n\t\timport(\"../core/session-manager.ts\"),\n\t\timport(\"../core/settings-manager.ts\"),\n\t]);\n\tconst cwd = job.workdir ?? process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });\n\tregisterModelTierBridge(settingsManager);\n\tregisterMemoryCapBridge(settingsManager);\n\tregisterBehaviorPresetBridge(settingsManager);\n\treconcileBehaviorPreset(settingsManager, agentDir);\n\tregisterCustomizeBridge(settingsManager);\n\n\t// In-memory on purpose: cron runs must not persist session files or pollute\n\t// the per-chat gateway sessions — <agentDir>/cron/output/ is the audit trail.\n\tconst sessionManager = SessionManager.inMemory(cwd);\n\n\t// Services-first (mirrors main.ts): extension-registered providers (e.g.\n\t// ollama-cloud) must land in the shared ModelRuntime BEFORE session\n\t// creation — otherwise findInitialModel can't resolve the user's default\n\t// model and silently falls back to an arbitrary catalog provider.\n\tconst services = await createAgentSessionServices({\n\t\tcwd,\n\t\tagentDir,\n\t\tsettingsManager,\n\t\tresourceLoaderOptions: { extensionFactories: await loadAllBuiltinExtensions() },\n\t});\n\t// Fallback-model attempts pin the session model explicitly; an unresolvable\n\t// or unauthenticated override fails the attempt so the next fallback runs.\n\tlet model: Model<any> | undefined;\n\tif (modelOverride) {\n\t\tconst resolved = services.modelRuntime.getModel(modelOverride.provider, modelOverride.modelId);\n\t\tif (!resolved) throw new Error(`model not found: ${modelOverride.provider}/${modelOverride.modelId}`);\n\t\tif (!services.modelRuntime.hasConfiguredAuth(resolved.provider)) {\n\t\t\tthrow new Error(`no configured auth for provider: ${modelOverride.provider}`);\n\t\t}\n\t\tmodel = resolved;\n\t}\n\tconst { session } = await createAgentSessionFromServices({ services, sessionManager, model });\n\tgetModelTiersBridge()?.setParentThinkingProvider(() => session.thinkingLevel);\n\tawait session.bindExtensions({\n\t\tmode: \"print\",\n\t\tonError: (err) => console.error(`[gateway cron] extension error (${err.extensionPath}): ${err.error}`),\n\t});\n\treturn session;\n}\n\n/** Final assistant text, print-mode style; throws on error/aborted stop. */\nfunction extractFinalText(session: BridgeSession): string {\n\tconst messages = session.state.messages;\n\tconst last = messages[messages.length - 1];\n\tif (last?.role !== \"assistant\") return \"\";\n\tconst assistant = last as {\n\t\tstopReason?: string;\n\t\terrorMessage?: string;\n\t\tcontent?: Array<{ type: string; text?: string }>;\n\t};\n\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") {\n\t\tthrow new Error(assistant.errorMessage || `Request ${assistant.stopReason}`);\n\t}\n\tlet text = \"\";\n\tfor (const content of assistant.content ?? []) {\n\t\tif (content.type === \"text\") text += content.text ?? \"\";\n\t}\n\treturn text;\n}\n\ninterface ResolvedTarget {\n\tplatform: string;\n\tchatId: string;\n\tthreadId?: string;\n}\n\nconst EXPLICIT_TARGET_RE = /^([a-z0-9_-]+):([^:]+)(?::([^:]+))?$/i;\n\n/** Resolve one deliver target to a concrete destination. Returns an error string on failure. */\nfunction resolveTarget(target: string, job: CronJob, cfg: GatewayConfig): ResolvedTarget | string {\n\tif (target === \"origin\") {\n\t\tconst origin = job.origin;\n\t\tif (origin?.platform && origin.chatId) {\n\t\t\treturn { platform: origin.platform, chatId: origin.chatId, threadId: origin.threadId };\n\t\t}\n\t\t// Missing/incomplete origin → fall back to a homeChannel: the origin's\n\t\t// platform when known, otherwise the first platform that has one.\n\t\tconst platforms = origin?.platform ? [origin.platform] : KNOWN_PLATFORMS;\n\t\tfor (const platform of platforms) {\n\t\t\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\t\t\tif (home) return { platform, chatId: home };\n\t\t}\n\t\treturn `deliver target \"origin\": job has no origin and no homeChannel is configured`;\n\t}\n\n\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\tif (platformConfigFor(cfg, platform) === undefined) {\n\t\treturn `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t}\n\tif (explicit) {\n\t\treturn { platform, chatId: explicit[2], threadId: explicit[3] };\n\t}\n\t// Bare platform name → its homeChannel.\n\tconst home = platformConfigFor(cfg, platform)?.homeChannel;\n\tif (!home) return `no homeChannel configured for ${platform}`;\n\treturn { platform, chatId: home };\n}\n\nfunction createDeliverValidator(\n\tcfg: GatewayConfig,\n): (deliver: string, origin?: CronJobOrigin | null) => string | undefined {\n\treturn (deliver, origin) => {\n\t\tconst targets = deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue;\n\t\t\tif (target === \"origin\") continue; // delivery time resolves/fails with a sensible error\n\t\t\tconst explicit = EXPLICIT_TARGET_RE.exec(target);\n\t\t\tconst platform = explicit ? explicit[1].toLowerCase() : target.toLowerCase();\n\t\t\tconst platformCfg = platformConfigFor(cfg, platform);\n\t\t\tif (!platformCfg) return `unknown platform \"${explicit ? explicit[1] : target}\"`;\n\t\t\tif (explicit) {\n\t\t\t\tconst chatId = explicit[2];\n\t\t\t\tconst allowed = new Set(\n\t\t\t\t\t[\n\t\t\t\t\t\tplatformCfg.homeChannel,\n\t\t\t\t\t\t...(platformCfg.allowedChats ?? []),\n\t\t\t\t\t\t...(origin?.platform === platform ? [origin.chatId] : []),\n\t\t\t\t\t].filter((x): x is string => !!x),\n\t\t\t\t);\n\t\t\t\tif (!allowed.has(chatId)) {\n\t\t\t\t\treturn `deliver target \"${target}\" is not an allowed chat for ${platform}`;\n\t\t\t\t}\n\t\t\t} else if (!platformCfg.homeChannel) {\n\t\t\t\treturn `no homeChannel configured for ${platform}`;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t};\n}\n\n/** Wrap the run output with a compact cron header. */\nexport function wrapCronContent(job: CronJob, content: string): string {\n\treturn `☾ Cron: ${job.name}\\n———\\n${content}`;\n}\n\n/**\n * The platform deliverer installed at the `@lunr/cron-delivery` bridge.\n * Every target is attempted; the first error is returned (null on success).\n */\nexport function createPlatformDeliverer(adapters: Map<string, PlatformAdapter>, cfg: GatewayConfig): DeliveryBridge {\n\treturn async (job, content) => {\n\t\tconst targets = job.deliver\n\t\t\t.split(\",\")\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t\tlet firstError: string | null = null;\n\t\tfor (const target of targets) {\n\t\t\tif (target === \"local\") continue; // output file already written by the scheduler\n\t\t\tconst resolved = resolveTarget(target, job, cfg);\n\t\t\tif (typeof resolved === \"string\") {\n\t\t\t\tfirstError ??= resolved;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst adapter = adapters.get(resolved.platform);\n\t\t\tif (!adapter) {\n\t\t\t\tfirstError ??= `no adapter connected for ${resolved.platform}`;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst wrapped = wrapCronContent(job, content);\n\t\t\tfor (const chunk of splitMessage(wrapped, adapter.maxMessageLength)) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await adapter.send(resolved.chatId, chunk, {\n\t\t\t\t\t\tthreadId: resolved.threadId,\n\t\t\t\t\t});\n\t\t\t\t\tif (!result.success) {\n\t\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${result.error ?? \"unknown error\"}`;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tfirstError ??= `${resolved.platform} send failed: ${err instanceof Error ? err.message : String(err)}`;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn firstError;\n\t};\n}\n\n/** Race one prompt attempt against its share of the job budget. */\nfunction withAttemptTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n\treturn Promise.race([\n\t\tpromise,\n\t\tnew Promise<never>((_, reject) => {\n\t\t\tconst timer = setTimeout(() => reject(new Error(`attempt timed out after ${ms}ms`)), ms);\n\t\t\t// If the attempt settles first this promise is discarded; the timer must not keep the process alive.\n\t\t\ttimer.unref?.();\n\t\t}),\n\t]);\n}\n\n/**\n * Read settings.json `cronFallbackModels` (\"provider/modelId\" entries).\n * Fresh read per fire so edits apply without a daemon restart; invalid\n * entries are skipped with a log line. Never throws.\n */\nasync function readCronFallbackModels(): Promise<CronModelRef[]> {\n\ttry {\n\t\tconst [{ getAgentDir }, { SettingsManager }] = await Promise.all([\n\t\t\timport(\"../config.ts\"),\n\t\t\timport(\"../core/settings-manager.ts\"),\n\t\t]);\n\t\tconst settingsManager = SettingsManager.create(process.cwd(), getAgentDir(), { projectTrusted: false });\n\t\tconst out: CronModelRef[] = [];\n\t\tfor (const entry of settingsManager.getCronFallbackModels()) {\n\t\t\tconst idx = entry.indexOf(\"/\");\n\t\t\tif (idx <= 0 || idx === entry.length - 1) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[gateway cron] ignoring invalid cronFallbackModels entry \"${entry}\" (want \"provider/modelId\")`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tout.push({ provider: entry.slice(0, idx), modelId: entry.slice(idx + 1) });\n\t\t}\n\t\treturn out;\n\t} catch (err) {\n\t\tconsole.error(\n\t\t\t`[gateway cron] failed to read cronFallbackModels: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\n/**\n * Start the cron scheduler inside the gateway daemon. Starts even with zero\n * stored jobs — jobs can be created later from chats. stop() halts the loop.\n */\nexport function startGatewayCron(options: GatewayCronOptions): { stop(): void; intervalMs: number } {\n\tconst { adapters, cfg } = options;\n\tconst sessionFactory = options.sessionFactory ?? defaultCronSessionFactory;\n\tconst platformDeliverer = createPlatformDeliverer(adapters, cfg);\n\n\t// Explicit so runJob can split it into per-attempt budgets that sum to (at\n\t// most) the scheduler's outer race (core/cron/scheduler.ts runSchedulerTick).\n\tconst jobTimeoutMs = 5 * 60 * 1000;\n\n\t// Reject deliver targets that are not local, origin, a configured home channel,\n\t// or an explicit chat in the platform allowlist / the job's origin.\n\tsetCronDeliverValidator(createDeliverValidator(cfg));\n\n\t// Replace the local-notify bridge lunr-cron registered with the platform deliverer.\n\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\n\tconst runJob = async (prompt: string, job: CronJob): Promise<string> => {\n\t\tconst fallbacks = options.fallbackModels ?? (await readCronFallbackModels());\n\t\tconst candidates: Array<CronModelRef | undefined> = [undefined, ...fallbacks];\n\t\tconst attemptTimeoutMs = Math.floor(jobTimeoutMs / candidates.length);\n\t\tconst errors: string[] = [];\n\t\tbeginCronFire();\n\t\ttry {\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tconst label = candidate ? `${candidate.provider}/${candidate.modelId}` : \"default model\";\n\t\t\t\tlet session: BridgeSession | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tsession = await sessionFactory(job, candidate);\n\t\t\t\t\t// Loading the builtin extensions re-registered lunr-cron's local-notify\n\t\t\t\t\t// bridge; restore the platform deliverer before any delivery re-reads it.\n\t\t\t\t\t(globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] = platformDeliverer;\n\t\t\t\t\tawait withAttemptTimeout(session.prompt(prompt, { source: \"extension\" }), attemptTimeoutMs);\n\t\t\t\t\tconst text = extractFinalText(session);\n\t\t\t\t\t// The marker lands in the output file (the audit trail); [SILENT] as\n\t\t\t\t\t// the last line still suppresses delivery (scheduler isSilent check).\n\t\t\t\t\treturn candidate ? `[fell back to ${label}]\\n${text}` : text;\n\t\t\t\t} catch (err) {\n\t\t\t\t\terrors.push(`${label}: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t} finally {\n\t\t\t\t\tsession?.dispose?.();\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new Error(errors.join(\" | \") || \"no model candidates\");\n\t\t} finally {\n\t\t\tendCronFire();\n\t\t}\n\t};\n\n\tconst deliverResult = async (job: CronJob, content: string): Promise<void> => {\n\t\t// Re-read the bridge on every delivery (same pattern as lunr-cron).\n\t\tconst bridge = (globalThis as Record<symbol, unknown>)[DELIVERY_BRIDGE_SYMBOL] as DeliveryBridge | undefined;\n\t\tif (!bridge) throw new Error(\"no cron delivery bridge registered\");\n\t\tconst err = await bridge(job, content);\n\t\tif (err) throw new Error(err);\n\t};\n\n\tconst scheduler = startScheduler({ runJob, deliverResult, intervalMs: options.intervalMs, jobTimeoutMs });\n\treturn { stop: () => scheduler.stop(), intervalMs: options.intervalMs ?? 60_000 };\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,EAAE,MAAM,4BAA4B,CAAC;AAqClE;;;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;AA+YD,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;CACvC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBA4c/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 {\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 { exportFromFile } from \"./core/export-html/index.ts\";\nimport type { InlineExtension } 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 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 { handleGatewayCommand } from \"./gateway/command.ts\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.ts\";\nimport { InteractiveMode } from \"./modes/interactive/interactive-mode.ts\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.ts\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.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): Promise<boolean> {\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): 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?\");\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 selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\t(onProgress) => SessionManager.listAll(sessionDir, onProgress),\n\t\t\t\tsettingsManager,\n\t\t\t);\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\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\treturn showStartupSelector(settingsManager, formatMissingSessionCwdPrompt(issue), [\n\t\t{ label: \"Continue\", value: issue.fallbackCwd },\n\t\t{ label: \"Cancel\", value: undefined },\n\t]);\n}\n\nexport interface MainOptions {\n\textensionFactories?: InlineExtension[];\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 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\t// lunr: `lunr gateway` daemon/subcommands — same interception seam as the\n\t// package/config commands above (before parseArgs, which knows nothing\n\t// about gateway args).\n\tif (await handleGatewayCommand(args)) {\n\t\treturn;\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 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\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\tawait showFirstTimeSetup(startupSettingsManager);\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);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\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\t// lunr: session retention — delete session files older than sessionRetentionDays\n\t// (default 30; 0 = keep forever). Best-effort: per-file errors are swallowed,\n\t// deletions go to the debug log only, and the active session file is never deleted.\n\ttry {\n\t\tconst retentionDays = startupSettingsManager.getSessionRetentionDays();\n\t\tif (retentionDays > 0) {\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\t// Also cover a custom session dir (--session-dir / settings), which is a flat\n\t\t\t// .jsonl directory outside the default sessions root.\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((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tappendDebugLog(`session-retention: prune failed: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\ttime(\"pruneOldSessions\");\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\tlet builtinRoster =\n\t\tappMode === \"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\tcreateProjectTrustContext({\n\t\t\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\t\t\tmode: isInitialRuntime ? trustPromptMode : appMode,\n\t\t\t\t\t\t\t\t\t\tsettingsManager: startupSettingsManager,\n\t\t\t\t\t\t\t\t\t\thasUI: isInitialRuntime && trustPromptMode === \"interactive\",\n\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\ttime(\"createAgentSessionRuntime\");\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\tregisterMemoryCapBridge(settingsManager);\n\tregisterCustomizeBridge(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\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tif (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes(\"Failed to load extension\"))) {\n\t\t\tconsole.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));\n\t\t}\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 { loadDeferredBuiltinExtensions } = await import(\"./builtin-extensions/index.ts\");\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\tdeferredBuiltinFactories: parsed.noExtensions ? undefined : loadDeferredBuiltinExtensions,\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.waitForDeferredBuiltins();\n\t\t\ttime(\"attachDeferredBuiltins\");\n\t\t\t// Give the TUI's stdin handler a brief chance to consume terminal query replies\n\t\t\t// (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\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;AAiCH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAuClE;;;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;AA+YD,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;CACvC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBAsd/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 {\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 { reconcileBehaviorPreset, registerBehaviorPresetBridge } from \"./core/behavior-preset.ts\";\nimport { registerCustomizeBridge } from \"./core/customize.ts\";\nimport { exportFromFile } from \"./core/export-html/index.ts\";\nimport type { InlineExtension } 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 { registerUsageServiceBridge } from \"./core/usage-service.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 { handleGatewayCommand } from \"./gateway/command.ts\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.ts\";\nimport { InteractiveMode } from \"./modes/interactive/interactive-mode.ts\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.ts\";\nimport { handleUpdateCli } from \"./cli/update-cli.ts\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.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): Promise<boolean> {\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): 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?\");\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 selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\t(onProgress) => SessionManager.listAll(sessionDir, onProgress),\n\t\t\t\tsettingsManager,\n\t\t\t);\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\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\treturn showStartupSelector(settingsManager, formatMissingSessionCwdPrompt(issue), [\n\t\t{ label: \"Continue\", value: issue.fallbackCwd },\n\t\t{ label: \"Cancel\", value: undefined },\n\t]);\n}\n\nexport interface MainOptions {\n\textensionFactories?: InlineExtension[];\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\t// lunr: `lunr gateway` daemon/subcommands — same interception seam as the\n\t// package/config commands above (before parseArgs, which knows nothing\n\t// about gateway args).\n\tif (await handleGatewayCommand(args)) {\n\t\treturn;\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 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\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\tregisterBehaviorPresetBridge(startupSettingsManager);\n\treconcileBehaviorPreset(startupSettingsManager, agentDir);\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\tawait showFirstTimeSetup(startupSettingsManager);\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);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\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\t// lunr: session retention — delete session files older than sessionRetentionDays\n\t// (default 30; 0 = keep forever). Best-effort: per-file errors are swallowed,\n\t// deletions go to the debug log only, and the active session file is never deleted.\n\ttry {\n\t\tconst retentionDays = startupSettingsManager.getSessionRetentionDays();\n\t\tif (retentionDays > 0) {\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\t// Also cover a custom session dir (--session-dir / settings), which is a flat\n\t\t\t// .jsonl directory outside the default sessions root.\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((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tappendDebugLog(`session-retention: prune failed: ${error instanceof Error ? error.message : String(error)}`);\n\t}\n\ttime(\"pruneOldSessions\");\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\tlet builtinRoster =\n\t\tappMode === \"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\tcreateProjectTrustContext({\n\t\t\t\t\t\t\t\t\t\tcwd,\n\t\t\t\t\t\t\t\t\t\tmode: isInitialRuntime ? trustPromptMode : appMode,\n\t\t\t\t\t\t\t\t\t\tsettingsManager: startupSettingsManager,\n\t\t\t\t\t\t\t\t\t\thasUI: isInitialRuntime && trustPromptMode === \"interactive\",\n\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\ttime(\"createAgentSessionRuntime\");\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\tregisterBehaviorPresetBridge(settingsManager);\n\treconcileBehaviorPreset(settingsManager, agentDir);\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\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tif (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes(\"Failed to load extension\"))) {\n\t\t\tconsole.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));\n\t\t}\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 { loadDeferredBuiltinExtensions } = await import(\"./builtin-extensions/index.ts\");\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\tdeferredBuiltinFactories: parsed.noExtensions ? undefined : loadDeferredBuiltinExtensions,\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.waitForDeferredBuiltins();\n\t\t\ttime(\"attachDeferredBuiltins\");\n\t\t\t// Give the TUI's stdin handler a brief chance to consume terminal query replies\n\t\t\t// (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\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
@@ -20,12 +20,14 @@ import { APP_NAME, appendDebugLog, ENV_SESSION_DIR, expandTildePath, getAgentDir
20
20
  import { createAgentSessionRuntime } from "./core/agent-session-runtime.js";
21
21
  import { createAgentSessionFromServices, createAgentSessionServices, } from "./core/agent-session-services.js";
22
22
  import { formatNoModelsAvailableMessage } from "./core/auth-guidance.js";
23
+ import { reconcileBehaviorPreset, registerBehaviorPresetBridge } from "./core/behavior-preset.js";
23
24
  import { registerCustomizeBridge } from "./core/customize.js";
24
25
  import { exportFromFile } from "./core/export-html/index.js";
25
26
  import { applyHttpProxySettings, configureHttpDispatcher } from "./core/http-dispatcher.js";
26
27
  import { registerMemoryCapBridge } from "./core/memory-cap.js";
27
28
  import { resolveCliModel, resolveModelScope } from "./core/model-resolver.js";
28
- import { registerModelTierBridge } from "./core/model-tiers.js";
29
+ import { getModelTiersBridge, registerModelTierBridge } from "./core/model-tiers.js";
30
+ import { registerUsageServiceBridge } from "./core/usage-service.js";
29
31
  import { restoreStdout, takeOverStdout } from "./core/output-guard.js";
30
32
  import { resolveProjectTrusted } from "./core/project-trust.js";
31
33
  import { formatMissingSessionCwdPrompt, getMissingSessionCwdIssue, MissingSessionCwdError, } from "./core/session-cwd.js";
@@ -39,6 +41,7 @@ import { handleGatewayCommand } from "./gateway/command.js";
39
41
  import { runMigrations, showDeprecationWarnings } from "./migrations.js";
40
42
  import { InteractiveMode } from "./modes/interactive/interactive-mode.js";
41
43
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
44
+ import { handleUpdateCli } from "./cli/update-cli.js";
42
45
  import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
43
46
  import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.js";
44
47
  const EXTENSION_LOAD_FAILURE_HINT = `Hint: Start without extensions using "${APP_NAME} -ne".`;
@@ -416,6 +419,9 @@ export async function main(args, options) {
416
419
  if (await handleInstallCli(args)) {
417
420
  return;
418
421
  }
422
+ if (await handleUpdateCli(args)) {
423
+ return;
424
+ }
419
425
  if (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) {
420
426
  const exitCode = process.exitCode ?? 0;
421
427
  process.exit(exitCode);
@@ -485,6 +491,8 @@ export async function main(args, options) {
485
491
  // lunr: same for the simple-pi-memory character cap — extensions read it via the
486
492
  // bridge at load/runtime; re-pointed to the runtime settings manager below.
487
493
  registerMemoryCapBridge(startupSettingsManager);
494
+ registerBehaviorPresetBridge(startupSettingsManager);
495
+ reconcileBehaviorPreset(startupSettingsManager, agentDir);
488
496
  // lunr: TUI customize settings (spinner, dividers, rail, prompt symbol) read by
489
497
  // ashxj-spinners and ashxj-tui via the bridge; re-pointed to runtime below.
490
498
  registerCustomizeBridge(startupSettingsManager);
@@ -698,8 +706,12 @@ export async function main(args, options) {
698
706
  // Point the model-tier bridge at the live runtime settings manager so /settings
699
707
  // changes take effect without a restart.
700
708
  registerModelTierBridge(settingsManager);
709
+ getModelTiersBridge()?.setParentThinkingProvider(() => session.thinkingLevel);
701
710
  registerMemoryCapBridge(settingsManager);
711
+ registerBehaviorPresetBridge(settingsManager);
712
+ reconcileBehaviorPreset(settingsManager, agentDir);
702
713
  registerCustomizeBridge(settingsManager);
714
+ registerUsageServiceBridge(modelRuntime, settingsManager);
703
715
  if (parsed.help) {
704
716
  const extensionFlags = resourceLoader
705
717
  .getExtensions()