@prestyj/cli 5.9.0 → 5.10.0

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 (321) hide show
  1. package/README.md +1 -1
  2. package/assets/skills/evidence-led-ui/SKILL.md +1 -1
  3. package/dist/app-sidecar.js +374 -14
  4. package/dist/app-sidecar.js.map +1 -1
  5. package/dist/chat-agents/general.test.js +5 -3
  6. package/dist/chat-agents/general.test.js.map +1 -1
  7. package/dist/chat-agents/memory.d.ts.map +1 -1
  8. package/dist/chat-agents/memory.js +14 -5
  9. package/dist/chat-agents/memory.js.map +1 -1
  10. package/dist/chat-agents/memory.test.js +11 -1
  11. package/dist/chat-agents/memory.test.js.map +1 -1
  12. package/dist/chat-agents/shared.js +1 -1
  13. package/dist/chat-agents/shared.js.map +1 -1
  14. package/dist/chat-agents/specialists.test.js +3 -2
  15. package/dist/chat-agents/specialists.test.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +16 -4
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config.d.ts +14 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +41 -2
  22. package/dist/config.js.map +1 -1
  23. package/dist/config.test.js +61 -1
  24. package/dist/config.test.js.map +1 -1
  25. package/dist/core/agent-mcp-passthrough.test.d.ts +2 -0
  26. package/dist/core/agent-mcp-passthrough.test.d.ts.map +1 -0
  27. package/dist/core/agent-mcp-passthrough.test.js +61 -0
  28. package/dist/core/agent-mcp-passthrough.test.js.map +1 -0
  29. package/dist/core/agent-session-add-dir.test.d.ts +2 -0
  30. package/dist/core/agent-session-add-dir.test.d.ts.map +1 -0
  31. package/dist/core/agent-session-add-dir.test.js +110 -0
  32. package/dist/core/agent-session-add-dir.test.js.map +1 -0
  33. package/dist/core/agent-session-compaction.test.js +4 -7
  34. package/dist/core/agent-session-compaction.test.js.map +1 -1
  35. package/dist/core/agent-session-marker-anchors.test.d.ts +2 -0
  36. package/dist/core/agent-session-marker-anchors.test.d.ts.map +1 -0
  37. package/dist/core/agent-session-marker-anchors.test.js +147 -0
  38. package/dist/core/agent-session-marker-anchors.test.js.map +1 -0
  39. package/dist/core/agent-session-memory-tail.test.js +4 -7
  40. package/dist/core/agent-session-memory-tail.test.js.map +1 -1
  41. package/dist/core/agent-session-queue.test.js +4 -7
  42. package/dist/core/agent-session-queue.test.js.map +1 -1
  43. package/dist/core/agent-session-subagent-policy.test.js +7 -3
  44. package/dist/core/agent-session-subagent-policy.test.js.map +1 -1
  45. package/dist/core/agent-session.d.ts +42 -0
  46. package/dist/core/agent-session.d.ts.map +1 -1
  47. package/dist/core/agent-session.js +93 -7
  48. package/dist/core/agent-session.js.map +1 -1
  49. package/dist/core/agent-tools-allowlist.test.js +5 -3
  50. package/dist/core/agent-tools-allowlist.test.js.map +1 -1
  51. package/dist/core/agents.d.ts +11 -0
  52. package/dist/core/agents.d.ts.map +1 -1
  53. package/dist/core/agents.js +21 -0
  54. package/dist/core/agents.js.map +1 -1
  55. package/dist/core/agents.test.d.ts +2 -0
  56. package/dist/core/agents.test.d.ts.map +1 -0
  57. package/dist/core/agents.test.js +67 -0
  58. package/dist/core/agents.test.js.map +1 -0
  59. package/dist/core/auth-providers.js +1 -1
  60. package/dist/core/auth-providers.js.map +1 -1
  61. package/dist/core/compaction/compactor.test.js +1 -1
  62. package/dist/core/compaction/compactor.test.js.map +1 -1
  63. package/dist/core/compaction/token-estimator.test.js +1 -1
  64. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  65. package/dist/core/encode-cwd.d.ts +17 -0
  66. package/dist/core/encode-cwd.d.ts.map +1 -1
  67. package/dist/core/encode-cwd.js +20 -7
  68. package/dist/core/encode-cwd.js.map +1 -1
  69. package/dist/core/encode-cwd.test.js +20 -1
  70. package/dist/core/encode-cwd.test.js.map +1 -1
  71. package/dist/core/ideal-review.d.ts.map +1 -1
  72. package/dist/core/ideal-review.js +6 -1
  73. package/dist/core/ideal-review.js.map +1 -1
  74. package/dist/core/ideal-review.test.js +7 -2
  75. package/dist/core/ideal-review.test.js.map +1 -1
  76. package/dist/core/local-endpoint-store.d.ts +50 -0
  77. package/dist/core/local-endpoint-store.d.ts.map +1 -0
  78. package/dist/core/local-endpoint-store.js +201 -0
  79. package/dist/core/local-endpoint-store.js.map +1 -0
  80. package/dist/core/local-endpoint-store.test.d.ts +2 -0
  81. package/dist/core/local-endpoint-store.test.d.ts.map +1 -0
  82. package/dist/core/local-endpoint-store.test.js +131 -0
  83. package/dist/core/local-endpoint-store.test.js.map +1 -0
  84. package/dist/core/lsp/client.d.ts +50 -0
  85. package/dist/core/lsp/client.d.ts.map +1 -1
  86. package/dist/core/lsp/client.js +158 -13
  87. package/dist/core/lsp/client.js.map +1 -1
  88. package/dist/core/lsp/jsonrpc.d.ts +38 -1
  89. package/dist/core/lsp/jsonrpc.d.ts.map +1 -1
  90. package/dist/core/lsp/jsonrpc.js +13 -1
  91. package/dist/core/lsp/jsonrpc.js.map +1 -1
  92. package/dist/core/lsp/manager.d.ts.map +1 -1
  93. package/dist/core/lsp/manager.js +23 -2
  94. package/dist/core/lsp/manager.js.map +1 -1
  95. package/dist/core/lsp/manager.test.js +32 -1
  96. package/dist/core/lsp/manager.test.js.map +1 -1
  97. package/dist/core/lsp/test-support.d.ts +18 -0
  98. package/dist/core/lsp/test-support.d.ts.map +1 -0
  99. package/dist/core/lsp/test-support.js +27 -0
  100. package/dist/core/lsp/test-support.js.map +1 -0
  101. package/dist/core/lsp/uri.test.d.ts +2 -0
  102. package/dist/core/lsp/uri.test.d.ts.map +1 -0
  103. package/dist/core/lsp/uri.test.js +56 -0
  104. package/dist/core/lsp/uri.test.js.map +1 -0
  105. package/dist/core/lsp/windows.test.d.ts +2 -0
  106. package/dist/core/lsp/windows.test.d.ts.map +1 -0
  107. package/dist/core/lsp/windows.test.js +251 -0
  108. package/dist/core/lsp/windows.test.js.map +1 -0
  109. package/dist/core/mcp/resolve-stdio.d.ts +35 -0
  110. package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
  111. package/dist/core/mcp/resolve-stdio.js +80 -1
  112. package/dist/core/mcp/resolve-stdio.js.map +1 -1
  113. package/dist/core/mcp/resolve-stdio.test.js +99 -4
  114. package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
  115. package/dist/core/network-guard.d.ts +47 -0
  116. package/dist/core/network-guard.d.ts.map +1 -0
  117. package/dist/core/network-guard.js +208 -0
  118. package/dist/core/network-guard.js.map +1 -0
  119. package/dist/core/network-guard.test.d.ts +2 -0
  120. package/dist/core/network-guard.test.d.ts.map +1 -0
  121. package/dist/core/network-guard.test.js +72 -0
  122. package/dist/core/network-guard.test.js.map +1 -0
  123. package/dist/core/persistent-shell.d.ts +3 -1
  124. package/dist/core/persistent-shell.d.ts.map +1 -1
  125. package/dist/core/persistent-shell.js +13 -3
  126. package/dist/core/persistent-shell.js.map +1 -1
  127. package/dist/core/process-manager-dev-server-repro.test.js +34 -12
  128. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  129. package/dist/core/process-manager.d.ts +1 -1
  130. package/dist/core/process-manager.d.ts.map +1 -1
  131. package/dist/core/process-manager.js +23 -13
  132. package/dist/core/process-manager.js.map +1 -1
  133. package/dist/core/project-discovery.d.ts +7 -0
  134. package/dist/core/project-discovery.d.ts.map +1 -1
  135. package/dist/core/project-discovery.js +40 -9
  136. package/dist/core/project-discovery.js.map +1 -1
  137. package/dist/core/project-discovery.test.js +102 -1
  138. package/dist/core/project-discovery.test.js.map +1 -1
  139. package/dist/core/prompt-commands.d.ts.map +1 -1
  140. package/dist/core/prompt-commands.js +89 -156
  141. package/dist/core/prompt-commands.js.map +1 -1
  142. package/dist/core/prompt-commands.test.js +4 -10
  143. package/dist/core/prompt-commands.test.js.map +1 -1
  144. package/dist/core/radio.test.js +4 -1
  145. package/dist/core/radio.test.js.map +1 -1
  146. package/dist/core/runtime-mode.d.ts +2 -0
  147. package/dist/core/runtime-mode.d.ts.map +1 -1
  148. package/dist/core/runtime-mode.js +4 -0
  149. package/dist/core/runtime-mode.js.map +1 -1
  150. package/dist/core/session-export.d.ts +98 -0
  151. package/dist/core/session-export.d.ts.map +1 -0
  152. package/dist/core/session-export.js +356 -0
  153. package/dist/core/session-export.js.map +1 -0
  154. package/dist/core/session-export.test.d.ts +2 -0
  155. package/dist/core/session-export.test.d.ts.map +1 -0
  156. package/dist/core/session-export.test.js +353 -0
  157. package/dist/core/session-export.test.js.map +1 -0
  158. package/dist/core/session-storage.d.ts.map +1 -1
  159. package/dist/core/session-storage.js +25 -1
  160. package/dist/core/session-storage.js.map +1 -1
  161. package/dist/core/session-storage.test.js +4 -1
  162. package/dist/core/session-storage.test.js.map +1 -1
  163. package/dist/core/settings-manager.d.ts +5 -0
  164. package/dist/core/settings-manager.d.ts.map +1 -1
  165. package/dist/core/settings-manager.js +10 -0
  166. package/dist/core/settings-manager.js.map +1 -1
  167. package/dist/core/setup-history.d.ts +0 -6
  168. package/dist/core/setup-history.d.ts.map +1 -1
  169. package/dist/core/setup-history.js +2 -18
  170. package/dist/core/setup-history.js.map +1 -1
  171. package/dist/core/setup-history.test.js +11 -26
  172. package/dist/core/setup-history.test.js.map +1 -1
  173. package/dist/core/skills-routing.test.js +3 -1
  174. package/dist/core/skills-routing.test.js.map +1 -1
  175. package/dist/core/slash-commands.d.ts +18 -0
  176. package/dist/core/slash-commands.d.ts.map +1 -1
  177. package/dist/core/slash-commands.js +42 -10
  178. package/dist/core/slash-commands.js.map +1 -1
  179. package/dist/core/slash-commands.test.d.ts +2 -0
  180. package/dist/core/slash-commands.test.d.ts.map +1 -0
  181. package/dist/core/slash-commands.test.js +59 -0
  182. package/dist/core/slash-commands.test.js.map +1 -0
  183. package/dist/core/speed-benchmark.test.js +9 -3
  184. package/dist/core/speed-benchmark.test.js.map +1 -1
  185. package/dist/core/subagent-manager.d.ts +1 -1
  186. package/dist/core/subagent-manager.d.ts.map +1 -1
  187. package/dist/core/subagent-manager.js +9 -0
  188. package/dist/core/subagent-manager.js.map +1 -1
  189. package/dist/core/subagent-store.test.js +14 -2
  190. package/dist/core/subagent-store.test.js.map +1 -1
  191. package/dist/core/workspace-guard.d.ts +2 -0
  192. package/dist/core/workspace-guard.d.ts.map +1 -1
  193. package/dist/core/workspace-guard.js +4 -1
  194. package/dist/core/workspace-guard.js.map +1 -1
  195. package/dist/core/workspace-guard.test.js +21 -0
  196. package/dist/core/workspace-guard.test.js.map +1 -1
  197. package/dist/modes/json-mode.d.ts +7 -0
  198. package/dist/modes/json-mode.d.ts.map +1 -1
  199. package/dist/modes/json-mode.js +1 -0
  200. package/dist/modes/json-mode.js.map +1 -1
  201. package/dist/modes/subagent-worker-mode.d.ts +2 -0
  202. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  203. package/dist/modes/subagent-worker-mode.js.map +1 -1
  204. package/dist/system-prompt.d.ts +11 -1
  205. package/dist/system-prompt.d.ts.map +1 -1
  206. package/dist/system-prompt.js +39 -18
  207. package/dist/system-prompt.js.map +1 -1
  208. package/dist/system-prompt.test.js +33 -17
  209. package/dist/system-prompt.test.js.map +1 -1
  210. package/dist/test-support/fake-home.d.ts +20 -0
  211. package/dist/test-support/fake-home.d.ts.map +1 -0
  212. package/dist/test-support/fake-home.js +37 -0
  213. package/dist/test-support/fake-home.js.map +1 -0
  214. package/dist/tools/bash.d.ts +2 -1
  215. package/dist/tools/bash.d.ts.map +1 -1
  216. package/dist/tools/bash.js +22 -5
  217. package/dist/tools/bash.js.map +1 -1
  218. package/dist/tools/bash.test.js +122 -7
  219. package/dist/tools/bash.test.js.map +1 -1
  220. package/dist/tools/edit-robustness.test.js +3 -1
  221. package/dist/tools/edit-robustness.test.js.map +1 -1
  222. package/dist/tools/find.d.ts.map +1 -1
  223. package/dist/tools/find.js +4 -1
  224. package/dist/tools/find.js.map +1 -1
  225. package/dist/tools/grep.d.ts +3 -1
  226. package/dist/tools/grep.d.ts.map +1 -1
  227. package/dist/tools/grep.js +135 -11
  228. package/dist/tools/grep.js.map +1 -1
  229. package/dist/tools/grep.test.js +53 -0
  230. package/dist/tools/grep.test.js.map +1 -1
  231. package/dist/tools/index.d.ts +6 -0
  232. package/dist/tools/index.d.ts.map +1 -1
  233. package/dist/tools/index.js +3 -3
  234. package/dist/tools/index.js.map +1 -1
  235. package/dist/tools/overflow.test.js +4 -7
  236. package/dist/tools/overflow.test.js.map +1 -1
  237. package/dist/tools/path-utils.d.ts +15 -0
  238. package/dist/tools/path-utils.d.ts.map +1 -1
  239. package/dist/tools/path-utils.js +17 -0
  240. package/dist/tools/path-utils.js.map +1 -1
  241. package/dist/tools/path-utils.test.js +7 -2
  242. package/dist/tools/path-utils.test.js.map +1 -1
  243. package/dist/tools/prompt-hints.js +1 -1
  244. package/dist/tools/prompt-hints.js.map +1 -1
  245. package/dist/tools/search-code.d.ts.map +1 -1
  246. package/dist/tools/search-code.js +6 -4
  247. package/dist/tools/search-code.js.map +1 -1
  248. package/dist/tools/subagent.d.ts +1 -1
  249. package/dist/tools/subagent.d.ts.map +1 -1
  250. package/dist/tools/subagent.js +8 -0
  251. package/dist/tools/subagent.js.map +1 -1
  252. package/dist/tools/web-fetch.d.ts +2 -1
  253. package/dist/tools/web-fetch.d.ts.map +1 -1
  254. package/dist/tools/web-fetch.js +17 -6
  255. package/dist/tools/web-fetch.js.map +1 -1
  256. package/dist/tools/web-fetch.test.js +34 -0
  257. package/dist/tools/web-fetch.test.js.map +1 -1
  258. package/dist/tools/web-search.d.ts +2 -1
  259. package/dist/tools/web-search.d.ts.map +1 -1
  260. package/dist/tools/web-search.js +21 -5
  261. package/dist/tools/web-search.js.map +1 -1
  262. package/dist/ui/App.d.ts.map +1 -1
  263. package/dist/ui/App.js +5 -80
  264. package/dist/ui/App.js.map +1 -1
  265. package/dist/ui/app-items.d.ts +1 -12
  266. package/dist/ui/app-items.d.ts.map +1 -1
  267. package/dist/ui/app-items.js.map +1 -1
  268. package/dist/ui/components/Footer.d.ts +1 -1
  269. package/dist/ui/components/Footer.js +2 -2
  270. package/dist/ui/components/Footer.js.map +1 -1
  271. package/dist/ui/hooks/usePixelFixFlow.d.ts +1 -2
  272. package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -1
  273. package/dist/ui/hooks/usePixelFixFlow.js +2 -5
  274. package/dist/ui/hooks/usePixelFixFlow.js.map +1 -1
  275. package/dist/ui/login.js +1 -1
  276. package/dist/ui/login.js.map +1 -1
  277. package/dist/ui/terminal-history-status-renderers.d.ts +1 -2
  278. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
  279. package/dist/ui/terminal-history-status-renderers.js +2 -15
  280. package/dist/ui/terminal-history-status-renderers.js.map +1 -1
  281. package/dist/ui/terminal-history.d.ts.map +1 -1
  282. package/dist/ui/terminal-history.js +2 -4
  283. package/dist/ui/terminal-history.js.map +1 -1
  284. package/dist/ui/thinking-level-cycle.test.js +8 -8
  285. package/dist/ui/thinking-level-cycle.test.js.map +1 -1
  286. package/dist/ui/transcript/MiscRows.d.ts +1 -4
  287. package/dist/ui/transcript/MiscRows.d.ts.map +1 -1
  288. package/dist/ui/transcript/MiscRows.js +2 -7
  289. package/dist/ui/transcript/MiscRows.js.map +1 -1
  290. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
  291. package/dist/ui/transcript/TranscriptRenderer.js +1 -3
  292. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
  293. package/dist/ui/transcript/presentation.d.ts +1 -9
  294. package/dist/ui/transcript/presentation.d.ts.map +1 -1
  295. package/dist/ui/transcript/presentation.js +0 -9
  296. package/dist/ui/transcript/presentation.js.map +1 -1
  297. package/dist/ui/transcript/spacing.d.ts +2 -2
  298. package/dist/ui/transcript/spacing.d.ts.map +1 -1
  299. package/dist/ui/transcript/spacing.js +0 -1
  300. package/dist/ui/transcript/spacing.js.map +1 -1
  301. package/dist/ui/transcript/spacing.test.js +1 -3
  302. package/dist/ui/transcript/spacing.test.js.map +1 -1
  303. package/dist/ui/tui-history-parity.test.js +2 -9
  304. package/dist/ui/tui-history-parity.test.js.map +1 -1
  305. package/dist/utils/github.d.ts +20 -0
  306. package/dist/utils/github.d.ts.map +1 -0
  307. package/dist/utils/github.js +65 -0
  308. package/dist/utils/github.js.map +1 -0
  309. package/dist/utils/github.test.d.ts +2 -0
  310. package/dist/utils/github.test.d.ts.map +1 -0
  311. package/dist/utils/github.test.js +23 -0
  312. package/dist/utils/github.test.js.map +1 -0
  313. package/dist/utils/process.d.ts +26 -3
  314. package/dist/utils/process.d.ts.map +1 -1
  315. package/dist/utils/process.js +68 -10
  316. package/dist/utils/process.js.map +1 -1
  317. package/dist/utils/process.test.d.ts +2 -0
  318. package/dist/utils/process.test.d.ts.map +1 -0
  319. package/dist/utils/process.test.js +42 -0
  320. package/dist/utils/process.test.js.map +1 -0
  321. package/package.json +5 -5
package/README.md CHANGED
@@ -69,7 +69,7 @@ Switch mid-conversation with `/model`. Not locked to anyone.
69
69
 
70
70
  | Provider | Models | Auth |
71
71
  |---|---|---|
72
- | **Anthropic** | Claude Opus 4.8, Sonnet 5, Haiku 4.5 | OAuth |
72
+ | **Anthropic** | Claude Opus 5, Sonnet 5, Haiku 4.5 | OAuth |
73
73
  | **OpenAI** | GPT-5.5, GPT-5.5 Pro, GPT-5.4, GPT-5.3 Codex | OAuth |
74
74
  | **Moonshot** | Kimi K3, Kimi K2.7 Code | OAuth or API key |
75
75
  | **Z.AI (GLM)** | GLM-5.1, GLM-4.7, GLM-4.7 Flash | API key |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: evidence-led-ui
3
- description: Use for web/mobile UI changes or evaluation: pages, components, dashboards, design systems, responsiveness, accessibility, or visual polish, even without explicit design request. Exclude database/API schemas, CLI output, copy-only work, standalone image/SVG generation, and description-only tasks.
3
+ description: Use for broad or design-sensitive web/mobile UI work: net-new pages/screens, redesigns, design-system work, visual polish, responsive or accessibility overhauls, and UI quality review/critique. Do NOT use for small UI edits that follow existing patterns (bug fixes, minor styling tweaks, wiring, copy changes, small component additions matching neighbors); just match the surrounding code for those. Also exclude database/API schemas, CLI output, standalone image/SVG generation, and description-only tasks.
4
4
  license: See LICENSES.md
5
5
  compatibility: Full review requires filesystem inspection and rendered screenshots; web research and browser/device tooling are optional and unavailable checks must be reported honestly.
6
6
  ---
@@ -21,6 +21,7 @@ import { parseArgs } from "node:util";
21
21
  import { environmentSecrets, formatError, redactValue } from "@prestyj/ai";
22
22
  import { runJsonMode } from "./modes/json-mode.js";
23
23
  import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
24
+ import { setStreamDiagnostic } from "@prestyj/agent";
24
25
  import { AgentSession } from "./core/agent-session.js";
25
26
  import { RunLifecycle } from "./core/run-lifecycle.js";
26
27
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
@@ -33,10 +34,12 @@ import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycl
33
34
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
34
35
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
35
36
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
37
+ import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
36
38
  import { AuthStorage } from "./core/auth-storage.js";
37
39
  import { cleanupToolOutputs } from "./tools/overflow.js";
38
40
  import { readCappedBody } from "./utils/http-body.js";
39
- import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
41
+ import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
42
+ import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
40
43
  import { loginAnthropic } from "./core/oauth/anthropic.js";
41
44
  import { loginOpenAI } from "./core/oauth/openai.js";
42
45
  import { loginGemini } from "./core/oauth/gemini.js";
@@ -44,9 +47,10 @@ import { loginKimi } from "./core/oauth/kimi.js";
44
47
  import { AUTH_PROVIDERS } from "./core/auth-providers.js";
45
48
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
46
49
  import { SettingsManager } from "./core/settings-manager.js";
47
- import { getModel, getDefaultThinkingLevel, getContextWindow, MODELS, } from "./core/model-registry.js";
50
+ import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
48
51
  import { resolveStartOrFallback } from "./core/resolve-start.js";
49
52
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
53
+ import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
50
54
  import { extractPlanSteps } from "./utils/plan-steps.js";
51
55
  import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
52
56
  import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
@@ -344,6 +348,11 @@ async function searchProjectFiles(cwd, rawQuery) {
344
348
  * hook prompt, by its distinctive opening phrase. Returns the hook kind so the
345
349
  * webview can render the short notice line instead of the full prompt body.
346
350
  */
351
+ /** `?tools=` on /export. Anything unrecognised (including absent) falls back to
352
+ * the human-facing `summary` default rather than dumping every payload. */
353
+ function parseToolDetail(value) {
354
+ return value === "none" || value === "full" ? value : "summary";
355
+ }
347
356
  function detectHookKind(text) {
348
357
  const t = text.trimStart();
349
358
  if (t.startsWith("Ideal? Review the actual work"))
@@ -451,6 +460,7 @@ async function runJsonModeIfRequested() {
451
460
  "max-turns": { type: "string" },
452
461
  "system-prompt": { type: "string" },
453
462
  tools: { type: "string" },
463
+ "mcp-servers": { type: "string" },
454
464
  "prompt-cache-key": { type: "string" },
455
465
  },
456
466
  allowPositionals: true,
@@ -467,14 +477,24 @@ async function runJsonModeIfRequested() {
467
477
  .filter(Boolean)
468
478
  : [];
469
479
  const allowedTools = parsedTools.length > 0 ? parsedTools : undefined;
480
+ // MCP servers the agent definition asked for. Without forwarding these, an
481
+ // allow-listed child connects no MCP at all and loses live code search.
482
+ const parsedMcpServers = values["mcp-servers"]
483
+ ? values["mcp-servers"]
484
+ .split(",")
485
+ .map((s) => s.trim())
486
+ .filter(Boolean)
487
+ : [];
488
+ const allowedMcpServers = parsedMcpServers.length > 0 ? parsedMcpServers : undefined;
470
489
  await runJsonMode({
471
490
  message: positionals[0] ?? "",
472
491
  provider: (values.provider ?? "anthropic"),
473
- model: values.model ?? "claude-opus-4-8",
492
+ model: values.model ?? "claude-opus-5",
474
493
  cwd: process.cwd(),
475
494
  systemPrompt: values["system-prompt"],
476
495
  maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
477
496
  allowedTools,
497
+ allowedMcpServers,
478
498
  promptCacheKey: values["prompt-cache-key"],
479
499
  }).catch(async (err) => {
480
500
  captureSidecarError(err, "app-sidecar.json-mode", {
@@ -518,6 +538,31 @@ async function main() {
518
538
  // ~/.ezcoder/debug.log (initLogger truncates on each start).
519
539
  const sidecarLog = path.join(paths.agentDir, "ezcoder-app-sidecar.log");
520
540
  initLogger(sidecarLog);
541
+ // The desktop sidecar previously omitted the stream diagnostic hook used by
542
+ // the CLI, leaving device-specific provider stalls impossible to distinguish from
543
+ // event-loop starvation or a broken streaming network path. Keep routine
544
+ // phases lightweight; timeout phases include non-sensitive runtime context.
545
+ setStreamDiagnostic((phase, data) => {
546
+ const includeRuntime = phase === "idle_timeout_fired" ||
547
+ phase === "hard_timeout_fired" ||
548
+ phase === "stall_exhausted";
549
+ log("INFO", "stream", phase, {
550
+ ...(data ?? {}),
551
+ ...(includeRuntime
552
+ ? {
553
+ platform: process.platform,
554
+ arch: process.arch,
555
+ osRelease: os.release(),
556
+ node: process.version,
557
+ logicalCpus: os.cpus().length,
558
+ totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024),
559
+ freeMemoryMb: Math.round(os.freemem() / 1024 / 1024),
560
+ rssMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
561
+ processUptimeSec: Math.round(process.uptime()),
562
+ }
563
+ : {}),
564
+ });
565
+ });
521
566
  // Global last-resort guards, installed as early as the logger allows so they
522
567
  // cover the WHOLE lifecycle — including startup/initialize, the phase the
523
568
  // "sidecar did not start in time" bug lives in. The sidecar is a long-lived
@@ -1072,6 +1117,25 @@ async function createSession(deps, opts) {
1072
1117
  // /help
1073
1118
  .replaceAll(/see \/help/gi, "check the help menu"));
1074
1119
  }
1120
+ /**
1121
+ * Guidance for a connection failure against the ACTIVE local endpoint, or
1122
+ * undefined when that isn't the situation (so the generic wording stands).
1123
+ * "Disable your VPN / allow us through the firewall" is useless advice for a
1124
+ * server running on this machine — name what the user has to restart.
1125
+ */
1126
+ function localNetworkGuidance(source) {
1127
+ if (source !== "network")
1128
+ return undefined;
1129
+ const state = session.getState();
1130
+ if (state.provider !== "local")
1131
+ return undefined;
1132
+ const parsed = parseLocalModelId(state.model);
1133
+ const probe = localProbes.find((p) => p.endpoint.id === parsed?.endpointId);
1134
+ if (!probe)
1135
+ return undefined;
1136
+ return (`${probe.endpoint.label} stopped responding at ${probe.endpoint.baseUrl}. ` +
1137
+ "Start it again and retry, or pick another model.");
1138
+ }
1075
1139
  // Turn any thrown value into the same clear headline/message/guidance shape
1076
1140
  // the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
1077
1141
  // the full detail, and broadcast it under `type` ("error" or "nolan_error").
@@ -1081,7 +1145,7 @@ async function createSession(deps, opts) {
1081
1145
  function broadcastError(type, logLabel, err) {
1082
1146
  const f = formatError(err);
1083
1147
  const message = f.message ? desktopGuidance(f.message) : undefined;
1084
- const guidance = desktopGuidance(f.guidance);
1148
+ const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
1085
1149
  captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
1086
1150
  scope: type,
1087
1151
  ...(f.provider ? { provider: f.provider } : {}),
@@ -1197,17 +1261,116 @@ async function createSession(deps, opts) {
1197
1261
  }
1198
1262
  }
1199
1263
  log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
1264
+ // ── Local models (Ollama / LM Studio / llama.cpp / vLLM) ──
1265
+ // Probing four HTTP endpoints must never delay readiness, so this runs in the
1266
+ // background (same shape as backgroundMcpConnect) and pushes a models_change
1267
+ // frame when it lands.
1268
+ let localProbes = [];
1269
+ async function scanLocalModels(force) {
1270
+ const endpoints = await listAllEndpoints();
1271
+ const { probes, models } = await discoverLocalModels(endpoints, { force });
1272
+ localProbes = probes;
1273
+ // Only endpoints that answered get a credential: writing one for a server
1274
+ // that isn't running would make `hasProviderAuth("local")` true forever.
1275
+ await syncEndpointCredentials(probes.filter((probe) => probe.reachable).map((probe) => probe.endpoint));
1276
+ clearRuntimeModels((m) => m.provider === "local");
1277
+ registerRuntimeModels(models);
1278
+ log("INFO", "app-sidecar", "local model scan", {
1279
+ reachable: probes.filter((p) => p.reachable).length + "/" + probes.length,
1280
+ models: String(models.length),
1281
+ });
1282
+ return probes;
1283
+ }
1284
+ /** Endpoint rows + models, in the shape the Local models UI renders. */
1285
+ function localStatePayload() {
1286
+ return {
1287
+ endpoints: localProbes.map((probe) => ({
1288
+ id: probe.endpoint.id,
1289
+ label: probe.endpoint.label,
1290
+ baseUrl: probe.endpoint.baseUrl,
1291
+ kind: probe.endpoint.kind,
1292
+ custom: probe.endpoint.custom === true,
1293
+ reachable: probe.reachable,
1294
+ ...(probe.reason ? { reason: probe.reason } : {}),
1295
+ models: probe.models.map((m) => ({
1296
+ id: formatLocalModelId(probe.endpoint.id, m.rawId),
1297
+ rawId: m.rawId,
1298
+ contextWindow: m.contextWindow,
1299
+ contextWindowKnown: m.contextWindowKnown,
1300
+ supportsTools: m.supportsTools,
1301
+ supportsImages: m.supportsImages,
1302
+ supportsThinking: m.supportsThinking,
1303
+ ...(m.loaded === undefined ? {} : { loaded: m.loaded }),
1304
+ })),
1305
+ })),
1306
+ };
1307
+ }
1308
+ /**
1309
+ * Why `modelId` can't be selected right now, or `undefined` when it can.
1310
+ * Two real footguns get a clear answer instead of a mid-run provider error:
1311
+ * a model with no tool calling (can't drive the agent at all), and a server
1312
+ * that has since been shut down.
1313
+ */
1314
+ async function localModelBlocker(modelId) {
1315
+ const parsed = parseLocalModelId(modelId);
1316
+ if (!parsed)
1317
+ return undefined;
1318
+ const endpoints = await listAllEndpoints();
1319
+ const endpoint = endpoints.find((e) => e.id === parsed.endpointId);
1320
+ if (!endpoint)
1321
+ return `Unknown local endpoint "${parsed.endpointId}" — re-scan for local models.`;
1322
+ const probe = await probeEndpoint(endpoint);
1323
+ // Keep the cached view honest: this probe is fresher than the last scan.
1324
+ localProbes = localProbes.map((p) => (p.endpoint.id === endpoint.id ? probe : p));
1325
+ if (!probe.reachable) {
1326
+ return `${endpoint.label} isn't running at ${endpoint.baseUrl}. Start it and scan again.`;
1327
+ }
1328
+ const model = probe.models.find((m) => m.rawId === parsed.rawId);
1329
+ if (!model) {
1330
+ return `${endpoint.label} no longer serves "${parsed.rawId}".`;
1331
+ }
1332
+ if (!model.supportsTools) {
1333
+ return `${parsed.rawId} has no tool calling, so it can't run the agent. Pick a tool-capable model.`;
1334
+ }
1335
+ registerRuntimeModels(probe.models.map((m) => localModelInfo(m, endpoint)));
1336
+ return undefined;
1337
+ }
1338
+ /**
1339
+ * A restored per-project pref can ask for thinking on a local model that
1340
+ * turns out not to reason (capabilities are only known after a probe). Drop
1341
+ * the level once we know, so the first prompt doesn't carry a
1342
+ * `reasoning_effort` the server rejects.
1343
+ */
1344
+ function clampLocalThinking() {
1345
+ const st = session.getState();
1346
+ const level = session.getThinkingLevel();
1347
+ if (!level || st.provider !== "local")
1348
+ return;
1349
+ if (isThinkingLevelSupported(st.provider, st.model, level))
1350
+ return;
1351
+ session.setThinkingLevel(undefined);
1352
+ broadcast("thinking_change", {
1353
+ thinkingLevel: null,
1354
+ supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
1355
+ });
1356
+ }
1200
1357
  // Workspace extras (context window, git status, background tasks). Git state
1201
1358
  // is resolved once at startup and refreshed after every run; the context
1202
1359
  // window follows the active model.
1203
- const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
1360
+ const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
1204
1361
  getGitBranch(cwd).catch(() => null),
1205
1362
  isGitRepo(cwd).catch(() => false),
1206
1363
  getGitDirtyFileCount(cwd).catch(() => 0),
1364
+ getGitHubRepoSlug(cwd).catch(() => null),
1207
1365
  ]);
1208
1366
  let gitBranch = initialGitBranch;
1209
1367
  let gitIsRepo = initialGitIsRepo;
1210
1368
  let gitDirtyFileCount = initialDirtyFileCount;
1369
+ // Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
1370
+ // auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
1371
+ const gitHubSlug = initialGitHubSlug;
1372
+ let gitHubIssues = null;
1373
+ let gitHubPRs = null;
1211
1374
  function currentContextWindow() {
1212
1375
  const st = session.getState();
1213
1376
  return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
@@ -1220,9 +1383,41 @@ async function createSession(deps, opts) {
1220
1383
  gitBranch,
1221
1384
  isGitRepo: gitIsRepo,
1222
1385
  gitDirtyFileCount,
1386
+ gitHubIssues,
1387
+ gitHubPRs,
1388
+ gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
1223
1389
  tasks: session.listBackgroundProcesses(),
1390
+ // Roots added with /add-dir — the header shows a badge when non-empty.
1391
+ additionalRoots: session.getAdditionalRoots(),
1224
1392
  };
1225
1393
  }
1394
+ void scanLocalModels(false)
1395
+ .then(() => {
1396
+ clampLocalThinking();
1397
+ broadcast("extras", footerExtras());
1398
+ })
1399
+ .then(() => broadcast("models_change", { local: localStatePayload() }))
1400
+ .catch((err) => {
1401
+ // A discovery failure is never fatal — the user simply has no local
1402
+ // models. Log it; don't push an error row into the transcript.
1403
+ log("WARN", "app-sidecar", "local model scan failed", {
1404
+ error: err instanceof Error ? err.message : String(err),
1405
+ });
1406
+ });
1407
+ // Refresh the GitHub counts and broadcast only on change. Transient failures
1408
+ // keep the last-known numbers so the chips don't flicker off on a timeout.
1409
+ async function refreshGitHubCounts() {
1410
+ if (!gitHubSlug)
1411
+ return;
1412
+ const counts = await getGitHubOpenCounts(gitHubSlug);
1413
+ if (!counts)
1414
+ return;
1415
+ if (counts.issues !== gitHubIssues || counts.prs !== gitHubPRs) {
1416
+ gitHubIssues = counts.issues;
1417
+ gitHubPRs = counts.prs;
1418
+ broadcast("extras", footerExtras());
1419
+ }
1420
+ }
1226
1421
  // tool_call_end carries no tool name (only the id), so remember each call's
1227
1422
  // name from tool_call_start to log a useful line on completion. Mirrors the
1228
1423
  // CLI's logging so the app sidecar's ~/.ezcoder/ezcoder-app-sidecar.log records tool
@@ -1664,6 +1859,9 @@ async function createSession(deps, opts) {
1664
1859
  isGitRepo(cwd).catch(() => gitIsRepo),
1665
1860
  getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
1666
1861
  ]);
1862
+ // A run may have opened/closed issues or PRs — refresh fire-and-forget so
1863
+ // teardown isn't delayed by the network. Broadcasts itself on change.
1864
+ void refreshGitHubCounts();
1667
1865
  // Serialize behind any marker/tool-triggered refresh so the terminal
1668
1866
  // progress snapshot uses the live plan file. Once every canonical step
1669
1867
  // is complete, remove the approved plan from future system prompts and
@@ -2136,6 +2334,20 @@ async function createSession(deps, opts) {
2136
2334
  gitPoll.unref?.();
2137
2335
  };
2138
2336
  scheduleGitPoll(5000);
2337
+ // GitHub issue/PR counts change outside the app (web UI, teammates), so poll
2338
+ // on a slow cadence. Network-bound, so keep it well under the search API's
2339
+ // rate budget (2 calls per tick). No-op when the origin isn't a GitHub repo.
2340
+ let gitHubPoll;
2341
+ let gitHubPollStopped = false;
2342
+ const scheduleGitHubPoll = (delay) => {
2343
+ if (gitHubPollStopped)
2344
+ return;
2345
+ gitHubPoll = setTimeout(() => {
2346
+ void refreshGitHubCounts().finally(() => scheduleGitHubPoll(60_000));
2347
+ }, delay);
2348
+ gitHubPoll.unref?.();
2349
+ };
2350
+ scheduleGitHubPoll(2000);
2139
2351
  function readBody(req, res) {
2140
2352
  return readCappedBody(req, res);
2141
2353
  }
@@ -2386,6 +2598,30 @@ async function createSession(deps, opts) {
2386
2598
  });
2387
2599
  return;
2388
2600
  }
2601
+ // Markdown transcript export for the app's download button. Serialized
2602
+ // here rather than in the webview because the webview's transcript model
2603
+ // deliberately keeps tool activity in the LiveToolPanel — exporting from
2604
+ // there would hand the user a coding session with the coding missing.
2605
+ // `?name=1` asks for the suggested filename only (the save dialog needs it
2606
+ // before there is a path), so the markdown never crosses IPC twice.
2607
+ if (method === "GET" && (url === "/export" || url.startsWith("/export?"))) {
2608
+ const query = new URLSearchParams(url.slice(url.indexOf("?") + 1));
2609
+ const st = session.getState();
2610
+ const filename = defaultExportFilename(mode);
2611
+ if (query.get("name") === "1") {
2612
+ json(res, 200, { filename });
2613
+ return;
2614
+ }
2615
+ const markdown = sessionToMarkdown({
2616
+ mode,
2617
+ cwd,
2618
+ provider: st.provider,
2619
+ model: st.model,
2620
+ ...(st.sessionId ? { sessionId: st.sessionId } : {}),
2621
+ }, session.getMessages(), { toolDetail: parseToolDetail(query.get("tools")) });
2622
+ json(res, 200, { filename, markdown });
2623
+ return;
2624
+ }
2389
2625
  if (method === "GET" && url === "/history") {
2390
2626
  // Reconstruct the transcript from persisted messages so resume is 1:1 with
2391
2627
  // the live SSE stream. Walks ALL message types (not just user/assistant):
@@ -2705,16 +2941,34 @@ async function createSession(deps, opts) {
2705
2941
  description: c.description,
2706
2942
  source: "built-in",
2707
2943
  }));
2944
+ // Desktop-safe registry actions belong in the same picker as workflows.
2945
+ // Most registry commands have dedicated app controls or TUI-only flows;
2946
+ // multi-root management has no other affordance, so expose only these.
2947
+ const workspaceActions = [
2948
+ {
2949
+ name: "add-dir",
2950
+ aliases: ["adddir"],
2951
+ description: "Add another project folder to this workspace",
2952
+ source: "built-in",
2953
+ },
2954
+ {
2955
+ name: "remove-dir",
2956
+ aliases: ["removedir"],
2957
+ description: "Remove an added project folder from this workspace",
2958
+ source: "built-in",
2959
+ },
2960
+ ];
2708
2961
  const custom = (await loadCustomCommands(cwd))
2709
- // A custom command can't shadow a built-in name.
2710
- .filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name))
2962
+ // A custom command can't shadow a built-in name or app action.
2963
+ .filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
2964
+ !workspaceActions.some((action) => action.name === c.name))
2711
2965
  .map((c) => ({
2712
2966
  name: c.name,
2713
2967
  aliases: [],
2714
2968
  description: c.description,
2715
2969
  source: "custom",
2716
2970
  }));
2717
- json(res, 200, { commands: [...builtins, ...custom] });
2971
+ json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
2718
2972
  })();
2719
2973
  return;
2720
2974
  }
@@ -3173,12 +3427,30 @@ async function createSession(deps, opts) {
3173
3427
  loggedIn.push(p);
3174
3428
  }
3175
3429
  // Just the names, grouped by provider in registry order — the UI shows
3176
- // a clean multi-column list of model ids.
3177
- const models = MODELS.filter((m) => loggedIn.includes(m.provider)).map((m) => ({
3178
- id: m.id,
3179
- name: m.name,
3180
- provider: m.provider,
3181
- }));
3430
+ // a clean multi-column list of model ids. Local models come from the
3431
+ // runtime registry (populated by the background scan) and are always
3432
+ // listed: their "login" is the endpoint answering a probe.
3433
+ const models = getAllModels()
3434
+ .filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
3435
+ .map((m) => {
3436
+ if (m.provider !== "local") {
3437
+ return { id: m.id, name: m.name, provider: m.provider };
3438
+ }
3439
+ const probed = findProbedModel(localProbes, m.id);
3440
+ return {
3441
+ id: m.id,
3442
+ name: m.name,
3443
+ provider: m.provider,
3444
+ local: true,
3445
+ endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
3446
+ // A local model that can't call tools can't run the agent — the UI
3447
+ // renders it disabled rather than hiding it, so the user learns why.
3448
+ supportsTools: probed?.model.supportsTools ?? true,
3449
+ contextWindow: m.contextWindow,
3450
+ contextWindowKnown: probed?.model.contextWindowKnown ?? false,
3451
+ supportsThinking: m.supportsThinking,
3452
+ };
3453
+ });
3182
3454
  json(res, 200, { models });
3183
3455
  })();
3184
3456
  return;
@@ -3207,6 +3479,13 @@ async function createSession(deps, opts) {
3207
3479
  // Pick up any natively-written keys (desktop app) before resolving the
3208
3480
  // new model's credentials, so switching to a just-added provider works.
3209
3481
  await auth.reload();
3482
+ if (target.provider === "local") {
3483
+ const problem = await localModelBlocker(target.id);
3484
+ if (problem) {
3485
+ json(res, 409, { error: problem });
3486
+ return;
3487
+ }
3488
+ }
3210
3489
  await session.switchModel(target.provider, target.id);
3211
3490
  // Nolan follows EZ Coder's model only while un-pinned; a user-set Nolan
3212
3491
  // override survives EZ Coder model switches untouched.
@@ -3632,6 +3911,84 @@ async function createSession(deps, opts) {
3632
3911
  });
3633
3912
  return;
3634
3913
  }
3914
+ // ── Local models ──────────────────────────────────────
3915
+ // GET returns the last scan (cheap, no probing) so opening the modal is
3916
+ // instant; POST /local/scan is the explicit refresh.
3917
+ if (method === "GET" && url === "/local") {
3918
+ json(res, 200, localStatePayload());
3919
+ return;
3920
+ }
3921
+ if (method === "POST" && url === "/local/scan") {
3922
+ void scanLocalModels(true)
3923
+ .then(() => {
3924
+ broadcast("models_change", { local: localStatePayload() });
3925
+ json(res, 200, localStatePayload());
3926
+ })
3927
+ .catch((err) => {
3928
+ broadcastError("error", "local model scan failed", err);
3929
+ json(res, 500, { error: "Local model scan failed — see the sidecar log." });
3930
+ });
3931
+ return;
3932
+ }
3933
+ if (method === "POST" && url === "/local/endpoints") {
3934
+ void readBody(req, res).then(async (raw) => {
3935
+ if (raw === null)
3936
+ return;
3937
+ let body;
3938
+ try {
3939
+ body = JSON.parse(raw);
3940
+ }
3941
+ catch {
3942
+ json(res, 400, { error: "invalid JSON body" });
3943
+ return;
3944
+ }
3945
+ try {
3946
+ const endpoint = await addCustomEndpoint({
3947
+ baseUrl: body.baseUrl ?? "",
3948
+ ...(body.label ? { label: body.label } : {}),
3949
+ ...(body.apiKey ? { apiKey: body.apiKey } : {}),
3950
+ });
3951
+ await scanLocalModels(true);
3952
+ broadcast("models_change", { local: localStatePayload() });
3953
+ json(res, 200, {
3954
+ endpoint: { id: endpoint.id, label: endpoint.label },
3955
+ ...localStatePayload(),
3956
+ });
3957
+ }
3958
+ catch (err) {
3959
+ // Validation errors are the user's typo, not a system fault — 400 with
3960
+ // the exact reason, and nothing in the transcript.
3961
+ if (err instanceof LocalEndpointError) {
3962
+ json(res, 400, { error: err.message });
3963
+ return;
3964
+ }
3965
+ broadcastError("error", "add local endpoint failed", err);
3966
+ json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
3967
+ }
3968
+ });
3969
+ return;
3970
+ }
3971
+ if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
3972
+ const id = decodeURIComponent(url.slice("/local/endpoints/".length));
3973
+ void (async () => {
3974
+ try {
3975
+ await removeCustomEndpoint(id);
3976
+ clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
3977
+ await scanLocalModels(true);
3978
+ broadcast("models_change", { local: localStatePayload() });
3979
+ json(res, 200, localStatePayload());
3980
+ }
3981
+ catch (err) {
3982
+ if (err instanceof LocalEndpointError) {
3983
+ json(res, 400, { error: err.message });
3984
+ return;
3985
+ }
3986
+ broadcastError("error", "remove local endpoint failed", err);
3987
+ json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
3988
+ }
3989
+ })();
3990
+ return;
3991
+ }
3635
3992
  // ── Telegram config (mirrors `ezcoder telegram`) ─────────
3636
3993
  if (method === "GET" && url === "/telegram") {
3637
3994
  void loadTelegramConfig().then((cfg) => {
@@ -3919,6 +4276,9 @@ async function createSession(deps, opts) {
3919
4276
  gitPollStopped = true;
3920
4277
  if (gitPoll)
3921
4278
  clearTimeout(gitPoll);
4279
+ gitHubPollStopped = true;
4280
+ if (gitHubPoll)
4281
+ clearTimeout(gitHubPoll);
3922
4282
  // Stop the Telegram serve loop + dispose its per-chat sessions.
3923
4283
  if (serveController)
3924
4284
  await serveController.stop().catch(() => { });