@prestyj/cli 5.10.0 → 5.12.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 (703) hide show
  1. package/README.md +3 -4
  2. package/assets/skills/compliance-guard/SKILL.md +251 -0
  3. package/assets/skills/compliance-guard/references/artifacts.md +186 -0
  4. package/assets/skills/compliance-guard/references/eu-uk.md +151 -0
  5. package/assets/skills/compliance-guard/references/exposure-triage.md +161 -0
  6. package/assets/skills/compliance-guard/references/lawsuit-vectors.md +141 -0
  7. package/assets/skills/compliance-guard/references/provenance.md +42 -0
  8. package/assets/skills/compliance-guard/references/sector-gates.md +173 -0
  9. package/assets/skills/compliance-guard/references/security-baseline.md +130 -0
  10. package/assets/skills/compliance-guard/references/trigger-map.md +163 -0
  11. package/assets/skills/compliance-guard/references/us.md +164 -0
  12. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  13. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  14. package/dist/app-sidecar-auth-broadcast.test.js +348 -0
  15. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  16. package/dist/app-sidecar-sessions.d.ts +5 -2
  17. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  18. package/dist/app-sidecar-sessions.js +34 -8
  19. package/dist/app-sidecar-sessions.js.map +1 -1
  20. package/dist/app-sidecar-sessions.test.js +96 -1
  21. package/dist/app-sidecar-sessions.test.js.map +1 -1
  22. package/dist/app-sidecar.js +616 -173
  23. package/dist/app-sidecar.js.map +1 -1
  24. package/dist/chat-agents/jiwa.test.js +1 -1
  25. package/dist/cli/auth.d.ts.map +1 -1
  26. package/dist/cli/auth.js +33 -36
  27. package/dist/cli/auth.js.map +1 -1
  28. package/dist/cli/command-routing.d.ts +1 -1
  29. package/dist/cli/command-routing.d.ts.map +1 -1
  30. package/dist/cli/command-routing.js +1 -0
  31. package/dist/cli/command-routing.js.map +1 -1
  32. package/dist/cli/command-routing.test.js +1 -0
  33. package/dist/cli/command-routing.test.js.map +1 -1
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +179 -48
  36. package/dist/cli.js.map +1 -1
  37. package/dist/config.d.ts +18 -5
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +45 -76
  40. package/dist/config.js.map +1 -1
  41. package/dist/config.test.js +30 -13
  42. package/dist/config.test.js.map +1 -1
  43. package/dist/core/agent-notifications.d.ts +62 -0
  44. package/dist/core/agent-notifications.d.ts.map +1 -0
  45. package/dist/core/agent-notifications.js +92 -0
  46. package/dist/core/agent-notifications.js.map +1 -0
  47. package/dist/core/agent-notifications.test.d.ts +2 -0
  48. package/dist/core/agent-notifications.test.d.ts.map +1 -0
  49. package/dist/core/agent-notifications.test.js +97 -0
  50. package/dist/core/agent-notifications.test.js.map +1 -0
  51. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  52. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  53. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  54. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  55. package/dist/core/agent-session-compaction.test.js +323 -6
  56. package/dist/core/agent-session-compaction.test.js.map +1 -1
  57. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  58. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  59. package/dist/core/agent-session-crash-durability.test.js +102 -0
  60. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  61. package/dist/core/agent-session-marker-anchors.test.js +181 -2
  62. package/dist/core/agent-session-marker-anchors.test.js.map +1 -1
  63. package/dist/core/agent-session-model-switch.test.d.ts +2 -0
  64. package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  65. package/dist/core/agent-session-model-switch.test.js +161 -0
  66. package/dist/core/agent-session-model-switch.test.js.map +1 -0
  67. package/dist/core/agent-session-notifications.test.d.ts +2 -0
  68. package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
  69. package/dist/core/agent-session-notifications.test.js +114 -0
  70. package/dist/core/agent-session-notifications.test.js.map +1 -0
  71. package/dist/core/agent-session-process-gate.test.d.ts +2 -0
  72. package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
  73. package/dist/core/agent-session-process-gate.test.js +98 -0
  74. package/dist/core/agent-session-process-gate.test.js.map +1 -0
  75. package/dist/core/agent-session-queue.test.js +65 -0
  76. package/dist/core/agent-session-queue.test.js.map +1 -1
  77. package/dist/core/agent-session-review-coverage.test.js +80 -23
  78. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  79. package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
  80. package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
  81. package/dist/core/agent-session-slash-restore.test.js +121 -0
  82. package/dist/core/agent-session-slash-restore.test.js.map +1 -0
  83. package/dist/core/agent-session-tool-tiers.test.d.ts +2 -0
  84. package/dist/core/agent-session-tool-tiers.test.d.ts.map +1 -0
  85. package/dist/core/agent-session-tool-tiers.test.js +138 -0
  86. package/dist/core/agent-session-tool-tiers.test.js.map +1 -0
  87. package/dist/core/agent-session.d.ts +257 -13
  88. package/dist/core/agent-session.d.ts.map +1 -1
  89. package/dist/core/agent-session.js +1118 -241
  90. package/dist/core/agent-session.js.map +1 -1
  91. package/dist/core/agents.d.ts +29 -1
  92. package/dist/core/agents.d.ts.map +1 -1
  93. package/dist/core/agents.js +47 -97
  94. package/dist/core/agents.js.map +1 -1
  95. package/dist/core/agents.test.js +87 -7
  96. package/dist/core/agents.test.js.map +1 -1
  97. package/dist/core/api-benchmark.d.ts +3 -3
  98. package/dist/core/api-benchmark.js +7 -7
  99. package/dist/core/auth-providers.d.ts +37 -1
  100. package/dist/core/auth-providers.d.ts.map +1 -1
  101. package/dist/core/auth-providers.js +83 -3
  102. package/dist/core/auth-providers.js.map +1 -1
  103. package/dist/core/auto-update.test.js +1 -1
  104. package/dist/core/autopilot-gate.d.ts +2 -2
  105. package/dist/core/autopilot-gate.js +1 -1
  106. package/dist/core/bash-spawn-benchmark.js +6 -6
  107. package/dist/core/bundled-agents.d.ts +3 -0
  108. package/dist/core/bundled-agents.d.ts.map +1 -0
  109. package/dist/core/bundled-agents.js +252 -0
  110. package/dist/core/bundled-agents.js.map +1 -0
  111. package/dist/core/cache-warm-benchmark.d.ts +2 -2
  112. package/dist/core/cache-warm-benchmark.js +5 -5
  113. package/dist/core/code-retrieval-chunkers.d.ts +27 -0
  114. package/dist/core/code-retrieval-chunkers.d.ts.map +1 -0
  115. package/dist/core/code-retrieval-chunkers.js +264 -0
  116. package/dist/core/code-retrieval-chunkers.js.map +1 -0
  117. package/dist/core/code-retrieval-chunkers.test.d.ts +2 -0
  118. package/dist/core/code-retrieval-chunkers.test.d.ts.map +1 -0
  119. package/dist/core/code-retrieval-chunkers.test.js +203 -0
  120. package/dist/core/code-retrieval-chunkers.test.js.map +1 -0
  121. package/dist/core/code-retrieval.d.ts +6 -0
  122. package/dist/core/code-retrieval.d.ts.map +1 -1
  123. package/dist/core/code-retrieval.js +38 -1
  124. package/dist/core/code-retrieval.js.map +1 -1
  125. package/dist/core/compaction/active-context.test.js +21 -0
  126. package/dist/core/compaction/active-context.test.js.map +1 -1
  127. package/dist/core/compaction/compactor.d.ts +108 -8
  128. package/dist/core/compaction/compactor.d.ts.map +1 -1
  129. package/dist/core/compaction/compactor.js +423 -119
  130. package/dist/core/compaction/compactor.js.map +1 -1
  131. package/dist/core/compaction/compactor.test.js +360 -8
  132. package/dist/core/compaction/compactor.test.js.map +1 -1
  133. package/dist/core/compaction/policy.d.ts +17 -0
  134. package/dist/core/compaction/policy.d.ts.map +1 -0
  135. package/dist/core/compaction/policy.js +22 -0
  136. package/dist/core/compaction/policy.js.map +1 -0
  137. package/dist/core/compaction/policy.test.d.ts +2 -0
  138. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  139. package/dist/core/compaction/policy.test.js +35 -0
  140. package/dist/core/compaction/policy.test.js.map +1 -0
  141. package/dist/core/compaction/query-aware-selector.d.ts +33 -0
  142. package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
  143. package/dist/core/compaction/query-aware-selector.js +202 -0
  144. package/dist/core/compaction/query-aware-selector.js.map +1 -0
  145. package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
  146. package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
  147. package/dist/core/compaction/query-aware-selector.test.js +96 -0
  148. package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
  149. package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
  150. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
  151. package/dist/core/compaction/tool-result-pruner.js +98 -26
  152. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  153. package/dist/core/compaction/tool-result-pruner.test.js +178 -6
  154. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
  155. package/dist/core/encode-cwd.test.js +1 -1
  156. package/dist/core/event-bus.d.ts +23 -0
  157. package/dist/core/event-bus.d.ts.map +1 -1
  158. package/dist/core/event-bus.js +10 -0
  159. package/dist/core/event-bus.js.map +1 -1
  160. package/dist/core/extensions/loader.d.ts.map +1 -1
  161. package/dist/core/extensions/loader.js +28 -8
  162. package/dist/core/extensions/loader.js.map +1 -1
  163. package/dist/core/extensions/plugin-bundles.d.ts +23 -0
  164. package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
  165. package/dist/core/extensions/plugin-bundles.js +228 -0
  166. package/dist/core/extensions/plugin-bundles.js.map +1 -0
  167. package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
  168. package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
  169. package/dist/core/extensions/plugin-bundles.test.js +110 -0
  170. package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
  171. package/dist/core/fast-apply-benchmark.d.ts +2 -2
  172. package/dist/core/fast-apply-benchmark.js +6 -6
  173. package/dist/core/foreign-session-import.d.ts +120 -0
  174. package/dist/core/foreign-session-import.d.ts.map +1 -0
  175. package/dist/core/foreign-session-import.js +598 -0
  176. package/dist/core/foreign-session-import.js.map +1 -0
  177. package/dist/core/foreign-session-import.test.d.ts +2 -0
  178. package/dist/core/foreign-session-import.test.d.ts.map +1 -0
  179. package/dist/core/foreign-session-import.test.js +220 -0
  180. package/dist/core/foreign-session-import.test.js.map +1 -0
  181. package/dist/core/foreign-transcript-blocks.d.ts +51 -0
  182. package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
  183. package/dist/core/foreign-transcript-blocks.js +151 -0
  184. package/dist/core/foreign-transcript-blocks.js.map +1 -0
  185. package/dist/core/goal-lifecycle-smoke.test.js +2 -2
  186. package/dist/core/goal-store.js +1 -1
  187. package/dist/core/goal-store.test.js +2 -2
  188. package/dist/core/goal-verifier.test.js +2 -2
  189. package/dist/core/goal-worker-dev-server-lifecycle.test.js +1 -1
  190. package/dist/core/goal-worker.test.js +2 -2
  191. package/dist/core/hashline-edit-benchmark.d.ts +2 -2
  192. package/dist/core/hashline-edit-benchmark.js +7 -7
  193. package/dist/core/ideal-review.d.ts +20 -1
  194. package/dist/core/ideal-review.d.ts.map +1 -1
  195. package/dist/core/ideal-review.js +37 -2
  196. package/dist/core/ideal-review.js.map +1 -1
  197. package/dist/core/ideal-review.test.js +99 -3
  198. package/dist/core/ideal-review.test.js.map +1 -1
  199. package/dist/core/local-endpoint-store.test.js +1 -1
  200. package/dist/core/logger.d.ts +1 -1
  201. package/dist/core/logger.js +2 -2
  202. package/dist/core/logger.js.map +1 -1
  203. package/dist/core/loop-breaker.d.ts.map +1 -1
  204. package/dist/core/loop-breaker.js +1 -0
  205. package/dist/core/loop-breaker.js.map +1 -1
  206. package/dist/core/lsp/client-navigation.test.d.ts +2 -0
  207. package/dist/core/lsp/client-navigation.test.d.ts.map +1 -0
  208. package/dist/core/lsp/client-navigation.test.js +135 -0
  209. package/dist/core/lsp/client-navigation.test.js.map +1 -0
  210. package/dist/core/lsp/client.d.ts +84 -0
  211. package/dist/core/lsp/client.d.ts.map +1 -1
  212. package/dist/core/lsp/client.js +190 -3
  213. package/dist/core/lsp/client.js.map +1 -1
  214. package/dist/core/lsp/integration.test.js +2 -2
  215. package/dist/core/lsp/manager-navigation.test.d.ts +2 -0
  216. package/dist/core/lsp/manager-navigation.test.d.ts.map +1 -0
  217. package/dist/core/lsp/manager-navigation.test.js +121 -0
  218. package/dist/core/lsp/manager-navigation.test.js.map +1 -0
  219. package/dist/core/lsp/manager.d.ts +81 -9
  220. package/dist/core/lsp/manager.d.ts.map +1 -1
  221. package/dist/core/lsp/manager.js +183 -64
  222. package/dist/core/lsp/manager.js.map +1 -1
  223. package/dist/core/lsp/manager.test.js +18 -0
  224. package/dist/core/lsp/manager.test.js.map +1 -1
  225. package/dist/core/lsp/pool.d.ts +113 -0
  226. package/dist/core/lsp/pool.d.ts.map +1 -0
  227. package/dist/core/lsp/pool.js +255 -0
  228. package/dist/core/lsp/pool.js.map +1 -0
  229. package/dist/core/lsp/pool.test.d.ts +2 -0
  230. package/dist/core/lsp/pool.test.d.ts.map +1 -0
  231. package/dist/core/lsp/pool.test.js +286 -0
  232. package/dist/core/lsp/pool.test.js.map +1 -0
  233. package/dist/core/lsp/servers.d.ts.map +1 -1
  234. package/dist/core/lsp/servers.js +43 -3
  235. package/dist/core/lsp/servers.js.map +1 -1
  236. package/dist/core/lsp/servers.test.d.ts +2 -0
  237. package/dist/core/lsp/servers.test.d.ts.map +1 -0
  238. package/dist/core/lsp/servers.test.js +67 -0
  239. package/dist/core/lsp/servers.test.js.map +1 -0
  240. package/dist/core/lsp/windows.test.js +6 -18
  241. package/dist/core/lsp/windows.test.js.map +1 -1
  242. package/dist/core/mcp/catalog-cache.d.ts +63 -0
  243. package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
  244. package/dist/core/mcp/catalog-cache.js +163 -0
  245. package/dist/core/mcp/catalog-cache.js.map +1 -0
  246. package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
  247. package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
  248. package/dist/core/mcp/catalog-cache.test.js +99 -0
  249. package/dist/core/mcp/catalog-cache.test.js.map +1 -0
  250. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  251. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  252. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  253. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  254. package/dist/core/mcp/client.d.ts +158 -0
  255. package/dist/core/mcp/client.d.ts.map +1 -1
  256. package/dist/core/mcp/client.js +478 -45
  257. package/dist/core/mcp/client.js.map +1 -1
  258. package/dist/core/mcp/content.d.ts +19 -0
  259. package/dist/core/mcp/content.d.ts.map +1 -0
  260. package/dist/core/mcp/content.js +172 -0
  261. package/dist/core/mcp/content.js.map +1 -0
  262. package/dist/core/mcp/content.test.d.ts +2 -0
  263. package/dist/core/mcp/content.test.d.ts.map +1 -0
  264. package/dist/core/mcp/content.test.js +135 -0
  265. package/dist/core/mcp/content.test.js.map +1 -0
  266. package/dist/core/mcp/deferred-catalog.d.ts +1 -1
  267. package/dist/core/mcp/deferred-catalog.js +1 -1
  268. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  269. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  270. package/dist/core/mcp/elicitation-bridge.js +52 -0
  271. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  272. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  273. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  274. package/dist/core/mcp/elicitation.test.js +158 -0
  275. package/dist/core/mcp/elicitation.test.js.map +1 -0
  276. package/dist/core/mcp/index.d.ts +5 -1
  277. package/dist/core/mcp/index.d.ts.map +1 -1
  278. package/dist/core/mcp/index.js +2 -0
  279. package/dist/core/mcp/index.js.map +1 -1
  280. package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
  281. package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
  282. package/dist/core/mcp/legacy-negotiation.test.js +80 -0
  283. package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
  284. package/dist/core/mcp/oauth-provider.d.ts +1 -2
  285. package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
  286. package/dist/core/mcp/oauth-provider.js +6 -2
  287. package/dist/core/mcp/oauth-provider.js.map +1 -1
  288. package/dist/core/mcp/oauth-store.d.ts +1 -1
  289. package/dist/core/mcp/oauth-store.d.ts.map +1 -1
  290. package/dist/core/mcp/oauth-store.js.map +1 -1
  291. package/dist/core/mcp/oauth-store.test.js +2 -2
  292. package/dist/core/mcp/resolve-stdio.test.js +5 -5
  293. package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
  294. package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
  295. package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
  296. package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
  297. package/dist/core/mcp/shared-pool.d.ts +158 -0
  298. package/dist/core/mcp/shared-pool.d.ts.map +1 -0
  299. package/dist/core/mcp/shared-pool.js +235 -0
  300. package/dist/core/mcp/shared-pool.js.map +1 -0
  301. package/dist/core/mcp/shared-pool.test.d.ts +2 -0
  302. package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
  303. package/dist/core/mcp/shared-pool.test.js +425 -0
  304. package/dist/core/mcp/shared-pool.test.js.map +1 -0
  305. package/dist/core/mcp/store.test.js +5 -5
  306. package/dist/core/mcp/types.d.ts +12 -0
  307. package/dist/core/mcp/types.d.ts.map +1 -1
  308. package/dist/core/mcp/when-connected.test.d.ts +2 -0
  309. package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
  310. package/dist/core/mcp/when-connected.test.js +46 -0
  311. package/dist/core/mcp/when-connected.test.js.map +1 -0
  312. package/dist/core/nolan-context.d.ts.map +1 -1
  313. package/dist/core/nolan-context.js +8 -0
  314. package/dist/core/nolan-context.js.map +1 -1
  315. package/dist/core/nolan-context.test.js +40 -0
  316. package/dist/core/nolan-context.test.js.map +1 -1
  317. package/dist/core/nolan-model.test.js +1 -1
  318. package/dist/core/nolan-prompt.js +8 -5
  319. package/dist/core/nolan-prompt.js.map +1 -1
  320. package/dist/core/nolan-prompt.test.js +5 -0
  321. package/dist/core/nolan-prompt.test.js.map +1 -1
  322. package/dist/core/oauth/xai.d.ts +2 -0
  323. package/dist/core/oauth/xai.d.ts.map +1 -0
  324. package/dist/core/oauth/xai.js +4 -0
  325. package/dist/core/oauth/xai.js.map +1 -0
  326. package/dist/core/persistent-shell.d.ts +3 -2
  327. package/dist/core/persistent-shell.d.ts.map +1 -1
  328. package/dist/core/persistent-shell.js +11 -4
  329. package/dist/core/persistent-shell.js.map +1 -1
  330. package/dist/core/persistent-shell.test.js +6 -6
  331. package/dist/core/process-gate.d.ts +45 -0
  332. package/dist/core/process-gate.d.ts.map +1 -0
  333. package/dist/core/process-gate.js +79 -0
  334. package/dist/core/process-gate.js.map +1 -0
  335. package/dist/core/process-gate.test.d.ts +2 -0
  336. package/dist/core/process-gate.test.d.ts.map +1 -0
  337. package/dist/core/process-gate.test.js +87 -0
  338. package/dist/core/process-gate.test.js.map +1 -0
  339. package/dist/core/process-manager-dev-server-repro.test.js +51 -12
  340. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  341. package/dist/core/process-manager-notifications.test.d.ts +2 -0
  342. package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
  343. package/dist/core/process-manager-notifications.test.js +227 -0
  344. package/dist/core/process-manager-notifications.test.js.map +1 -0
  345. package/dist/core/process-manager.d.ts +72 -1
  346. package/dist/core/process-manager.d.ts.map +1 -1
  347. package/dist/core/process-manager.js +247 -4
  348. package/dist/core/process-manager.js.map +1 -1
  349. package/dist/core/progress/git-xp.test.js +2 -2
  350. package/dist/core/progress/ranks.d.ts +14 -3
  351. package/dist/core/progress/ranks.d.ts.map +1 -1
  352. package/dist/core/progress/ranks.js +181 -12
  353. package/dist/core/progress/ranks.js.map +1 -1
  354. package/dist/core/progress/ranks.test.js +48 -7
  355. package/dist/core/progress/ranks.test.js.map +1 -1
  356. package/dist/core/progress/rebuild.test.js +1 -1
  357. package/dist/core/progress/store.test.js +1 -1
  358. package/dist/core/progress/types.d.ts +2 -1
  359. package/dist/core/progress/types.d.ts.map +1 -1
  360. package/dist/core/project-discovery.d.ts +31 -6
  361. package/dist/core/project-discovery.d.ts.map +1 -1
  362. package/dist/core/project-discovery.js +349 -79
  363. package/dist/core/project-discovery.js.map +1 -1
  364. package/dist/core/project-discovery.test.js +128 -4
  365. package/dist/core/project-discovery.test.js.map +1 -1
  366. package/dist/core/prompt-commands.js +3 -3
  367. package/dist/core/radio.d.ts +1 -1
  368. package/dist/core/radio.d.ts.map +1 -1
  369. package/dist/core/radio.js +90 -6
  370. package/dist/core/radio.js.map +1 -1
  371. package/dist/core/radio.test.js +15 -2
  372. package/dist/core/radio.test.js.map +1 -1
  373. package/dist/core/regrounding.d.ts.map +1 -1
  374. package/dist/core/regrounding.js +1 -0
  375. package/dist/core/regrounding.js.map +1 -1
  376. package/dist/core/run-claim.d.ts +37 -0
  377. package/dist/core/run-claim.d.ts.map +1 -0
  378. package/dist/core/run-claim.js +46 -0
  379. package/dist/core/run-claim.js.map +1 -0
  380. package/dist/core/run-claim.test.d.ts +2 -0
  381. package/dist/core/run-claim.test.d.ts.map +1 -0
  382. package/dist/core/run-claim.test.js +107 -0
  383. package/dist/core/run-claim.test.js.map +1 -0
  384. package/dist/core/run-journal.test.d.ts +2 -0
  385. package/dist/core/run-journal.test.d.ts.map +1 -0
  386. package/dist/core/run-journal.test.js +195 -0
  387. package/dist/core/run-journal.test.js.map +1 -0
  388. package/dist/core/run-lifecycle.d.ts +22 -3
  389. package/dist/core/run-lifecycle.d.ts.map +1 -1
  390. package/dist/core/run-lifecycle.js +12 -3
  391. package/dist/core/run-lifecycle.js.map +1 -1
  392. package/dist/core/sandbox-domains.d.ts +14 -0
  393. package/dist/core/sandbox-domains.d.ts.map +1 -0
  394. package/dist/core/sandbox-domains.js +76 -0
  395. package/dist/core/sandbox-domains.js.map +1 -0
  396. package/dist/core/sandbox-feedback.d.ts +22 -0
  397. package/dist/core/sandbox-feedback.d.ts.map +1 -0
  398. package/dist/core/sandbox-feedback.js +69 -0
  399. package/dist/core/sandbox-feedback.js.map +1 -0
  400. package/dist/core/sandbox-feedback.test.d.ts +2 -0
  401. package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
  402. package/dist/core/sandbox-feedback.test.js +32 -0
  403. package/dist/core/sandbox-feedback.test.js.map +1 -0
  404. package/dist/core/sandbox.d.ts +91 -0
  405. package/dist/core/sandbox.d.ts.map +1 -0
  406. package/dist/core/sandbox.js +254 -0
  407. package/dist/core/sandbox.js.map +1 -0
  408. package/dist/core/sandbox.test.d.ts +2 -0
  409. package/dist/core/sandbox.test.d.ts.map +1 -0
  410. package/dist/core/sandbox.test.js +157 -0
  411. package/dist/core/sandbox.test.js.map +1 -0
  412. package/dist/core/semantic-search-benchmark.d.ts +2 -2
  413. package/dist/core/semantic-search-benchmark.js +5 -5
  414. package/dist/core/session-compaction.d.ts +6 -0
  415. package/dist/core/session-compaction.d.ts.map +1 -1
  416. package/dist/core/session-compaction.js +11 -1
  417. package/dist/core/session-compaction.js.map +1 -1
  418. package/dist/core/session-compaction.test.js +19 -2
  419. package/dist/core/session-compaction.test.js.map +1 -1
  420. package/dist/core/session-export.d.ts.map +1 -1
  421. package/dist/core/session-export.js +12 -4
  422. package/dist/core/session-export.js.map +1 -1
  423. package/dist/core/session-history.d.ts +95 -6
  424. package/dist/core/session-history.d.ts.map +1 -1
  425. package/dist/core/session-history.js +261 -17
  426. package/dist/core/session-history.js.map +1 -1
  427. package/dist/core/session-history.test.js +173 -2
  428. package/dist/core/session-history.test.js.map +1 -1
  429. package/dist/core/session-manager.d.ts +223 -6
  430. package/dist/core/session-manager.d.ts.map +1 -1
  431. package/dist/core/session-manager.js +579 -74
  432. package/dist/core/session-manager.js.map +1 -1
  433. package/dist/core/session-manager.test.js +154 -10
  434. package/dist/core/session-manager.test.js.map +1 -1
  435. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  436. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  437. package/dist/core/session-notification-visibility.test.js +113 -0
  438. package/dist/core/session-notification-visibility.test.js.map +1 -0
  439. package/dist/core/session-preview.d.ts +5 -4
  440. package/dist/core/session-preview.d.ts.map +1 -1
  441. package/dist/core/session-preview.js +12 -5
  442. package/dist/core/session-preview.js.map +1 -1
  443. package/dist/core/session-restore-display.test.js +25 -0
  444. package/dist/core/session-restore-display.test.js.map +1 -1
  445. package/dist/core/session-storage-fd-leak.test.d.ts +2 -0
  446. package/dist/core/session-storage-fd-leak.test.d.ts.map +1 -0
  447. package/dist/core/session-storage-fd-leak.test.js +116 -0
  448. package/dist/core/session-storage-fd-leak.test.js.map +1 -0
  449. package/dist/core/session-storage.d.ts +22 -2
  450. package/dist/core/session-storage.d.ts.map +1 -1
  451. package/dist/core/session-storage.js +72 -9
  452. package/dist/core/session-storage.js.map +1 -1
  453. package/dist/core/session-storage.test.js +8 -3
  454. package/dist/core/session-storage.test.js.map +1 -1
  455. package/dist/core/settings-manager.d.ts +10 -2
  456. package/dist/core/settings-manager.d.ts.map +1 -1
  457. package/dist/core/settings-manager.js +36 -1
  458. package/dist/core/settings-manager.js.map +1 -1
  459. package/dist/core/shell-path.js +1 -1
  460. package/dist/core/shell.d.ts +1 -1
  461. package/dist/core/shell.js +2 -2
  462. package/dist/core/shell.test.js +2 -2
  463. package/dist/core/shutdown.d.ts +71 -0
  464. package/dist/core/shutdown.d.ts.map +1 -0
  465. package/dist/core/shutdown.js +144 -0
  466. package/dist/core/shutdown.js.map +1 -0
  467. package/dist/core/shutdown.test.d.ts +2 -0
  468. package/dist/core/shutdown.test.d.ts.map +1 -0
  469. package/dist/core/shutdown.test.js +160 -0
  470. package/dist/core/shutdown.test.js.map +1 -0
  471. package/dist/core/sidecar-error-reporter.js +1 -1
  472. package/dist/core/sidecar-error-reporter.test.js +2 -2
  473. package/dist/core/skills-routing.test.js +96 -0
  474. package/dist/core/skills-routing.test.js.map +1 -1
  475. package/dist/core/skills.d.ts.map +1 -1
  476. package/dist/core/skills.js +3 -0
  477. package/dist/core/skills.js.map +1 -1
  478. package/dist/core/steering.d.ts +12 -0
  479. package/dist/core/steering.d.ts.map +1 -1
  480. package/dist/core/steering.js +16 -0
  481. package/dist/core/steering.js.map +1 -1
  482. package/dist/core/subagent-manager.d.ts +22 -0
  483. package/dist/core/subagent-manager.d.ts.map +1 -1
  484. package/dist/core/subagent-manager.js +84 -13
  485. package/dist/core/subagent-manager.js.map +1 -1
  486. package/dist/core/subagent-manager.test.js +57 -15
  487. package/dist/core/subagent-manager.test.js.map +1 -1
  488. package/dist/core/subagent-store.test.js +3 -3
  489. package/dist/core/task-store.js +1 -1
  490. package/dist/core/verification-evidence.d.ts +17 -0
  491. package/dist/core/verification-evidence.d.ts.map +1 -0
  492. package/dist/core/verification-evidence.js +283 -0
  493. package/dist/core/verification-evidence.js.map +1 -0
  494. package/dist/core/verification-evidence.test.d.ts +2 -0
  495. package/dist/core/verification-evidence.test.d.ts.map +1 -0
  496. package/dist/core/verification-evidence.test.js +103 -0
  497. package/dist/core/verification-evidence.test.js.map +1 -0
  498. package/dist/index.js +1 -1
  499. package/dist/index.js.map +1 -1
  500. package/dist/interactive.d.ts.map +1 -1
  501. package/dist/interactive.js +17 -3
  502. package/dist/interactive.js.map +1 -1
  503. package/dist/modes/acp-mode.d.ts +122 -0
  504. package/dist/modes/acp-mode.d.ts.map +1 -0
  505. package/dist/modes/acp-mode.js +1366 -0
  506. package/dist/modes/acp-mode.js.map +1 -0
  507. package/dist/modes/acp-mode.test.d.ts +2 -0
  508. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  509. package/dist/modes/acp-mode.test.js +1065 -0
  510. package/dist/modes/acp-mode.test.js.map +1 -0
  511. package/dist/modes/agent-home-mode.d.ts.map +1 -1
  512. package/dist/modes/agent-home-mode.js +15 -11
  513. package/dist/modes/agent-home-mode.js.map +1 -1
  514. package/dist/modes/index.d.ts +1 -0
  515. package/dist/modes/index.d.ts.map +1 -1
  516. package/dist/modes/index.js +1 -0
  517. package/dist/modes/index.js.map +1 -1
  518. package/dist/modes/json-mode.d.ts +36 -0
  519. package/dist/modes/json-mode.d.ts.map +1 -1
  520. package/dist/modes/json-mode.js +39 -0
  521. package/dist/modes/json-mode.js.map +1 -1
  522. package/dist/modes/json-mode.test.d.ts +2 -0
  523. package/dist/modes/json-mode.test.d.ts.map +1 -0
  524. package/dist/modes/json-mode.test.js +84 -0
  525. package/dist/modes/json-mode.test.js.map +1 -0
  526. package/dist/modes/serve-mode.d.ts +1 -1
  527. package/dist/modes/serve-mode.d.ts.map +1 -1
  528. package/dist/modes/serve-mode.js +14 -9
  529. package/dist/modes/serve-mode.js.map +1 -1
  530. package/dist/modes/subagent-worker-mode.d.ts +8 -0
  531. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  532. package/dist/modes/subagent-worker-mode.js +91 -11
  533. package/dist/modes/subagent-worker-mode.js.map +1 -1
  534. package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
  535. package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
  536. package/dist/modes/subagent-worker-mode.test.js +52 -0
  537. package/dist/modes/subagent-worker-mode.test.js.map +1 -0
  538. package/dist/session.d.ts +29 -0
  539. package/dist/session.d.ts.map +1 -1
  540. package/dist/session.js +41 -0
  541. package/dist/session.js.map +1 -1
  542. package/dist/session.test.js +1 -1
  543. package/dist/system-prompt.d.ts +37 -1
  544. package/dist/system-prompt.d.ts.map +1 -1
  545. package/dist/system-prompt.js +123 -11
  546. package/dist/system-prompt.js.map +1 -1
  547. package/dist/system-prompt.test.js +90 -11
  548. package/dist/system-prompt.test.js.map +1 -1
  549. package/dist/system-prompt.tiering.test.d.ts +2 -0
  550. package/dist/system-prompt.tiering.test.d.ts.map +1 -0
  551. package/dist/system-prompt.tiering.test.js +105 -0
  552. package/dist/system-prompt.tiering.test.js.map +1 -0
  553. package/dist/tools/bash.d.ts +2 -1
  554. package/dist/tools/bash.d.ts.map +1 -1
  555. package/dist/tools/bash.js +55 -16
  556. package/dist/tools/bash.js.map +1 -1
  557. package/dist/tools/code-nav.d.ts +30 -0
  558. package/dist/tools/code-nav.d.ts.map +1 -0
  559. package/dist/tools/code-nav.js +320 -0
  560. package/dist/tools/code-nav.js.map +1 -0
  561. package/dist/tools/code-nav.test.d.ts +2 -0
  562. package/dist/tools/code-nav.test.d.ts.map +1 -0
  563. package/dist/tools/code-nav.test.js +136 -0
  564. package/dist/tools/code-nav.test.js.map +1 -0
  565. package/dist/tools/edit.test.js +1 -1
  566. package/dist/tools/generate-image.d.ts +1 -1
  567. package/dist/tools/goals.d.ts +4 -4
  568. package/dist/tools/goals.test.js +2 -2
  569. package/dist/tools/grep-parity.test.d.ts +2 -0
  570. package/dist/tools/grep-parity.test.d.ts.map +1 -0
  571. package/dist/tools/grep-parity.test.js +92 -0
  572. package/dist/tools/grep-parity.test.js.map +1 -0
  573. package/dist/tools/grep-recall.test.d.ts +2 -0
  574. package/dist/tools/grep-recall.test.d.ts.map +1 -0
  575. package/dist/tools/grep-recall.test.js +114 -0
  576. package/dist/tools/grep-recall.test.js.map +1 -0
  577. package/dist/tools/grep-spawn.test.d.ts +2 -0
  578. package/dist/tools/grep-spawn.test.d.ts.map +1 -0
  579. package/dist/tools/grep-spawn.test.js +66 -0
  580. package/dist/tools/grep-spawn.test.js.map +1 -0
  581. package/dist/tools/grep.d.ts +13 -3
  582. package/dist/tools/grep.d.ts.map +1 -1
  583. package/dist/tools/grep.js +342 -41
  584. package/dist/tools/grep.js.map +1 -1
  585. package/dist/tools/grep.test.js +17 -14
  586. package/dist/tools/grep.test.js.map +1 -1
  587. package/dist/tools/index.d.ts +20 -2
  588. package/dist/tools/index.d.ts.map +1 -1
  589. package/dist/tools/index.js +28 -18
  590. package/dist/tools/index.js.map +1 -1
  591. package/dist/tools/index.test.d.ts +2 -0
  592. package/dist/tools/index.test.d.ts.map +1 -0
  593. package/dist/tools/index.test.js +55 -0
  594. package/dist/tools/index.test.js.map +1 -0
  595. package/dist/tools/plan-mode.test.js +1 -1
  596. package/dist/tools/prompt-hints.d.ts +9 -0
  597. package/dist/tools/prompt-hints.d.ts.map +1 -1
  598. package/dist/tools/prompt-hints.js +61 -8
  599. package/dist/tools/prompt-hints.js.map +1 -1
  600. package/dist/tools/read-only-bash.d.ts +3 -0
  601. package/dist/tools/read-only-bash.d.ts.map +1 -1
  602. package/dist/tools/read-only-bash.js +7 -9
  603. package/dist/tools/read-only-bash.js.map +1 -1
  604. package/dist/tools/safe-env.js +1 -1
  605. package/dist/tools/search-code.d.ts.map +1 -1
  606. package/dist/tools/search-code.js +7 -6
  607. package/dist/tools/search-code.js.map +1 -1
  608. package/dist/tools/search-code.test.js +8 -2
  609. package/dist/tools/search-code.test.js.map +1 -1
  610. package/dist/tools/skill.js +2 -1
  611. package/dist/tools/skill.js.map +1 -1
  612. package/dist/tools/subagent-control.d.ts.map +1 -1
  613. package/dist/tools/subagent-control.js +25 -6
  614. package/dist/tools/subagent-control.js.map +1 -1
  615. package/dist/tools/subagent-control.test.js +34 -1
  616. package/dist/tools/subagent-control.test.js.map +1 -1
  617. package/dist/tools/subagent-shared.d.ts +26 -1
  618. package/dist/tools/subagent-shared.d.ts.map +1 -1
  619. package/dist/tools/subagent-shared.js +40 -7
  620. package/dist/tools/subagent-shared.js.map +1 -1
  621. package/dist/tools/subagent-shared.test.js +43 -9
  622. package/dist/tools/subagent-shared.test.js.map +1 -1
  623. package/dist/tools/subagent.d.ts.map +1 -1
  624. package/dist/tools/subagent.js +54 -16
  625. package/dist/tools/subagent.js.map +1 -1
  626. package/dist/tools/subagent.test.js +68 -3
  627. package/dist/tools/subagent.test.js.map +1 -1
  628. package/dist/tools/task-output.d.ts.map +1 -1
  629. package/dist/tools/task-output.js +3 -1
  630. package/dist/tools/task-output.js.map +1 -1
  631. package/dist/tools/task-send.test.js +19 -10
  632. package/dist/tools/task-send.test.js.map +1 -1
  633. package/dist/tools/tool-search.d.ts +12 -1
  634. package/dist/tools/tool-search.d.ts.map +1 -1
  635. package/dist/tools/tool-search.js +24 -4
  636. package/dist/tools/tool-search.js.map +1 -1
  637. package/dist/tools/tool-search.test.d.ts +2 -0
  638. package/dist/tools/tool-search.test.d.ts.map +1 -0
  639. package/dist/tools/tool-search.test.js +69 -0
  640. package/dist/tools/tool-search.test.js.map +1 -0
  641. package/dist/tools/tool-tiers.d.ts +42 -0
  642. package/dist/tools/tool-tiers.d.ts.map +1 -0
  643. package/dist/tools/tool-tiers.js +78 -0
  644. package/dist/tools/tool-tiers.js.map +1 -0
  645. package/dist/tools/tool-tiers.test.d.ts +2 -0
  646. package/dist/tools/tool-tiers.test.d.ts.map +1 -0
  647. package/dist/tools/tool-tiers.test.js +48 -0
  648. package/dist/tools/tool-tiers.test.js.map +1 -0
  649. package/dist/tools/web-fetch.d.ts +1 -1
  650. package/dist/tools/web-fetch.d.ts.map +1 -1
  651. package/dist/tools/web-fetch.js +5 -2
  652. package/dist/tools/web-fetch.js.map +1 -1
  653. package/dist/tools/write.test.js +1 -1
  654. package/dist/types.d.ts +7 -0
  655. package/dist/types.d.ts.map +1 -1
  656. package/dist/ui/App.d.ts.map +1 -1
  657. package/dist/ui/App.js +27 -1
  658. package/dist/ui/App.js.map +1 -1
  659. package/dist/ui/components/Banner.d.ts.map +1 -1
  660. package/dist/ui/components/Banner.js +24 -16
  661. package/dist/ui/components/Banner.js.map +1 -1
  662. package/dist/ui/components/ToolExecution.js +42 -34
  663. package/dist/ui/components/ToolExecution.js.map +1 -1
  664. package/dist/ui/components/force-color.d.ts +2 -0
  665. package/dist/ui/components/force-color.d.ts.map +1 -0
  666. package/dist/ui/components/force-color.js +11 -0
  667. package/dist/ui/components/force-color.js.map +1 -0
  668. package/dist/ui/components/themed-output.test.d.ts +2 -0
  669. package/dist/ui/components/themed-output.test.d.ts.map +1 -0
  670. package/dist/ui/components/themed-output.test.js +111 -0
  671. package/dist/ui/components/themed-output.test.js.map +1 -0
  672. package/dist/ui/error-item.d.ts +1 -1
  673. package/dist/ui/error-item.js +3 -3
  674. package/dist/ui/error-item.js.map +1 -1
  675. package/dist/ui/hooks/useAgentLoop.d.ts +3 -0
  676. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  677. package/dist/ui/hooks/useAgentLoop.js +45 -6
  678. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  679. package/dist/ui/login.js +1 -1
  680. package/dist/ui/login.js.map +1 -1
  681. package/dist/ui/render.d.ts +1 -1
  682. package/dist/ui/render.js +6 -6
  683. package/dist/ui/theme/dark-ansi.json +6 -0
  684. package/dist/ui/theme/dark-daltonized.json +6 -0
  685. package/dist/ui/theme/dark.json +6 -0
  686. package/dist/ui/theme/light-ansi.json +6 -0
  687. package/dist/ui/theme/light-daltonized.json +6 -0
  688. package/dist/ui/theme/light.json +6 -0
  689. package/dist/ui/theme/theme.d.ts +6 -0
  690. package/dist/ui/theme/theme.d.ts.map +1 -1
  691. package/dist/ui/tui-terminal-recorder.test.js +1 -1
  692. package/dist/utils/git.test.js +3 -3
  693. package/dist/utils/github.test.js +3 -3
  694. package/dist/utils/github.test.js.map +1 -1
  695. package/dist/utils/image.d.ts +26 -3
  696. package/dist/utils/image.d.ts.map +1 -1
  697. package/dist/utils/image.js +83 -14
  698. package/dist/utils/image.js.map +1 -1
  699. package/dist/utils/image.test.d.ts +2 -0
  700. package/dist/utils/image.test.d.ts.map +1 -0
  701. package/dist/utils/image.test.js +139 -0
  702. package/dist/utils/image.test.js.map +1 -0
  703. package/package.json +7 -6
@@ -8,7 +8,7 @@
8
8
  * POST /prompt → { text } ; runs AgentSession.prompt(text)
9
9
  * POST /cancel → aborts the in-flight run
10
10
  *
11
- * The agent spine (gg-ai → gg-agent → gg-core) and every tool are reused
11
+ * The agent spine (@prestyj/ai → @prestyj/agent → @prestyj/core) and every tool are reused
12
12
  * unchanged via AgentSession — this file is only a network seam.
13
13
  */
14
14
  import http from "node:http";
@@ -24,6 +24,7 @@ import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
24
24
  import { setStreamDiagnostic } from "@prestyj/agent";
25
25
  import { AgentSession } from "./core/agent-session.js";
26
26
  import { RunLifecycle } from "./core/run-lifecycle.js";
27
+ import { RunClaim } from "./core/run-claim.js";
27
28
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
28
29
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
29
30
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
@@ -33,20 +34,22 @@ import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
33
34
  import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
34
35
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
35
36
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
36
- import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
37
+ import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, getHistoryMessageVisibility, replayMessagesInOrder, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, } from "./core/session-history.js";
37
38
  import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
38
39
  import { AuthStorage } from "./core/auth-storage.js";
39
40
  import { cleanupToolOutputs } from "./tools/overflow.js";
40
41
  import { readCappedBody } from "./utils/http-body.js";
41
- import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
42
+ import { fetchSubscriptionUsage, SubscriptionUsageError, XIAOMI_CREDITS_KEY, dualAuthProvider, oauthStorageKey, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
42
43
  import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
43
44
  import { loginAnthropic } from "./core/oauth/anthropic.js";
44
45
  import { loginOpenAI } from "./core/oauth/openai.js";
45
46
  import { loginGemini } from "./core/oauth/gemini.js";
46
47
  import { loginKimi } from "./core/oauth/kimi.js";
47
- import { AUTH_PROVIDERS } from "./core/auth-providers.js";
48
+ import { loginXai } from "./core/oauth/xai.js";
49
+ import { AUTH_PROVIDERS, authPriorityNote, describeAuthMethods, } from "./core/auth-providers.js";
48
50
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
49
51
  import { SettingsManager } from "./core/settings-manager.js";
52
+ import { installPlugin, listInstalledPlugins, removePlugin, } from "./core/extensions/plugin-bundles.js";
50
53
  import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
51
54
  import { resolveStartOrFallback } from "./core/resolve-start.js";
52
55
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
@@ -59,18 +62,24 @@ import { discoverProjects } from "./core/project-discovery.js";
59
62
  import { listSidecarSessions } from "./app-sidecar-sessions.js";
60
63
  import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
61
64
  import { initLogger, log } from "./core/logger.js";
65
+ import { installTerminationHandlers } from "./core/shutdown.js";
62
66
  import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
63
67
  import { enrichProcessPath } from "./core/shell-path.js";
64
68
  import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
65
69
  import { startServeMode } from "./modes/serve-mode.js";
66
70
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
67
- import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
71
+ import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, createElicitationBridge, } from "./core/mcp/index.js";
68
72
  import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
69
73
  import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
70
74
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
71
75
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
72
76
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
73
77
  import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
78
+ const AUTOMATION_PROVENANCE = {
79
+ source: "runtime",
80
+ kind: "automation",
81
+ visibility: "hidden",
82
+ };
74
83
  const ALL_PROVIDERS = [
75
84
  // US
76
85
  "anthropic",
@@ -87,6 +96,12 @@ const ALL_PROVIDERS = [
87
96
  "sakana",
88
97
  "openrouter",
89
98
  ];
99
+ function stringArray(value) {
100
+ if (!Array.isArray(value))
101
+ return undefined;
102
+ const out = value.filter((v) => typeof v === "string" && v.trim().length > 0);
103
+ return out.length > 0 ? out : undefined;
104
+ }
90
105
  function appSettingsFile() {
91
106
  return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
92
107
  }
@@ -110,6 +125,8 @@ async function loadAppSettings() {
110
125
  projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
111
126
  autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
112
127
  nolanModels: raw.nolanModels && typeof raw.nolanModels === "object" ? raw.nolanModels : undefined,
128
+ projectRoots: stringArray(raw.projectRoots),
129
+ hiddenProjects: stringArray(raw.hiddenProjects),
113
130
  };
114
131
  }
115
132
  catch {
@@ -363,30 +380,6 @@ function detectHookKind(text) {
363
380
  return "regrounding";
364
381
  return null;
365
382
  }
366
- // Separator AgentSession.prompt() inserts between a command's prompt body and
367
- // the user's trailing args. Must stay in sync with the expansion there.
368
- const COMMAND_ARGS_SEP = "\n\n## User Instructions\n\n";
369
- /**
370
- * Reverse a prompt-template command's expansion. When a `/name` command runs,
371
- * the agent persists the FULL expanded prompt body as the user message — so on
372
- * resume the raw body would render instead of the short `/name` chip the user
373
- * saw live. Given the candidate commands (built-in + custom) and a restored
374
- * message body, recover the original `/name [args]` invocation. Returns null
375
- * when the text isn't a known command body (an ordinary user message).
376
- */
377
- function detectPromptCommand(text, candidates) {
378
- for (const c of candidates) {
379
- if (!c.prompt)
380
- continue;
381
- if (text === c.prompt)
382
- return `/${c.name}`;
383
- if (text.startsWith(c.prompt + COMMAND_ARGS_SEP)) {
384
- const args = text.slice(c.prompt.length + COMMAND_ARGS_SEP.length).trim();
385
- return args ? `/${c.name} ${args}` : `/${c.name}`;
386
- }
387
- }
388
- return null;
389
- }
390
383
  /** A short transport summary for display (URL for http/sse, command+args for stdio). */
391
384
  function mcpRowSummary(config) {
392
385
  if (config.url)
@@ -459,6 +452,8 @@ async function runJsonModeIfRequested() {
459
452
  model: { type: "string" },
460
453
  "max-turns": { type: "string" },
461
454
  "system-prompt": { type: "string" },
455
+ "agent-prompt": { type: "string" },
456
+ "agent-context": { type: "string" },
462
457
  tools: { type: "string" },
463
458
  "mcp-servers": { type: "string" },
464
459
  "prompt-cache-key": { type: "string" },
@@ -492,6 +487,8 @@ async function runJsonModeIfRequested() {
492
487
  model: values.model ?? "claude-opus-5",
493
488
  cwd: process.cwd(),
494
489
  systemPrompt: values["system-prompt"],
490
+ agentPrompt: values["agent-prompt"],
491
+ agentContext: values["agent-context"] === "none" ? "none" : undefined,
495
492
  maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
496
493
  allowedTools,
497
494
  allowedMcpServers,
@@ -595,9 +592,23 @@ async function main() {
595
592
  await auth.load();
596
593
  // Every window's session lives here as an in-process object, keyed by the id
597
594
  // the daemon hands back from POST /session. The Rust shell routes each proxy
598
- // request to its window's session via the `x-gg-session` header (and the
595
+ // request to its window's session via the `x-ez-session` header (and the
599
596
  // `?session=` query for the SSE /events stream).
600
597
  const sessions = new Map();
598
+ /**
599
+ * Fan one frame out to every window.
600
+ *
601
+ * For state that is genuinely global rather than per-session — `~/.ezcoder/auth.json`
602
+ * is shared by all windows, so connecting a provider in one must refresh the
603
+ * model picker in all of them. Mirrors the memory/jiwa/progress fan-outs.
604
+ */
605
+ const broadcastAll = (type, data) => {
606
+ for (const ctx of sessions.values())
607
+ ctx.broadcast(type, data);
608
+ };
609
+ // Providers currently mid-OAuth in some window. Daemon-level so two windows
610
+ // cannot race two browser flows for the same provider into one auth file.
611
+ const oauthInFlightProviders = new Set();
601
612
  const memoryStore = new MemoryStore({
602
613
  onChange: ({ memories }) => {
603
614
  for (const ctx of sessions.values()) {
@@ -621,6 +632,13 @@ async function main() {
621
632
  });
622
633
  const usageCache = new Map();
623
634
  const usageRequests = new Map();
635
+ // Last snapshot that actually carried windows, per provider. Replayed while a
636
+ // fetch is failing so the title meter never blinks out of existence. Bounded
637
+ // by USAGE_LAST_GOOD_MAX_AGE_MS — a provider that never recovers must stop
638
+ // reporting rather than freeze a percentage (and a long-past reset time) on
639
+ // screen forever. The 429 backoff alone runs to 24h, far past any usefulness.
640
+ const usageLastGood = new Map();
641
+ const USAGE_LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
624
642
  // 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
625
643
  // provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
626
644
  // the provider value so a malformed header can neither hammer the endpoint
@@ -633,8 +651,11 @@ async function main() {
633
651
  const displayName = provider === "anthropic" ? "Anthropic" : provider === "openai" ? "Codex" : "Kimi";
634
652
  // Kimi plan usage is tracked on the OAuth credential specifically — the
635
653
  // Moonshot platform API key is metered per-token, not per plan window.
636
- const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
654
+ const authKey = oauthStorageKey(provider) ?? provider;
637
655
  if (!(await auth.hasProviderAuth(authKey))) {
656
+ // Logged out: drop the replay cache so a later login on a DIFFERENT
657
+ // account can never inherit the previous one's numbers.
658
+ usageLastGood.delete(provider);
638
659
  return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
639
660
  }
640
661
  try {
@@ -651,7 +672,14 @@ async function main() {
651
672
  // A provider can revoke an access token before its stored expiry. Refresh
652
673
  // once on 401, matching inference auth recovery, then retry the usage call.
653
674
  if (error instanceof SubscriptionUsageError && error.status === 401) {
654
- credentials = await auth.resolveCredentials(authKey, { forceRefresh: true });
675
+ // Name the rejected token. This poller runs on a timer alongside live
676
+ // agent runs, so an unconditional refresh here would invalidate the
677
+ // token those runs are holding and fail them mid-turn — a background
678
+ // status check must never be able to log out the foreground.
679
+ credentials = await auth.resolveCredentials(authKey, {
680
+ forceRefresh: true,
681
+ rejectedToken: credentials.accessToken,
682
+ });
655
683
  const snapshot = {
656
684
  ...(await fetchSubscriptionUsage(provider, credentials)),
657
685
  connected: true,
@@ -678,6 +706,18 @@ async function main() {
678
706
  ...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
679
707
  });
680
708
  const connected = await auth.hasProviderAuth(authKey);
709
+ // Transient failures (notably the 429s these auxiliary quota endpoints
710
+ // hand out) must not blank the meter. Keep serving the last good
711
+ // snapshot, flagged `stale`, so the bar stays put instead of flickering
712
+ // out for the whole backoff window and back in on the next success.
713
+ // Past the max age it's dropped — no data beats confidently wrong data.
714
+ const lastGood = usageLastGood.get(provider);
715
+ if (lastGood && Date.now() - lastGood.fetchedAt >= USAGE_LAST_GOOD_MAX_AGE_MS) {
716
+ usageLastGood.delete(provider);
717
+ }
718
+ else if (connected && lastGood) {
719
+ return { ...lastGood, stale: true };
720
+ }
681
721
  return {
682
722
  provider,
683
723
  displayName,
@@ -699,6 +739,11 @@ async function main() {
699
739
  usageRequests.set(provider, request);
700
740
  try {
701
741
  const result = await request;
742
+ // Never re-store a replay — it would keep its original `fetchedAt`, but
743
+ // writing it back muddies the "last GOOD" contract for no gain.
744
+ if (result.connected && !result.error && !result.stale && result.windows.length > 0) {
745
+ usageLastGood.set(provider, result);
746
+ }
702
747
  // Anthropic can return utilization before it assigns reset timestamps
703
748
  // (notably before the account's first active request). Retry that partial
704
749
  // snapshot quickly; complete snapshots keep the normal one-minute cache.
@@ -719,10 +764,10 @@ async function main() {
719
764
  usageRequests.delete(provider);
720
765
  }
721
766
  }
722
- /** Resolve the target session id: the `x-gg-session` header, else a
767
+ /** Resolve the target session id: the `x-ez-session` header, else a
723
768
  * `?session=` query param (used by the SSE /events connection). */
724
769
  function sessionIdFromReq(req, url) {
725
- const header = req.headers["x-gg-session"];
770
+ const header = req.headers["x-ez-session"];
726
771
  if (typeof header === "string" && header.length > 0)
727
772
  return header;
728
773
  try {
@@ -740,7 +785,7 @@ async function main() {
740
785
  res.writeHead(204, {
741
786
  "access-control-allow-origin": "*",
742
787
  "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
743
- "access-control-allow-headers": "content-type, x-gg-session",
788
+ "access-control-allow-headers": "content-type, x-ez-session",
744
789
  });
745
790
  res.end();
746
791
  return;
@@ -766,7 +811,15 @@ async function main() {
766
811
  const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
767
812
  const id = randomUUID();
768
813
  try {
769
- const ctx = await createSession({ auth, paths, progress, memoryStore, jiwaStore }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
814
+ const ctx = await createSession({
815
+ auth,
816
+ paths,
817
+ progress,
818
+ memoryStore,
819
+ jiwaStore,
820
+ broadcastAll,
821
+ oauthInFlightProviders,
822
+ }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
770
823
  sessions.set(id, ctx);
771
824
  log("INFO", "app-sidecar", "session created", {
772
825
  id,
@@ -839,23 +892,28 @@ async function main() {
839
892
  log("INFO", "app-sidecar", "daemon listening", { port: String(addr.port), host });
840
893
  });
841
894
  const shellPid = process.ppid;
842
- let shuttingDown = false;
843
- async function shutdown() {
844
- if (shuttingDown)
845
- return;
846
- shuttingDown = true;
847
- clearInterval(parentWatch);
848
- // Radio playback is app-wide (one stream across all windows), so it stops
849
- // at the daemon level, not per session.
850
- stopRadio();
851
- // Close the ~/.ezcoder progress fs.watch handle (baseline #8 leak fix).
852
- progress.dispose();
853
- await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
854
- server.close();
855
- process.exit(0);
856
- }
857
- process.on("SIGINT", () => void shutdown());
858
- process.on("SIGTERM", () => void shutdown());
895
+ // Session teardown awaits MCP servers, LSP servers and third-party extension
896
+ // `deactivate()` hooks. Any can hang, so a deadline guarantees process exit.
897
+ const shutdown = installTerminationHandlers({
898
+ scope: "app-sidecar",
899
+ teardown: async () => {
900
+ clearInterval(parentWatch);
901
+ // Radio playback is app-wide (one stream across all windows).
902
+ stopRadio();
903
+ // Close the ~/.ezcoder progress fs.watch handle.
904
+ progress.dispose();
905
+ await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
906
+ server.close();
907
+ },
908
+ onTimeout: (timeoutMs) => {
909
+ // Radio is audible, so it must stop even when the rest is wedged.
910
+ stopRadio();
911
+ log("WARN", "app-sidecar", "daemon teardown hung; exiting on deadline", {
912
+ timeoutMs: String(timeoutMs),
913
+ sessions: String(sessions.size),
914
+ });
915
+ },
916
+ });
859
917
  process.once("exit", stopRadio);
860
918
  // Tauri can disappear without delivering a signal (force-quit, dev runner
861
919
  // teardown, crash). Detect reparenting or a dead shell so the daemon and its
@@ -871,7 +929,7 @@ async function main() {
871
929
  }
872
930
  }
873
931
  if (!parentAlive)
874
- void shutdown();
932
+ shutdown();
875
933
  }, 1_000);
876
934
  parentWatch.unref?.();
877
935
  }
@@ -1042,7 +1100,7 @@ async function createProgressManager(agentDir, broadcastAll) {
1042
1100
  * logger, PATH, shared auth file, and radio live at the daemon level.
1043
1101
  */
1044
1102
  async function createSession(deps, opts) {
1045
- const { auth, progress, memoryStore, jiwaStore } = deps;
1103
+ const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders } = deps;
1046
1104
  const paths = deps.paths;
1047
1105
  const mode = opts.mode;
1048
1106
  let chatAgent = opts.chatAgent;
@@ -1137,7 +1195,7 @@ async function createSession(deps, opts) {
1137
1195
  "Start it again and retry, or pick another model.");
1138
1196
  }
1139
1197
  // Turn any thrown value into the same clear headline/message/guidance shape
1140
- // the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
1198
+ // the TUI shows (see @prestyj/ai's formatError) instead of a bare `err.message`, log
1141
1199
  // the full detail, and broadcast it under `type` ("error" or "nolan_error").
1142
1200
  // Without this the webview only ever saw a raw provider string like
1143
1201
  // `400 {"code":"400",...}` with no "is this me or them / when does it reset"
@@ -1178,6 +1236,17 @@ async function createSession(deps, opts) {
1178
1236
  })
1179
1237
  .catch(() => { });
1180
1238
  }
1239
+ // ── MCP elicitation bridge ─────────────────────────────────
1240
+ // An MCP server can ask for user input in the middle of a tool call. The
1241
+ // bridge parks the promise; we broadcast the prompt over SSE and resolve it
1242
+ // when the webview POSTs /mcp/elicit/:id.
1243
+ const elicitations = createElicitationBridge({
1244
+ broadcast: (prompt) => broadcast("mcp_elicit", prompt),
1245
+ onTimeout: (prompt) => log("WARN", "app-sidecar", "MCP elicitation timed out", {
1246
+ id: prompt.id,
1247
+ server: prompt.server,
1248
+ }),
1249
+ });
1181
1250
  // The session file path to resume (passed by the daemon's POST /session);
1182
1251
  // empty/unset starts a fresh session.
1183
1252
  const resumeSessionPath = opts.sessionPath;
@@ -1191,6 +1260,7 @@ async function createSession(deps, opts) {
1191
1260
  signal: abort.signal,
1192
1261
  // Keep MCP startup off the readiness path in both modes.
1193
1262
  backgroundMcpConnect: true,
1263
+ onMcpElicit: elicitations.onElicit,
1194
1264
  // Keep restore-time auto-compaction off the readiness path too: its summary
1195
1265
  // LLM call (30s timeout) used to freeze waitForReady — and with it the whole
1196
1266
  // window (project picker, session list) — whenever a resumed session was
@@ -1513,6 +1583,10 @@ async function createSession(deps, opts) {
1513
1583
  recordApprovedPlanMarkers(d.text);
1514
1584
  });
1515
1585
  session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
1586
+ // The agent consumed queued steering at a turn boundary. Re-broadcast as the
1587
+ // usual `queued` depth update so the webview drops the pending affordance the
1588
+ // moment the message lands in the loop, not at run_end.
1589
+ session.eventBus.on("queue_drained", (d) => broadcast("queued", { count: d.count, messages: session.listQueuedMessages() }));
1516
1590
  session.eventBus.on("tool_call_start", (d) => {
1517
1591
  toolCallNames.set(d.toolCallId, d.name);
1518
1592
  broadcast("tool_call_start", d);
@@ -1564,10 +1638,29 @@ async function createSession(deps, opts) {
1564
1638
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1565
1639
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
1566
1640
  let running = false;
1641
+ // Closes the window between `/prompt` deciding to start a run and `runAgent`
1642
+ // flipping `running` — that stretch awaits, so Node yields inside it. See
1643
+ // RunClaim.
1644
+ const runClaim = new RunClaim();
1567
1645
  const runLifecycle = new RunLifecycle((runState) => {
1568
1646
  running = runState !== "idle";
1569
1647
  if (runState === "cancelling")
1570
1648
  broadcast("run_cancelling", { runState });
1649
+ },
1650
+ // Durable run journal. Fire-and-forget on purpose: an unwritten journal
1651
+ // entry is a missed crash hint, while a journal write that throws inside
1652
+ // begin()/settle() would break run ownership itself.
1653
+ {
1654
+ started: (generation) => void session.persistRunStarted(generation).catch((err) => {
1655
+ log("WARN", "app-sidecar", "failed to journal run start", {
1656
+ error: err instanceof Error ? err.message : String(err),
1657
+ });
1658
+ }),
1659
+ finished: (generation, outcome) => void session.persistRunFinished(generation, outcome).catch((err) => {
1660
+ log("WARN", "app-sidecar", "failed to journal run finish", {
1661
+ error: err instanceof Error ? err.message : String(err),
1662
+ });
1663
+ }),
1571
1664
  });
1572
1665
  const cancelledRunEndGenerations = new Set();
1573
1666
  let pendingCancelDrain = null;
@@ -1643,7 +1736,7 @@ async function createSession(deps, opts) {
1643
1736
  // with `@Nolan …`; he reads EZ Coder's transcript (one-way — EZ Coder never sees
1644
1737
  // Nolan's) and hands back runnable prompts + mentorship. Created lazily on the
1645
1738
  // first `@Nolan` so windows that never use Nolan pay zero cost. His events ride the
1646
- // SAME SSE stream with `ken_`-prefixed types, routed to the Nolan bubble.
1739
+ // SAME SSE stream with `nolan_`-prefixed types, routed to the Nolan bubble.
1647
1740
  let nolanSession = null;
1648
1741
  let nolanAbort = new AbortController();
1649
1742
  let nolanRunning = false;
@@ -1673,7 +1766,7 @@ async function createSession(deps, opts) {
1673
1766
  return { provider: st.provider, model: st.model };
1674
1767
  }
1675
1768
  /** Footer payload: Nolan's effective model + whether it's a pin. Merged into
1676
- * /state, the SSE ready frame, and every ken_model_change broadcast. */
1769
+ * /state, the SSE ready frame, and every nolan_model_change broadcast. */
1677
1770
  function nolanStatePayload() {
1678
1771
  const st = session.getState();
1679
1772
  return effectiveNolanModel(nolanModelOverride, { provider: st.provider, model: st.model });
@@ -1689,13 +1782,13 @@ async function createSession(deps, opts) {
1689
1782
  if (st.provider === provider && st.model === model)
1690
1783
  return;
1691
1784
  await nolanSession.switchModel(provider, model);
1692
- log("INFO", "app-sidecar", "ken session model synced", { provider, model });
1785
+ log("INFO", "app-sidecar", "nolan session model synced", { provider, model });
1693
1786
  }
1694
1787
  async function ensureNolanSession() {
1695
1788
  if (nolanSession)
1696
1789
  return nolanSession;
1697
1790
  const target = nolanCurrentModel();
1698
- const ken = new AgentSession({
1791
+ const nolanAgent = new AgentSession({
1699
1792
  provider: target.provider,
1700
1793
  model: target.model,
1701
1794
  cwd,
@@ -1704,38 +1797,41 @@ async function createSession(deps, opts) {
1704
1797
  allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
1705
1798
  transient: true,
1706
1799
  signal: nolanAbort.signal,
1707
- // Nolan's bursty, spread-out turns (chat) outlast the default 5-min cache
1800
+ // Nolan belongs to this window, so route MCP elicitation prompts there.
1801
+ // Keeping daemon sessions interactive also lets them share one pooled MCP connection.
1802
+ onMcpElicit: elicitations.onElicit,
1803
+ // Nolan's bursty, spread-out turns outlast the default cache
1708
1804
  // TTL regardless of the user's global speedProfile pick.
1709
1805
  forceLongCacheRetention: true,
1710
1806
  });
1711
- await ken.initialize();
1712
- // Bridge Nolan's bus to the shared SSE fan-out with ken_-prefixed types so the
1807
+ await nolanAgent.initialize();
1808
+ // Bridge Nolan's bus to the shared SSE fan-out with nolan_-prefixed types so the
1713
1809
  // webview routes them to the Nolan bubble, never EZ Coder's.
1714
- ken.eventBus.on("text_delta", (d) => broadcast("nolan_text_delta", d));
1715
- ken.eventBus.on("thinking_delta", (d) => broadcast("nolan_thinking_delta", d));
1716
- ken.eventBus.on("tool_call_start", (d) => {
1810
+ nolanAgent.eventBus.on("text_delta", (d) => broadcast("nolan_text_delta", d));
1811
+ nolanAgent.eventBus.on("thinking_delta", (d) => broadcast("nolan_thinking_delta", d));
1812
+ nolanAgent.eventBus.on("tool_call_start", (d) => {
1717
1813
  nolanToolCallNames.set(d.toolCallId, d.name);
1718
1814
  broadcast("nolan_tool_call_start", d);
1719
1815
  });
1720
- ken.eventBus.on("tool_call_update", (d) => broadcast("nolan_tool_call_update", d));
1721
- ken.eventBus.on("tool_call_end", (d) => {
1816
+ nolanAgent.eventBus.on("tool_call_update", (d) => broadcast("nolan_tool_call_update", d));
1817
+ nolanAgent.eventBus.on("tool_call_end", (d) => {
1722
1818
  nolanToolCallNames.delete(d.toolCallId);
1723
1819
  broadcast("nolan_tool_call_end", d);
1724
1820
  });
1725
1821
  // Native server tools (Anthropic web_search) stream text both before AND
1726
1822
  // after them in the same turn; forward so the webview can break the bubble
1727
1823
  // (otherwise "...work.Local tools..." glues together). Mirrors the build bus.
1728
- ken.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
1729
- ken.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
1730
- ken.eventBus.on("error", (d) => {
1731
- broadcastError("nolan_error", "ken error", d.error);
1824
+ nolanAgent.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
1825
+ nolanAgent.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
1826
+ nolanAgent.eventBus.on("error", (d) => {
1827
+ broadcastError("nolan_error", "nolan error", d.error);
1732
1828
  });
1733
- nolanSession = ken;
1734
- log("INFO", "app-sidecar", "ken session ready", {
1829
+ nolanSession = nolanAgent;
1830
+ log("INFO", "app-sidecar", "nolan session ready", {
1735
1831
  provider: target.provider,
1736
1832
  model: target.model,
1737
1833
  });
1738
- return ken;
1834
+ return nolanAgent;
1739
1835
  }
1740
1836
  // ── Autopilot Nolan (auto-reviewer) ──────────────────────────
1741
1837
  // A THIRD read-only AgentSession, separate from chat Nolan. In autopilot mode
@@ -1758,13 +1854,13 @@ async function createSession(deps, opts) {
1758
1854
  if (st.provider === provider && st.model === model)
1759
1855
  return;
1760
1856
  await nolanAutoSession.switchModel(provider, model);
1761
- log("INFO", "app-sidecar", "ken autopilot session model synced", { provider, model });
1857
+ log("INFO", "app-sidecar", "nolan autopilot session model synced", { provider, model });
1762
1858
  }
1763
1859
  async function ensureNolanAutoSession() {
1764
1860
  if (nolanAutoSession)
1765
1861
  return nolanAutoSession;
1766
1862
  const target = nolanCurrentModel();
1767
- const ken = new AgentSession({
1863
+ const nolanAgent = new AgentSession({
1768
1864
  provider: target.provider,
1769
1865
  model: target.model,
1770
1866
  cwd,
@@ -1773,26 +1869,32 @@ async function createSession(deps, opts) {
1773
1869
  allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
1774
1870
  transient: true,
1775
1871
  signal: nolanAutoAbort.signal,
1872
+ // Route this window's MCP elicitation prompts through the shared bridge.
1873
+ onMcpElicit: elicitations.onElicit,
1776
1874
  // Autopilot review rounds routinely span the injected EZ Coder run
1777
1875
  // (often >5 min) regardless of the user's global speedProfile pick.
1778
1876
  forceLongCacheRetention: true,
1779
1877
  });
1780
1878
  // Nolan is already the independent autopilot reviewer; recursively running
1781
1879
  // his own Ideal self-review adds latency and can corrupt the verdict shape.
1782
- ken.setIdealReviewSuppressed(true);
1783
- await ken.initialize();
1880
+ nolanAgent.setIdealReviewSuppressed(true);
1881
+ await nolanAgent.initialize();
1784
1882
  // Deliberately no bus bridge: the review is silent. Errors surface via the
1785
1883
  // runAutopilotReview try/catch as autopilot_error frames.
1786
- nolanAutoSession = ken;
1787
- log("INFO", "app-sidecar", "ken autopilot session ready", {
1884
+ nolanAutoSession = nolanAgent;
1885
+ log("INFO", "app-sidecar", "nolan autopilot session ready", {
1788
1886
  provider: target.provider,
1789
1887
  model: target.model,
1790
1888
  });
1791
- return ken;
1889
+ return nolanAgent;
1792
1890
  }
1793
1891
  function abortOwnedWork() {
1794
1892
  cancelGeneration++;
1795
1893
  abort.abort();
1894
+ // An MCP tool call parked on user input is not cancelled by the signal —
1895
+ // the promise lives in the bridge. Release it, or the aborted turn's tool
1896
+ // call never returns.
1897
+ elicitations.cancelAll();
1796
1898
  // Stop a run-all sweep and every async child through AgentSession's signal.
1797
1899
  taskRunAll = false;
1798
1900
  autopilotCancelled = true;
@@ -1804,9 +1906,9 @@ async function createSession(deps, opts) {
1804
1906
  nolanAutoAbort = new AbortController();
1805
1907
  nolanAutoSession?.setSignal(nolanAutoAbort.signal);
1806
1908
  }
1807
- function finishOwnedGeneration(generation, emitCancelledFallback) {
1909
+ function finishOwnedGeneration(generation, emitCancelledFallback, outcome = "completed") {
1808
1910
  const cancelled = runLifecycle.isCancellationRequested(generation);
1809
- const settlement = runLifecycle.settle(generation);
1911
+ const settlement = runLifecycle.settle(generation, outcome);
1810
1912
  if (!settlement.settled)
1811
1913
  return cancelled;
1812
1914
  // A replacement signal is safe only after the provider-backed owner settled.
@@ -1884,8 +1986,9 @@ async function createSession(deps, opts) {
1884
1986
  });
1885
1987
  }
1886
1988
  }
1887
- if (ownsGeneration)
1888
- finishOwnedGeneration(generation, false);
1989
+ if (ownsGeneration) {
1990
+ finishOwnedGeneration(generation, false, runSucceeded ? "completed" : "failed");
1991
+ }
1889
1992
  // A cancelled injected run is still owned by the surrounding autopilot
1890
1993
  // cycle; its outer finalizer emits the one terminal cancelled run_end.
1891
1994
  if (!(cancelled && !ownsGeneration)) {
@@ -1904,7 +2007,10 @@ async function createSession(deps, opts) {
1904
2007
  // completed ones so they drop out of the Tasks modal automatically (users
1905
2008
  // never have to delete finished tasks by hand).
1906
2009
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
1907
- broadcast("queued", { count: session.getQueuedCount() });
2010
+ broadcast("queued", {
2011
+ count: session.getQueuedCount(),
2012
+ messages: session.listQueuedMessages(),
2013
+ });
1908
2014
  broadcast("extras", footerExtras());
1909
2015
  }
1910
2016
  }
@@ -1918,7 +2024,7 @@ async function createSession(deps, opts) {
1918
2024
  autopilotReviewing = true;
1919
2025
  broadcast("autopilot_review_start", {});
1920
2026
  try {
1921
- const ken = await ensureNolanAutoSession();
2027
+ const nolanAgent = await ensureNolanAutoSession();
1922
2028
  const digest = buildNolanAutopilotContext({
1923
2029
  cwd,
1924
2030
  gitBranch,
@@ -1927,8 +2033,8 @@ async function createSession(deps, opts) {
1927
2033
  injectedPrompts: [...injectedAutopilotPrompts],
1928
2034
  workflowCommands: await loadWorkflowCommandSpecs(),
1929
2035
  });
1930
- await ken.prompt(digest);
1931
- return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
2036
+ await nolanAgent.prompt(digest);
2037
+ return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
1932
2038
  }
1933
2039
  catch (err) {
1934
2040
  if (!autopilotCancelled)
@@ -1958,7 +2064,7 @@ async function createSession(deps, opts) {
1958
2064
  autopilotReviewing = true;
1959
2065
  broadcast("autopilot_review_start", {});
1960
2066
  try {
1961
- const ken = await ensureNolanAutoSession();
2067
+ const nolanAgent = await ensureNolanAutoSession();
1962
2068
  // Re-read the plan file (the run may have revised it in place); fall
1963
2069
  // back to the content captured at exit_plan time.
1964
2070
  const planContent = await fs.readFile(planPath, "utf-8").catch(() => pendingPlanContent);
@@ -1971,10 +2077,10 @@ async function createSession(deps, opts) {
1971
2077
  workflowCommands: await loadWorkflowCommandSpecs(),
1972
2078
  planContent,
1973
2079
  });
1974
- await ken.prompt(digest);
2080
+ await nolanAgent.prompt(digest);
1975
2081
  if (autopilotCancelled || planGeneration !== genAtStart)
1976
2082
  return null;
1977
- return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
2083
+ return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
1978
2084
  }
1979
2085
  catch (err) {
1980
2086
  // User action mid-review (manual Accept aborts the nolanAuto run): drop
@@ -2000,7 +2106,7 @@ async function createSession(deps, opts) {
2000
2106
  const IMPLEMENT_PLAN_PROMPT = "The plan has been approved. Implement it now, following each step in order.";
2001
2107
  // Drive the review→prompt→review loop for one finished user turn. Only ever
2002
2108
  // called after shouldStartAutopilotCycle approves the turn (POST /prompt or
2003
- // the stranded-queue drain) — never from the task runner, resume, /ken, or
2109
+ // the stranded-queue drain) — never from the task runner, resume, /nolan, or
2004
2110
  // error paths, so there's no recursion and no guard tangle. The loop's
2005
2111
  // control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
2006
2112
  // every exit path is unit-tested; this only wires the real dependencies.
@@ -2064,7 +2170,7 @@ async function createSession(deps, opts) {
2064
2170
  // label stays the clean prompt.
2065
2171
  const framed = frameAutopilotInjection(IMPLEMENT_PLAN_PROMPT);
2066
2172
  injectedAutopilotPrompts.push(framed);
2067
- return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed));
2173
+ return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed, AUTOMATION_PROVENANCE));
2068
2174
  },
2069
2175
  // Lean context per user turn: wipe prior review history so each new
2070
2176
  // turn starts cheap, while within this cycle the few review messages
@@ -2093,19 +2199,21 @@ async function createSession(deps, opts) {
2093
2199
  },
2094
2200
  // Autopilot-injected run: EZ Coder receives the framed prompt (no human
2095
2201
  // is watching this turn) while run_start keeps the clean label.
2096
- runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body))),
2202
+ runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body), AUTOMATION_PROVENANCE)),
2097
2203
  emit: (event) => {
2098
2204
  // Persist the terminal verdict marker so a resumed session renders the
2099
2205
  // same Nolan bubble the live run showed instead of dropping it or
2100
2206
  // falling back to the raw verdict text (e.g. ALL_CLEAR).
2101
2207
  if (event.type === "autopilot_done") {
2102
2208
  // Broadcast the SAME copySeed the persisted marker will produce on
2103
- // resume, so the live all-clear wording matches the resumed one
2104
- // (computed before persist — same synchronous message count).
2209
+ // resume, so the live all-clear wording matches the resumed one.
2210
+ // Must use the PERSISTED count — that's what persistAutopilotMarker
2211
+ // anchors against, and it trails the in-memory list after a run
2212
+ // whose messages never made it to disk.
2105
2213
  const seed = autopilotMarkerCopySeed({
2106
2214
  version: 1,
2107
2215
  phase: "done",
2108
- afterMessageCount: session.getMessages().filter((m) => m.role !== "system").length,
2216
+ afterMessageCount: session.getPersistedTranscriptCount(),
2109
2217
  });
2110
2218
  broadcast(event.type, { ...event.data, copySeed: seed });
2111
2219
  void session.persistAutopilotMarker("done");
@@ -2150,7 +2258,10 @@ async function createSession(deps, opts) {
2150
2258
  const next = session.takeNextQueuedMessage();
2151
2259
  if (!next)
2152
2260
  return;
2153
- broadcast("queued", { count: session.getQueuedCount() });
2261
+ broadcast("queued", {
2262
+ count: session.getQueuedCount(),
2263
+ messages: session.listQueuedMessages(),
2264
+ });
2154
2265
  if (!next.text.trim() && next.attachments.length === 0)
2155
2266
  continue;
2156
2267
  // A queued message draining as a fresh turn supersedes any pending
@@ -2236,7 +2347,7 @@ async function createSession(deps, opts) {
2236
2347
  const shortId = task.id.slice(0, 8);
2237
2348
  const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2238
2349
  `tasks({ action: "done", id: "${shortId}" })`;
2239
- await runAgent(task.title, () => session.prompt(task.prompt + completionHint));
2350
+ await runAgent(task.title, () => session.prompt(task.prompt + completionHint, AUTOMATION_PROVENANCE));
2240
2351
  // The agent typically marks the task done via the tasks tool during the run;
2241
2352
  // prune completed tasks and push the refreshed list so the modal drops them.
2242
2353
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
@@ -2279,10 +2390,52 @@ async function createSession(deps, opts) {
2279
2390
  // Native (Rust) writes to auth.json bypass this daemon's cache; re-read so
2280
2391
  // the reported connection state matches what's actually on disk.
2281
2392
  await auth.reload();
2282
- const providers = await Promise.all(AUTH_PROVIDERS.map(async (p) => ({
2283
- ...p,
2284
- connected: await auth.hasProviderAuth(p.value),
2285
- })));
2393
+ const providers = await Promise.all(AUTH_PROVIDERS.map(async (p) => {
2394
+ const dual = dualAuthProvider(p.value);
2395
+ // Which methods hold a credential right now. Dual-auth providers can hold
2396
+ // both at once, and the app needs them separately: one "connected" bit
2397
+ // cannot express "OAuth signed in, key also on file as backup", nor offer
2398
+ // a per-method disconnect.
2399
+ const oauthKey = dual?.oauthKey;
2400
+ const hasOAuth = oauthKey ? await auth.hasCredentials(oauthKey) : false;
2401
+ const apiKeyKeys = p.apiKeyVariants?.map((v) => v.key) ?? [p.value];
2402
+ const hasApiKey = p.methods.includes("apikey")
2403
+ ? (await Promise.all(apiKeyKeys.map((k) => auth.hasCredentials(k)))).some(Boolean)
2404
+ : false;
2405
+ const connectedMethods = [];
2406
+ // OAuth-only providers (Anthropic/OpenAI/Gemini) store under the provider
2407
+ // id itself, so `hasProviderAuth` is what proves their OAuth connection.
2408
+ if (p.methods.includes("oauth") &&
2409
+ (hasOAuth || (!dual && (await auth.hasCredentials(p.value)))))
2410
+ connectedMethods.push("oauth");
2411
+ if (hasApiKey)
2412
+ connectedMethods.push("apikey");
2413
+ // Which one a request would actually use, mirroring AuthStorage's
2414
+ // resolution: OAuth wins unless its usage window is exhausted AND a key
2415
+ // is configured to cover it.
2416
+ const exhaustedUntil = oauthKey
2417
+ ? ((await auth.getCredentials(oauthKey))?.usageExhaustedUntil ?? 0)
2418
+ : 0;
2419
+ const oauthSidelined = hasOAuth && Date.now() < exhaustedUntil && hasApiKey;
2420
+ const activeMethod = connectedMethods.includes("oauth")
2421
+ ? oauthSidelined
2422
+ ? "apikey"
2423
+ : "oauth"
2424
+ : connectedMethods[0];
2425
+ // `methodDetails` is the server-side lookup table behind
2426
+ // `methodGuidance` — shipping both would duplicate every string on the
2427
+ // wire for no consumer.
2428
+ const { methodDetails: _table, ...wireMeta } = p;
2429
+ return {
2430
+ ...wireMeta,
2431
+ connected: await auth.hasProviderAuth(p.value),
2432
+ connectedMethods,
2433
+ ...(activeMethod ? { activeMethod } : {}),
2434
+ ...(oauthSidelined ? { oauthExhaustedUntil: exhaustedUntil } : {}),
2435
+ ...(authPriorityNote(p.value) ? { priorityNote: authPriorityNote(p.value) } : {}),
2436
+ methodGuidance: describeAuthMethods(p.value),
2437
+ };
2438
+ }));
2286
2439
  return { providers };
2287
2440
  }
2288
2441
  // Background tasks have no event source (the bash tool just spawns them), so
@@ -2469,6 +2622,45 @@ async function createSession(deps, opts) {
2469
2622
  });
2470
2623
  return;
2471
2624
  }
2625
+ if (method === "GET" && url === "/plugins") {
2626
+ void listInstalledPlugins(paths.extensionsDir)
2627
+ .then((plugins) => json(res, 200, { plugins }))
2628
+ .catch((error) => {
2629
+ captureSidecarError(error, "app-sidecar.plugins.list");
2630
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2631
+ });
2632
+ return;
2633
+ }
2634
+ if (method === "POST" && url === "/plugins/install") {
2635
+ void readBody(req, res).then(async (raw) => {
2636
+ if (raw === null)
2637
+ return;
2638
+ try {
2639
+ const bundlePath = JSON.parse(raw).bundlePath;
2640
+ if (typeof bundlePath !== "string" || !path.isAbsolute(bundlePath)) {
2641
+ json(res, 400, { error: "bundlePath must be an absolute path" });
2642
+ return;
2643
+ }
2644
+ const plugin = await installPlugin(bundlePath, paths.extensionsDir);
2645
+ json(res, 200, { plugin, restartRequired: true });
2646
+ }
2647
+ catch (error) {
2648
+ captureSidecarError(error, "app-sidecar.plugins.install");
2649
+ json(res, 400, { error: error instanceof Error ? error.message : String(error) });
2650
+ }
2651
+ });
2652
+ return;
2653
+ }
2654
+ if (method === "DELETE" && url.startsWith("/plugins/")) {
2655
+ const pluginId = decodeURIComponent(url.slice("/plugins/".length));
2656
+ void removePlugin(pluginId, paths.extensionsDir)
2657
+ .then(() => json(res, 200, { removed: pluginId, restartRequired: true }))
2658
+ .catch((error) => {
2659
+ captureSidecarError(error, "app-sidecar.plugins.remove");
2660
+ json(res, 400, { error: error instanceof Error ? error.message : String(error) });
2661
+ });
2662
+ return;
2663
+ }
2472
2664
  if (method === "GET" && url === "/settings") {
2473
2665
  // `configured` is true only when the user explicitly saved a projects root
2474
2666
  // (the ezcoder-app.json file exists with a value) — not when we fall back to the
@@ -2555,9 +2747,53 @@ async function createSession(deps, opts) {
2555
2747
  });
2556
2748
  return;
2557
2749
  }
2750
+ // Dismiss a project from the picker (or restore it with hidden:false). The
2751
+ // path is kept rather than the row: sessions in scratch dirs keep
2752
+ // re-surfacing, so the user's decision has to outlive any one scan.
2753
+ if (method === "POST" && url === "/projects/hidden") {
2754
+ void readBody(req, res).then(async (raw) => {
2755
+ if (raw === null)
2756
+ return;
2757
+ let body;
2758
+ try {
2759
+ body = JSON.parse(raw);
2760
+ }
2761
+ catch {
2762
+ json(res, 400, { error: "invalid JSON body" });
2763
+ return;
2764
+ }
2765
+ const target = body.path?.trim();
2766
+ if (!target) {
2767
+ json(res, 400, { error: "path is required" });
2768
+ return;
2769
+ }
2770
+ const key = path.resolve(target);
2771
+ try {
2772
+ const settings = await loadAppSettings();
2773
+ const current = new Set((settings.hiddenProjects ?? []).map((p) => path.resolve(p)));
2774
+ if (body.hidden === false)
2775
+ current.delete(key);
2776
+ else
2777
+ current.add(key);
2778
+ settings.hiddenProjects = current.size > 0 ? Array.from(current) : undefined;
2779
+ await saveAppSettings(settings);
2780
+ json(res, 200, { hidden: Array.from(current) });
2781
+ }
2782
+ catch (err) {
2783
+ captureSidecarError(err, "app-sidecar.projects.hidden");
2784
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
2785
+ }
2786
+ });
2787
+ return;
2788
+ }
2558
2789
  if (method === "GET" && url === "/projects") {
2559
- // Scan ezcoder + Claude Code + Codex session stores for known projects.
2560
- void discoverProjects()
2790
+ // Session stores plus configured project folders, including unopened projects.
2791
+ void loadAppSettings()
2792
+ .then(({ projectsRoot, projectRoots, hiddenProjects }) => discoverProjects({
2793
+ projectsRoot,
2794
+ extraRoots: projectRoots,
2795
+ hiddenPaths: hiddenProjects,
2796
+ }))
2561
2797
  .then((projects) => json(res, 200, { projects }))
2562
2798
  .catch((err) => {
2563
2799
  captureSidecarError(err, "app-sidecar.projects.discover");
@@ -2667,7 +2903,7 @@ async function createSession(deps, opts) {
2667
2903
  // Nolan (mentor) turns to interleave: group by the non-system message count
2668
2904
  // they were recorded after, so each lands right after that message. A
2669
2905
  // turn becomes two wire rows: the `@Nolan` question (user) + Nolan's reply
2670
- // (assistant), both flagged `ken` so the webview tints them.
2906
+ // (assistant), both flagged `nolan` so the webview tints them.
2671
2907
  // Deduped; stale anchors are clamped to the last message (Nolan turns
2672
2908
  // carry real conversation, so they render at the end instead of
2673
2909
  // vanishing).
@@ -2683,16 +2919,16 @@ async function createSession(deps, opts) {
2683
2919
  return;
2684
2920
  nolanByCount.delete(count);
2685
2921
  for (const turn of turns) {
2686
- history.push({ role: "user", text: `@Nolan ${turn.question}`, ken: true });
2687
- history.push({ role: "assistant", text: turn.reply, ken: true });
2922
+ history.push({ role: "user", text: `@Nolan ${turn.question}`, nolan: true });
2923
+ history.push({ role: "assistant", text: turn.reply, nolan: true });
2688
2924
  }
2689
2925
  };
2690
2926
  // Autopilot verdict markers to interleave, same anchor scheme as Nolan
2691
2927
  // turns — each becomes a single assistant row the webview renders
2692
2928
  // exactly like the live `autopilot` item (never a raw verdict string).
2693
- // Compact/continuation rewrites can carry old markers whose original
2694
- // afterMessageCount is beyond the restored message list; dropping those
2695
- // prevents stale all-clear bubbles from bunching at the bottom on resume.
2929
+ // Normalization pulls anchors left over from an unrebased compaction
2930
+ // back to where the marker was actually written, so stale all-clear
2931
+ // bubbles no longer bunch at the bottom of a reopened session.
2696
2932
  const restoredMessageCount = messages.filter((m) => m.role !== "system").length;
2697
2933
  const autopilotByCount = new Map();
2698
2934
  for (const marker of normalizeAutopilotMarkersForHistory(session.getAutopilotMarkers(), restoredMessageCount)) {
@@ -2776,6 +3012,21 @@ async function createSession(deps, opts) {
2776
3012
  },
2777
3013
  });
2778
3014
  }
3015
+ else if (marker.kind === "interrupted_run") {
3016
+ // Rendered as an error row: the run's tools already changed the
3017
+ // repo, so the user needs to see it and decide what to do. We
3018
+ // never replay it — that would duplicate those changes.
3019
+ history.push({
3020
+ role: "assistant",
3021
+ text: "",
3022
+ error: {
3023
+ scope: "interrupted_run",
3024
+ headline: "A run was interrupted",
3025
+ message: "EZ Coder stopped mid-run, so this turn is incomplete. Any files its tools already changed are still on disk.",
3026
+ guidance: "Review the working tree, then re-send the request if you still want it.",
3027
+ },
3028
+ });
3029
+ }
2779
3030
  }
2780
3031
  };
2781
3032
  let nonSystemCount = 0;
@@ -2784,10 +3035,11 @@ async function createSession(deps, opts) {
2784
3035
  flushNolan(0);
2785
3036
  flushAutopilot(0);
2786
3037
  flushAppMarkers(0);
2787
- for (const msg of messages) {
2788
- if (msg.role === "system")
2789
- continue;
2790
- nonSystemCount++;
3038
+ await replayMessagesInOrder(messages, async (msg, count) => {
3039
+ nonSystemCount = count;
3040
+ const visibility = getHistoryMessageVisibility(msg);
3041
+ if (visibility === "hidden")
3042
+ return;
2791
3043
  if (msg.role === "tool") {
2792
3044
  // Tool result messages: check for ImageContent blocks (screenshots,
2793
3045
  // generated images) and emit a toolImages entry.
@@ -2831,20 +3083,39 @@ async function createSession(deps, opts) {
2831
3083
  });
2832
3084
  }
2833
3085
  }
2834
- continue;
3086
+ return;
2835
3087
  }
2836
3088
  // User or assistant message — text/hook/command/compacted extraction,
2837
3089
  // plus sub-agent group detection for assistant tool_calls.
2838
3090
  if (msg.role === "user") {
2839
3091
  // Rebuild the live bubble: strip the steering wrapper, drop
2840
3092
  // attachment/file notes the model saw but the bubble never showed.
2841
- const restored = restoreUserRow(msg.content);
3093
+ const restored = restoreUserRow(msg.content, msg.provenance);
2842
3094
  const text = restored.text;
2843
- const hook = detectHookKind(text);
2844
- const compacted = !hook && text.startsWith("[Previous conversation summary]");
2845
- const command = !hook && !compacted ? detectPromptCommand(text, commandCandidates) : null;
2846
- if (text.trim() || restored.images.length > 0) {
2847
- const hint = userHintByCount.get(nonSystemCount);
3095
+ const hook = msg.provenance ? null : detectHookKind(text);
3096
+ const compacted = visibility === "summary" ||
3097
+ (!msg.provenance && !hook && text.startsWith("[Previous conversation summary]"));
3098
+ const hint = userHintByCount.get(nonSystemCount);
3099
+ // The typed invocation persisted alongside the prompt is
3100
+ // authoritative. Reversing the expanded body only works while the
3101
+ // template is byte-identical, and templates drift (edited
3102
+ // `.ezcoder/commands/*.md`, reworded built-ins, app-vs-CLI phrasing) —
3103
+ // after which the resumed session dumped the raw multi-KB body
3104
+ // instead of the `/name` chip. Older sessions have no hint, so the
3105
+ // body match stays as the fallback.
3106
+ const command = !hook && !compacted
3107
+ ? resolveRestoredCommand(typeof hint?.command === "string" ? hint.command : null, text, commandCandidates)
3108
+ : null;
3109
+ // Autopilot injected this turn — live showed only the Nolan-tinted
3110
+ // marker for it, never a user bubble. Emitting one here would print
3111
+ // the injected instruction a second time, unstyled.
3112
+ //
3113
+ // Pushed background-status updates are skipped for the same reason:
3114
+ // the live run rendered no bubble for them, so showing them here
3115
+ // would fill a reopened session with machine-facing status lines
3116
+ // the user never saw while working.
3117
+ if ((!msg.provenance || (!restored.autopilotInjected && !restored.notification)) &&
3118
+ (text.trim() || restored.images.length > 0)) {
2848
3119
  history.push({
2849
3120
  role: "user",
2850
3121
  text: command ?? text,
@@ -2907,12 +3178,13 @@ async function createSession(deps, opts) {
2907
3178
  });
2908
3179
  }
2909
3180
  }
2910
- // Interleave any Nolan turns / autopilot / app markers recorded right
2911
- // after this message.
2912
- flushNolan(nonSystemCount);
2913
- flushAutopilot(nonSystemCount);
2914
- flushAppMarkers(nonSystemCount);
2915
- }
3181
+ }, (count) => {
3182
+ // Markers flush after every physical message, including tools and
3183
+ // provenance-hidden runtime context.
3184
+ flushNolan(count);
3185
+ flushAutopilot(count);
3186
+ flushAppMarkers(count);
3187
+ });
2916
3188
  // Flush remaining Nolan turns whose anchor is at/after the message count so
2917
3189
  // none are dropped. Autopilot/app markers beyond the restored message
2918
3190
  // count were already filtered above; any remaining marker here is valid.
@@ -2973,7 +3245,12 @@ async function createSession(deps, opts) {
2973
3245
  return;
2974
3246
  }
2975
3247
  if (method === "POST" && url === "/prompt") {
2976
- void readBody(req, res).then(async (raw) => {
3248
+ // Per-request: only the prompt that actually claimed the start may release
3249
+ // it. A bare release would let an early-returning request (bad JSON, or a
3250
+ // prompt that queued) clear a claim another request is still holding.
3251
+ let claimedStart = false;
3252
+ void readBody(req, res)
3253
+ .then(async (raw) => {
2977
3254
  if (raw === null)
2978
3255
  return;
2979
3256
  let text;
@@ -2993,14 +3270,17 @@ async function createSession(deps, opts) {
2993
3270
  json(res, 400, { error: "empty prompt" });
2994
3271
  return;
2995
3272
  }
2996
- if (runLifecycle.running && runLifecycle.isCancellationRequested(runLifecycle.generation)) {
3273
+ if (runLifecycle.running &&
3274
+ runLifecycle.isCancellationRequested(runLifecycle.generation)) {
2997
3275
  json(res, 409, {
2998
3276
  error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
2999
3277
  runState: runLifecycle.state,
3000
3278
  });
3001
3279
  return;
3002
3280
  }
3003
- if (running || autopilotActive) {
3281
+ // `runClaim` covers the gap before `runAgent` flips `running`: a
3282
+ // prompt arriving in that window must queue, not start a second run.
3283
+ if (running || runClaim.active || autopilotActive) {
3004
3284
  // Queue prompts as mid-run steering (mirrors the CLI). Also queue while
3005
3285
  // an autopilot cycle is active but between injected runs (build idle,
3006
3286
  // Nolan reviewing) so the message never starts a run that collides with
@@ -3009,20 +3289,47 @@ async function createSession(deps, opts) {
3009
3289
  // path as a non-queued attachment prompt when it drains.
3010
3290
  const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
3011
3291
  const count = session.queueMessage(text, prepared);
3012
- broadcast("queued", { count });
3292
+ broadcast("queued", { count, messages: session.listQueuedMessages() });
3013
3293
  json(res, 202, { queued: true, count });
3014
3294
  return;
3015
3295
  }
3296
+ // Claim the run NOW, synchronously. Everything below this line may
3297
+ // yield, and `running` does not flip until runAgent begins.
3298
+ claimedStart = runClaim.claim();
3016
3299
  json(res, 202, { accepted: true });
3300
+ // Gate inputs captured around the run: whether this turn is a workflow
3301
+ // slash command (attachment prompts skip slash expansion entirely), and
3302
+ // how many assistant messages the run actually adds. Computed even when
3303
+ // autopilot is currently off — the toggle can flip ON mid-run, and the
3304
+ // gate reads the post-run value.
3305
+ const workflowCommand = attachments.length === 0 &&
3306
+ isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
3307
+ // Does this input actually expand into a persisted user message? Asked
3308
+ // of the session itself, because only it knows whether the command
3309
+ // resolves here (name/alias casing, custom `.ezcoder/commands`, non-coder
3310
+ // agents that don't expand at all). A looser guess would anchor the
3311
+ // hint at +1 with no message to land on — decorating an unrelated
3312
+ // later bubble with the wrong `/name`.
3313
+ const expandsToTemplate = attachments.length === 0 && (await session.willExpandPromptTemplate(text));
3017
3314
  // Webview display hint for this prompt's user bubble (nolanSent shimmer
3018
- // label / enhancer highlight segments). Anchored +1 so it attaches to
3019
- // the user message the prompt below is about to push. Queued prompts
3020
- // skip this (their position in the run is unpredictable).
3021
- if (meta && (meta.nolanSent === true || Array.isArray(meta.enhancements))) {
3315
+ // label / enhancer highlight segments / the `/name` a command was typed
3316
+ // as). Anchored +1 so it attaches to the user message the prompt below
3317
+ // is about to push. Queued prompts skip this (their position in the run
3318
+ // is unpredictable).
3319
+ //
3320
+ // Recording the invocation matters because the agent persists the
3321
+ // EXPANDED template as the user message. Resume used to recover
3322
+ // `/name` by matching that body against the current templates, which
3323
+ // silently fails the moment a template is edited or reworded — the
3324
+ // reopened session then rendered the raw multi-KB prompt instead of
3325
+ // the command chip.
3326
+ if (expandsToTemplate ||
3327
+ (meta && (meta.nolanSent === true || Array.isArray(meta.enhancements)))) {
3022
3328
  void session
3023
3329
  .persistAppMarker("user_hint", {
3024
- ...(meta.nolanSent === true ? { nolanSent: true } : {}),
3025
- ...(Array.isArray(meta.enhancements) ? { enhancements: meta.enhancements } : {}),
3330
+ ...(expandsToTemplate ? { command: text.trim() } : {}),
3331
+ ...(meta?.nolanSent === true ? { nolanSent: true } : {}),
3332
+ ...(Array.isArray(meta?.enhancements) ? { enhancements: meta.enhancements } : {}),
3026
3333
  }, 1)
3027
3334
  .catch(() => { });
3028
3335
  }
@@ -3033,12 +3340,6 @@ async function createSession(deps, opts) {
3033
3340
  // feedback, anything) supersedes the pending plan — the bump also
3034
3341
  // invalidates any in-flight Nolan plan review.
3035
3342
  clearPendingPlan();
3036
- // Gate inputs captured around the run: whether this turn is a workflow
3037
- // slash command (attachment prompts skip slash expansion entirely), and
3038
- // how many assistant messages the run actually adds. Computed even when
3039
- // autopilot is currently off — the toggle can flip ON mid-run, and the
3040
- // gate reads the post-run value.
3041
- const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
3042
3343
  const assistantsBefore = countAssistantMessages(session.getMessages());
3043
3344
  const messagesBefore = session.getMessages().length;
3044
3345
  await runAgent(text, async () => {
@@ -3090,14 +3391,18 @@ async function createSession(deps, opts) {
3090
3391
  // A prompt sent while Nolan was reviewing (build idle) queued but had no
3091
3392
  // run to steer into — run it now as a fresh turn so it never strands.
3092
3393
  await runStrandedQueue();
3394
+ })
3395
+ .finally(() => {
3396
+ if (claimedStart)
3397
+ runClaim.release();
3093
3398
  });
3094
3399
  return;
3095
3400
  }
3096
3401
  // Nolan Grout (mentor): an independent read-only advisory run on the nolanSession.
3097
- // Runs concurrently with a build run — its events are ken_-prefixed so the
3402
+ // Runs concurrently with a build run — its events are nolan_-prefixed so the
3098
3403
  // webview keeps the bubbles separate. The context digest is assembled fresh
3099
3404
  // from the BUILD session's transcript each turn (one-way mirror).
3100
- if (method === "POST" && url === "/ken/prompt") {
3405
+ if (method === "POST" && url === "/nolan/prompt") {
3101
3406
  if (mode === "chat") {
3102
3407
  json(res, 404, { error: "Nolan is not available in EZ Chat." });
3103
3408
  return;
@@ -3125,18 +3430,18 @@ async function createSession(deps, opts) {
3125
3430
  nolanRunning = true;
3126
3431
  broadcast("nolan_run_start", { text });
3127
3432
  try {
3128
- const ken = await ensureNolanSession();
3433
+ const nolanAgent = await ensureNolanSession();
3129
3434
  const digest = await buildNolanContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
3130
- await ken.prompt(digest);
3435
+ await nolanAgent.prompt(digest);
3131
3436
  // Record the turn against the BUILD session so it persists + survives
3132
3437
  // resume (advisory custom entry, never an LLM message). Reply is Nolan's
3133
3438
  // last assistant message; skip persistence if he produced nothing.
3134
- const reply = lastAssistantText(ken.getMessages());
3439
+ const reply = lastAssistantText(nolanAgent.getMessages());
3135
3440
  if (reply.trim())
3136
3441
  await session.persistNolanTurn(text, reply);
3137
3442
  }
3138
3443
  catch (err) {
3139
- broadcastError("nolan_error", "ken run failed", err);
3444
+ broadcastError("nolan_error", "nolan run failed", err);
3140
3445
  }
3141
3446
  finally {
3142
3447
  nolanRunning = false;
@@ -3149,7 +3454,7 @@ async function createSession(deps, opts) {
3149
3454
  });
3150
3455
  return;
3151
3456
  }
3152
- if (method === "POST" && url === "/ken/cancel") {
3457
+ if (method === "POST" && url === "/nolan/cancel") {
3153
3458
  nolanAbort.abort();
3154
3459
  nolanAbort = new AbortController();
3155
3460
  nolanSession?.setSignal(nolanAbort.signal);
@@ -3524,7 +3829,7 @@ async function createSession(deps, opts) {
3524
3829
  // no-op event (keeps footer/event tests from treating an EZ Coder
3525
3830
  // switch as a Nolan switch).
3526
3831
  if (!nolanModelOverride)
3527
- broadcast("ken_model_change", nolanStatePayload());
3832
+ broadcast("nolan_model_change", nolanStatePayload());
3528
3833
  // The new model usually has a different context window — push extras so
3529
3834
  // the footer's context meter rescales immediately.
3530
3835
  broadcast("extras", footerExtras());
@@ -3536,7 +3841,7 @@ async function createSession(deps, opts) {
3536
3841
  // { model: null } / "" to clear (Nolan resumes following EZ Coder). Applies
3537
3842
  // to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
3538
3843
  // either is mid-run defers via the pending-model mechanics.
3539
- if (method === "POST" && url === "/ken/model") {
3844
+ if (method === "POST" && url === "/nolan/model") {
3540
3845
  void readBody(req, res).then(async (raw) => {
3541
3846
  if (raw === null)
3542
3847
  return;
@@ -3571,17 +3876,49 @@ async function createSession(deps, opts) {
3571
3876
  await saveNolanModelPref(cwd, nolanModelOverride);
3572
3877
  await syncNolanModel(target.provider, target.id);
3573
3878
  await syncNolanAutoModel(target.provider, target.id);
3574
- log("INFO", "app-sidecar", "ken model pinned", {
3879
+ log("INFO", "app-sidecar", "nolan model pinned", {
3575
3880
  provider: target.provider,
3576
3881
  model: target.id,
3577
3882
  });
3578
3883
  }
3579
3884
  const payload = nolanStatePayload();
3580
- broadcast("ken_model_change", payload);
3885
+ broadcast("nolan_model_change", payload);
3581
3886
  json(res, 200, payload);
3582
3887
  });
3583
3888
  return;
3584
3889
  }
3890
+ // Pending queued steering, for the composer's cancel affordance.
3891
+ if (method === "GET" && url === "/queued") {
3892
+ json(res, 200, { queued: session.listQueuedMessages() });
3893
+ return;
3894
+ }
3895
+ // Cancel one pending queued message by id.
3896
+ if (method === "POST" && url === "/queued/cancel") {
3897
+ void readBody(req, res).then((raw) => {
3898
+ if (raw === null)
3899
+ return;
3900
+ let id;
3901
+ try {
3902
+ id = JSON.parse(raw).id ?? "";
3903
+ }
3904
+ catch {
3905
+ json(res, 400, { error: "invalid JSON body" });
3906
+ return;
3907
+ }
3908
+ if (!id.trim()) {
3909
+ json(res, 400, { error: "missing queued message id" });
3910
+ return;
3911
+ }
3912
+ // `false` means it already drained into the run between render and
3913
+ // click. That is a race, not an error, so report it as a normal result
3914
+ // and let the client reconcile from the fresh list.
3915
+ const cancelled = session.cancelQueuedMessage(id);
3916
+ const queued = session.listQueuedMessages();
3917
+ broadcast("queued", { count: queued.length, messages: queued });
3918
+ json(res, 200, { cancelled, queued });
3919
+ });
3920
+ return;
3921
+ }
3585
3922
  if (method === "POST" && url === "/kill") {
3586
3923
  void readBody(req, res).then(async (raw) => {
3587
3924
  if (raw === null)
@@ -3605,6 +3942,33 @@ async function createSession(deps, opts) {
3605
3942
  });
3606
3943
  return;
3607
3944
  }
3945
+ // Import a Claude Code / Codex / Cursor transcript into a resumable GG
3946
+ // Coder session. The importer never throws — it returns a typed failure so
3947
+ // the app can show the reason verbatim.
3948
+ if (method === "POST" && url === "/import-transcript") {
3949
+ void readBody(req, res).then(async (raw) => {
3950
+ if (raw === null)
3951
+ return;
3952
+ let body;
3953
+ try {
3954
+ body = JSON.parse(raw);
3955
+ }
3956
+ catch {
3957
+ json(res, 400, { error: "invalid JSON body" });
3958
+ return;
3959
+ }
3960
+ const filePath = body.path?.trim();
3961
+ if (!filePath) {
3962
+ json(res, 400, { error: "missing transcript path" });
3963
+ return;
3964
+ }
3965
+ const result = await session.importForeignTranscript(filePath, {
3966
+ ...(body.cwd ? { cwd: body.cwd } : {}),
3967
+ });
3968
+ json(res, result.ok ? 200 : 400, result);
3969
+ });
3970
+ return;
3971
+ }
3608
3972
  if (method === "POST" && url === "/thinking") {
3609
3973
  const st = session.getState();
3610
3974
  const next = getNextThinkingLevel(st.provider, st.model, session.getThinkingLevel());
@@ -3639,7 +4003,7 @@ async function createSession(deps, opts) {
3639
4003
  const generation = runLifecycle.generation;
3640
4004
  if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
3641
4005
  pendingCancelDrain = { generation, text: session.drainQueue() };
3642
- broadcast("queued", { count: 0 });
4006
+ broadcast("queued", { count: 0, messages: [] });
3643
4007
  }
3644
4008
  const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
3645
4009
  const drained = pendingCancelDrain.text;
@@ -3799,7 +4163,17 @@ async function createSession(deps, opts) {
3799
4163
  ...(baseUrl ? { baseUrl } : {}),
3800
4164
  };
3801
4165
  await auth.setCredentials(storageKey, creds);
3802
- broadcast("auth_done", { provider });
4166
+ // auth.json is shared by every window, so this is a global change:
4167
+ // close their login modals and refresh their provider lists too.
4168
+ broadcastAll("auth_done", { provider });
4169
+ // `auth_done` means "a login succeeded" (modals close on it).
4170
+ // `auth_change` means "auth.json changed" — which a DISCONNECT also is,
4171
+ // so connection state has one signal that covers both directions.
4172
+ broadcastAll("auth_change", { provider });
4173
+ // A newly connected provider unlocks its models. `/models` filters on
4174
+ // who is logged in, so every window's picker is now stale — without
4175
+ // this the new models don't appear until the session is reopened.
4176
+ broadcastAll("models_change", {});
3803
4177
  json(res, 200, { ok: true });
3804
4178
  });
3805
4179
  return;
@@ -3825,7 +4199,18 @@ async function createSession(deps, opts) {
3825
4199
  json(res, 409, { error: "a login is already in progress" });
3826
4200
  return;
3827
4201
  }
4202
+ // A login writes the shared ~/.ezcoder/auth.json, so two windows racing the
4203
+ // same provider means two browser tabs and two token exchanges whose
4204
+ // writes clobber each other. The per-session flag above cannot see
4205
+ // that — guard the provider daemon-wide as well.
4206
+ if (oauthInFlightProviders.has(provider)) {
4207
+ json(res, 409, {
4208
+ error: `a ${meta.label} login is already in progress in another window`,
4209
+ });
4210
+ return;
4211
+ }
3828
4212
  oauthInFlight = true;
4213
+ oauthInFlightProviders.add(provider);
3829
4214
  json(res, 202, { accepted: true });
3830
4215
  void (async () => {
3831
4216
  const cb = authCallbacks();
@@ -3838,18 +4223,29 @@ async function createSession(deps, opts) {
3838
4223
  creds = await loginOpenAI(cb);
3839
4224
  else if (provider === "gemini")
3840
4225
  creds = await loginGemini(cb);
3841
- else if (provider === "moonshot") {
3842
- creds = await loginKimi(cb);
3843
- storageKey = MOONSHOT_OAUTH_KEY;
4226
+ else if (provider === "moonshot" || provider === "xai") {
4227
+ // Subscription OAuth (Kimi plan / SuperGrok-X Premium) stores under
4228
+ // a distinct key so it can coexist with the provider's API key.
4229
+ creds = provider === "moonshot" ? await loginKimi(cb) : await loginXai(cb);
4230
+ storageKey = dualAuthProvider(provider).oauthKey;
3844
4231
  }
3845
4232
  else {
3846
4233
  throw new Error(`OAuth not implemented for ${provider}`);
3847
4234
  }
3848
4235
  await auth.setCredentials(storageKey, creds);
3849
- broadcast("auth_done", { provider });
4236
+ // Terminal outcome of a GLOBAL change: every window's login modal
4237
+ // should close and its provider list refresh, not just the one
4238
+ // that started the flow.
4239
+ broadcastAll("auth_done", { provider });
4240
+ broadcastAll("auth_change", { provider });
4241
+ // The OAuth provider's models just became selectable everywhere.
4242
+ broadcastAll("models_change", {});
3850
4243
  }
3851
4244
  catch (err) {
3852
4245
  captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
4246
+ // Deliberately session-scoped: this is the outcome of ONE window's
4247
+ // attempt. Another window that never pressed Connect has nothing to
4248
+ // show an error about, and its modal correctly still offers login.
3853
4249
  broadcast("auth_error", {
3854
4250
  provider,
3855
4251
  message: err instanceof Error ? err.message : String(err),
@@ -3857,6 +4253,7 @@ async function createSession(deps, opts) {
3857
4253
  }
3858
4254
  finally {
3859
4255
  oauthInFlight = false;
4256
+ oauthInFlightProviders.delete(provider);
3860
4257
  pendingCode = null;
3861
4258
  }
3862
4259
  })();
@@ -3885,27 +4282,72 @@ async function createSession(deps, opts) {
3885
4282
  });
3886
4283
  return;
3887
4284
  }
4285
+ if (method === "POST" && url.startsWith("/mcp/elicit/")) {
4286
+ const id = decodeURIComponent(url.slice("/mcp/elicit/".length));
4287
+ void readBody(req, res).then((raw) => {
4288
+ if (raw === null)
4289
+ return;
4290
+ let result;
4291
+ try {
4292
+ const parsed = JSON.parse(raw);
4293
+ if (parsed.action !== "accept" &&
4294
+ parsed.action !== "decline" &&
4295
+ parsed.action !== "cancel") {
4296
+ json(res, 400, { error: "action must be accept, decline, or cancel" });
4297
+ return;
4298
+ }
4299
+ result =
4300
+ parsed.action === "accept"
4301
+ ? { action: "accept", content: parsed.content ?? {} }
4302
+ : { action: parsed.action };
4303
+ }
4304
+ catch {
4305
+ json(res, 400, { error: "invalid JSON body" });
4306
+ return;
4307
+ }
4308
+ // Unknown id means it already timed out or was cancelled by an abort —
4309
+ // the tool call has moved on, so the answer has nowhere to go.
4310
+ if (!elicitations.settle(id, result)) {
4311
+ json(res, 409, { error: "no elicitation is awaiting a response" });
4312
+ return;
4313
+ }
4314
+ json(res, 200, { ok: true });
4315
+ });
4316
+ return;
4317
+ }
3888
4318
  if (method === "POST" && url === "/auth/logout") {
3889
4319
  void readBody(req, res).then(async (raw) => {
3890
4320
  if (raw === null)
3891
4321
  return;
3892
4322
  let provider;
4323
+ let logoutMethod;
3893
4324
  try {
3894
- provider = JSON.parse(raw).provider ?? "";
4325
+ const body = JSON.parse(raw);
4326
+ provider = body.provider ?? "";
4327
+ logoutMethod =
4328
+ body.method === "oauth" || body.method === "apikey" ? body.method : undefined;
3895
4329
  }
3896
4330
  catch {
3897
4331
  json(res, 400, { error: "invalid JSON body" });
3898
4332
  return;
3899
4333
  }
3900
- await auth.clearCredentials(provider);
3901
- // Moonshot's OAuth credential lives under a distinct key — clear both so
3902
- // "disconnect" fully removes Kimi OAuth and the API key.
3903
- if (provider === "moonshot")
3904
- await auth.clearCredentials(MOONSHOT_OAUTH_KEY);
3905
- // Xiaomi's API Credits credential lives under a distinct key — clear it
3906
- // too so "disconnect" fully removes both the Token Plan and Credits keys.
3907
- if (provider === "xiaomi")
3908
- await auth.clearCredentials(XIAOMI_CREDITS_KEY);
4334
+ const dual = dualAuthProvider(provider);
4335
+ // A dual-auth provider can be disconnected one method at a time: dropping
4336
+ // a spent API key should not sign the user out of their subscription, and
4337
+ // vice versa. Omitting `method` still clears everything (the plain
4338
+ // "Disconnect" action).
4339
+ if (logoutMethod !== "apikey") {
4340
+ await auth.clearCredentials(dual ? dual.oauthKey : provider);
4341
+ }
4342
+ if (logoutMethod !== "oauth") {
4343
+ // Non-dual providers store their only credential under the provider id,
4344
+ // so this covers both them and a dual provider's API key.
4345
+ await auth.clearCredentials(provider);
4346
+ // Xiaomi's API Credits credential lives under a distinct key — clear it
4347
+ // too so "disconnect" fully removes both the Token Plan and Credits keys.
4348
+ if (provider === "xiaomi")
4349
+ await auth.clearCredentials(XIAOMI_CREDITS_KEY);
4350
+ }
3909
4351
  broadcast("auth_done", { provider });
3910
4352
  json(res, 200, { ok: true });
3911
4353
  });
@@ -4270,6 +4712,7 @@ async function createSession(deps, opts) {
4270
4712
  json(res, 404, { error: "not found" });
4271
4713
  }
4272
4714
  async function dispose() {
4715
+ elicitations.cancelAll();
4273
4716
  tasksPollStopped = true;
4274
4717
  if (tasksPoll)
4275
4718
  clearTimeout(tasksPoll);