aicodeman 1.47.0 → 1.49.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 (361) hide show
  1. package/README.md +15 -3
  2. package/dist/cli-notify.d.ts +34 -0
  3. package/dist/cli-notify.d.ts.map +1 -0
  4. package/dist/cli-notify.js +100 -0
  5. package/dist/cli-notify.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +16 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config/cli-registry/patterns.d.ts +1 -1
  10. package/dist/config/cli-registry/patterns.d.ts.map +1 -1
  11. package/dist/config/cli-registry/patterns.js +4 -0
  12. package/dist/config/cli-registry/patterns.js.map +1 -1
  13. package/dist/config/cli-registry/schema.d.ts +7 -0
  14. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  15. package/dist/config/cli-registry/schema.js +67 -3
  16. package/dist/config/cli-registry/schema.js.map +1 -1
  17. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  18. package/dist/config/cli-registry/stock.js +114 -31
  19. package/dist/config/cli-registry/stock.js.map +1 -1
  20. package/dist/config/cli-registry/types.d.ts +51 -21
  21. package/dist/config/cli-registry/types.d.ts.map +1 -1
  22. package/dist/config/foreign-tmux.d.ts +48 -0
  23. package/dist/config/foreign-tmux.d.ts.map +1 -0
  24. package/dist/config/foreign-tmux.js +48 -0
  25. package/dist/config/foreign-tmux.js.map +1 -0
  26. package/dist/config/path-probe.d.ts +6 -0
  27. package/dist/config/path-probe.d.ts.map +1 -1
  28. package/dist/config/path-probe.js +6 -0
  29. package/dist/config/path-probe.js.map +1 -1
  30. package/dist/config-backup.d.ts +1 -1
  31. package/dist/config-backup.d.ts.map +1 -1
  32. package/dist/config-backup.js +2 -0
  33. package/dist/config-backup.js.map +1 -1
  34. package/dist/custom-model-hosts.d.ts +32 -8
  35. package/dist/custom-model-hosts.d.ts.map +1 -1
  36. package/dist/custom-model-hosts.js +10 -0
  37. package/dist/custom-model-hosts.js.map +1 -1
  38. package/dist/custom-model-injection-apply.d.ts +21 -5
  39. package/dist/custom-model-injection-apply.d.ts.map +1 -1
  40. package/dist/custom-model-injection-apply.js +121 -27
  41. package/dist/custom-model-injection-apply.js.map +1 -1
  42. package/dist/custom-model-injection.d.ts +6 -0
  43. package/dist/custom-model-injection.d.ts.map +1 -1
  44. package/dist/custom-model-injection.js +92 -10
  45. package/dist/custom-model-injection.js.map +1 -1
  46. package/dist/discord-bridge.d.ts +338 -0
  47. package/dist/discord-bridge.d.ts.map +1 -0
  48. package/dist/discord-bridge.js +1256 -0
  49. package/dist/discord-bridge.js.map +1 -0
  50. package/dist/docker-hosts.d.ts +8 -1
  51. package/dist/docker-hosts.d.ts.map +1 -1
  52. package/dist/docker-hosts.js +5 -5
  53. package/dist/docker-hosts.js.map +1 -1
  54. package/dist/edit-history-local-source.d.ts +83 -0
  55. package/dist/edit-history-local-source.d.ts.map +1 -0
  56. package/dist/edit-history-local-source.js +198 -0
  57. package/dist/edit-history-local-source.js.map +1 -0
  58. package/dist/edit-history-parse.d.ts +114 -0
  59. package/dist/edit-history-parse.d.ts.map +1 -0
  60. package/dist/edit-history-parse.js +312 -0
  61. package/dist/edit-history-parse.js.map +1 -0
  62. package/dist/edit-history-store.d.ts +209 -0
  63. package/dist/edit-history-store.d.ts.map +1 -0
  64. package/dist/edit-history-store.js +599 -0
  65. package/dist/edit-history-store.js.map +1 -0
  66. package/dist/foreign-tmux-discovery.d.ts +140 -0
  67. package/dist/foreign-tmux-discovery.d.ts.map +1 -0
  68. package/dist/foreign-tmux-discovery.js +366 -0
  69. package/dist/foreign-tmux-discovery.js.map +1 -0
  70. package/dist/foreign-tmux.d.ts +381 -0
  71. package/dist/foreign-tmux.d.ts.map +1 -0
  72. package/dist/foreign-tmux.js +775 -0
  73. package/dist/foreign-tmux.js.map +1 -0
  74. package/dist/git-clone.d.ts +6 -1
  75. package/dist/git-clone.d.ts.map +1 -1
  76. package/dist/git-clone.js +79 -14
  77. package/dist/git-clone.js.map +1 -1
  78. package/dist/git-fetch-scheduler.d.ts +2 -0
  79. package/dist/git-fetch-scheduler.d.ts.map +1 -1
  80. package/dist/git-fetch-scheduler.js +1 -1
  81. package/dist/git-fetch-scheduler.js.map +1 -1
  82. package/dist/git-fetch.d.ts +1 -1
  83. package/dist/git-fetch.d.ts.map +1 -1
  84. package/dist/git-workspace-status.d.ts +2 -2
  85. package/dist/git-workspace-status.d.ts.map +1 -1
  86. package/dist/hooks-config.d.ts +2 -1
  87. package/dist/hooks-config.d.ts.map +1 -1
  88. package/dist/hooks-config.js +2 -1
  89. package/dist/hooks-config.js.map +1 -1
  90. package/dist/image-watcher.js +4 -4
  91. package/dist/image-watcher.js.map +1 -1
  92. package/dist/live-worktree.d.ts +53 -0
  93. package/dist/live-worktree.d.ts.map +1 -0
  94. package/dist/live-worktree.js +97 -0
  95. package/dist/live-worktree.js.map +1 -0
  96. package/dist/mux-interface.d.ts +22 -2
  97. package/dist/mux-interface.d.ts.map +1 -1
  98. package/dist/reboot-restore.d.ts +1 -1
  99. package/dist/reboot-restore.d.ts.map +1 -1
  100. package/dist/reboot-restore.js +9 -0
  101. package/dist/reboot-restore.js.map +1 -1
  102. package/dist/remote-files.d.ts +150 -5
  103. package/dist/remote-files.d.ts.map +1 -1
  104. package/dist/remote-files.js +505 -7
  105. package/dist/remote-files.js.map +1 -1
  106. package/dist/remote-hosts.d.ts +15 -9
  107. package/dist/remote-hosts.d.ts.map +1 -1
  108. package/dist/remote-hosts.js +42 -20
  109. package/dist/remote-hosts.js.map +1 -1
  110. package/dist/session-cli-registry-bridge.d.ts.map +1 -1
  111. package/dist/session-cli-registry-bridge.js +23 -8
  112. package/dist/session-cli-registry-bridge.js.map +1 -1
  113. package/dist/session-env-clamp.d.ts +1 -1
  114. package/dist/session-env-clamp.d.ts.map +1 -1
  115. package/dist/session-env-clamp.js +4 -3
  116. package/dist/session-env-clamp.js.map +1 -1
  117. package/dist/session.d.ts +161 -24
  118. package/dist/session.d.ts.map +1 -1
  119. package/dist/session.js +337 -37
  120. package/dist/session.js.map +1 -1
  121. package/dist/tmux-manager.d.ts +114 -12
  122. package/dist/tmux-manager.d.ts.map +1 -1
  123. package/dist/tmux-manager.js +318 -41
  124. package/dist/tmux-manager.js.map +1 -1
  125. package/dist/types/discord.d.ts +55 -0
  126. package/dist/types/discord.d.ts.map +1 -0
  127. package/dist/types/discord.js +18 -0
  128. package/dist/types/discord.js.map +1 -0
  129. package/dist/types/foreign-tmux.d.ts +101 -0
  130. package/dist/types/foreign-tmux.d.ts.map +1 -0
  131. package/dist/types/foreign-tmux.js +27 -0
  132. package/dist/types/foreign-tmux.js.map +1 -0
  133. package/dist/types/index.d.ts +3 -0
  134. package/dist/types/index.d.ts.map +1 -1
  135. package/dist/types/index.js +3 -0
  136. package/dist/types/index.js.map +1 -1
  137. package/dist/types/session.d.ts +119 -2
  138. package/dist/types/session.d.ts.map +1 -1
  139. package/dist/types/session.js.map +1 -1
  140. package/dist/types/update.d.ts +4 -1
  141. package/dist/types/update.d.ts.map +1 -1
  142. package/dist/web/case-path.d.ts +2 -0
  143. package/dist/web/case-path.d.ts.map +1 -1
  144. package/dist/web/case-path.js +3 -3
  145. package/dist/web/case-path.js.map +1 -1
  146. package/dist/web/edit-history-sync.d.ts +178 -0
  147. package/dist/web/edit-history-sync.d.ts.map +1 -0
  148. package/dist/web/edit-history-sync.js +461 -0
  149. package/dist/web/edit-history-sync.js.map +1 -0
  150. package/dist/web/launch-defaults.d.ts +1 -1
  151. package/dist/web/launch-defaults.js +2 -2
  152. package/dist/web/launch-defaults.js.map +1 -1
  153. package/dist/web/linked-cases-writer.d.ts +43 -0
  154. package/dist/web/linked-cases-writer.d.ts.map +1 -0
  155. package/dist/web/linked-cases-writer.js +107 -0
  156. package/dist/web/linked-cases-writer.js.map +1 -0
  157. package/dist/web/paste-image-gc.d.ts +10 -1
  158. package/dist/web/paste-image-gc.d.ts.map +1 -1
  159. package/dist/web/paste-image-gc.js +10 -2
  160. package/dist/web/paste-image-gc.js.map +1 -1
  161. package/dist/web/ports/session-port.d.ts +13 -0
  162. package/dist/web/ports/session-port.d.ts.map +1 -1
  163. package/dist/web/public/admin-ui.js.gz +0 -0
  164. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  165. package/dist/web/public/{app.fe8a6b4c.js → app.9ea37070.js} +6 -6
  166. package/dist/web/public/app.9ea37070.js.br +0 -0
  167. package/dist/web/public/app.9ea37070.js.gz +0 -0
  168. package/dist/web/public/approvals-ui.js.gz +0 -0
  169. package/dist/web/public/{constants.affcdde6.js → constants.782170e1.js} +114 -25
  170. package/dist/web/public/constants.782170e1.js.br +0 -0
  171. package/dist/web/public/constants.782170e1.js.gz +0 -0
  172. package/dist/web/public/cron-ui.js.gz +0 -0
  173. package/dist/web/public/edit-history-ui.js +643 -0
  174. package/dist/web/public/edit-history-ui.js.br +0 -0
  175. package/dist/web/public/edit-history-ui.js.gz +0 -0
  176. package/dist/web/public/entrance-animations.js.gz +0 -0
  177. package/dist/web/public/foreign-sessions.js +476 -0
  178. package/dist/web/public/foreign-sessions.js.br +0 -0
  179. package/dist/web/public/foreign-sessions.js.gz +0 -0
  180. package/dist/web/public/git-status-ui.js +5 -3
  181. package/dist/web/public/git-status-ui.js.br +0 -0
  182. package/dist/web/public/git-status-ui.js.gz +0 -0
  183. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  184. package/dist/web/public/home-sessions.js.gz +0 -0
  185. package/dist/web/public/host-wake-ui.js.gz +0 -0
  186. package/dist/web/public/i18n.a5e82c95.js +2 -0
  187. package/dist/web/public/i18n.a5e82c95.js.br +0 -0
  188. package/dist/web/public/i18n.a5e82c95.js.gz +0 -0
  189. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  190. package/dist/web/public/index.html +183 -18
  191. package/dist/web/public/index.html.br +0 -0
  192. package/dist/web/public/index.html.gz +0 -0
  193. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  194. package/dist/web/public/{keyboard-accessory.ff64fafc.js → keyboard-accessory.b941676c.js} +75 -2
  195. package/dist/web/public/keyboard-accessory.b941676c.js.br +0 -0
  196. package/dist/web/public/keyboard-accessory.b941676c.js.gz +0 -0
  197. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  198. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  199. package/dist/web/public/mobile-overview.js +10 -0
  200. package/dist/web/public/mobile-overview.js.br +0 -0
  201. package/dist/web/public/mobile-overview.js.gz +0 -0
  202. package/dist/web/public/mobile.67c8eb5b.css +1 -0
  203. package/dist/web/public/mobile.67c8eb5b.css.br +0 -0
  204. package/dist/web/public/mobile.67c8eb5b.css.gz +0 -0
  205. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  206. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  207. package/dist/web/public/{panels-ui.4a56ee63.js → panels-ui.8bcd4473.js} +5 -5
  208. package/dist/web/public/panels-ui.8bcd4473.js.br +0 -0
  209. package/dist/web/public/panels-ui.8bcd4473.js.gz +0 -0
  210. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  211. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  212. package/dist/web/public/readmymind-ui.js.gz +0 -0
  213. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  214. package/dist/web/public/{respawn-ui.d7adcf6a.js → respawn-ui.304ba0d4.js} +1 -1
  215. package/dist/web/public/respawn-ui.304ba0d4.js.br +0 -0
  216. package/dist/web/public/respawn-ui.304ba0d4.js.gz +0 -0
  217. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  218. package/dist/web/public/security-notices.json +1 -21
  219. package/dist/web/public/session-lineage.js.gz +0 -0
  220. package/dist/web/public/{session-ui.426fb671.js → session-ui.a7c8c095.js} +30 -30
  221. package/dist/web/public/session-ui.a7c8c095.js.br +0 -0
  222. package/dist/web/public/session-ui.a7c8c095.js.gz +0 -0
  223. package/dist/web/public/settings-ui.18f92052.js +106 -0
  224. package/dist/web/public/settings-ui.18f92052.js.br +0 -0
  225. package/dist/web/public/settings-ui.18f92052.js.gz +0 -0
  226. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  227. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  228. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  229. package/dist/web/public/styles.6a718331.css +1 -0
  230. package/dist/web/public/styles.6a718331.css.br +0 -0
  231. package/dist/web/public/styles.6a718331.css.gz +0 -0
  232. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  233. package/dist/web/public/sw.js +2 -2
  234. package/dist/web/public/sw.js.br +0 -0
  235. package/dist/web/public/sw.js.gz +0 -0
  236. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  237. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  238. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  239. package/dist/web/public/terminal-split.js.gz +0 -0
  240. package/dist/web/public/terminal-tile.js +2 -2
  241. package/dist/web/public/terminal-tile.js.br +0 -0
  242. package/dist/web/public/terminal-tile.js.gz +0 -0
  243. package/dist/web/public/terminal-ui.a7883640.js +4 -0
  244. package/dist/web/public/terminal-ui.a7883640.js.br +0 -0
  245. package/dist/web/public/terminal-ui.a7883640.js.gz +0 -0
  246. package/dist/web/public/tile-grid.js.gz +0 -0
  247. package/dist/web/public/ultracode-panel.js.gz +0 -0
  248. package/dist/web/public/ultracode-windows.js.gz +0 -0
  249. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  250. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  251. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  252. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  253. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  254. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  255. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  256. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  257. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  258. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  259. package/dist/web/public/vendor/xterm.css.gz +0 -0
  260. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  261. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  262. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  263. package/dist/web/public/webview-tabs.js.gz +0 -0
  264. package/dist/web/public/whats-new.json +9 -8
  265. package/dist/web/routes/case-routes.d.ts.map +1 -1
  266. package/dist/web/routes/case-routes.js +165 -50
  267. package/dist/web/routes/case-routes.js.map +1 -1
  268. package/dist/web/routes/config-backup-routes.d.ts +5 -1
  269. package/dist/web/routes/config-backup-routes.d.ts.map +1 -1
  270. package/dist/web/routes/config-backup-routes.js +4 -1
  271. package/dist/web/routes/config-backup-routes.js.map +1 -1
  272. package/dist/web/routes/custom-model-routes.d.ts +7 -2
  273. package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
  274. package/dist/web/routes/custom-model-routes.js +51 -6
  275. package/dist/web/routes/custom-model-routes.js.map +1 -1
  276. package/dist/web/routes/discord-routes.d.ts +25 -0
  277. package/dist/web/routes/discord-routes.d.ts.map +1 -0
  278. package/dist/web/routes/discord-routes.js +206 -0
  279. package/dist/web/routes/discord-routes.js.map +1 -0
  280. package/dist/web/routes/edit-history-routes.d.ts +43 -0
  281. package/dist/web/routes/edit-history-routes.d.ts.map +1 -0
  282. package/dist/web/routes/edit-history-routes.js +107 -0
  283. package/dist/web/routes/edit-history-routes.js.map +1 -0
  284. package/dist/web/routes/file-routes.d.ts.map +1 -1
  285. package/dist/web/routes/file-routes.js +273 -102
  286. package/dist/web/routes/file-routes.js.map +1 -1
  287. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  288. package/dist/web/routes/git-status-routes.js +7 -2
  289. package/dist/web/routes/git-status-routes.js.map +1 -1
  290. package/dist/web/routes/index.d.ts +2 -0
  291. package/dist/web/routes/index.d.ts.map +1 -1
  292. package/dist/web/routes/index.js +2 -0
  293. package/dist/web/routes/index.js.map +1 -1
  294. package/dist/web/routes/mux-routes.d.ts +10 -3
  295. package/dist/web/routes/mux-routes.d.ts.map +1 -1
  296. package/dist/web/routes/mux-routes.js +59 -1
  297. package/dist/web/routes/mux-routes.js.map +1 -1
  298. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  299. package/dist/web/routes/ralph-routes.js +22 -0
  300. package/dist/web/routes/ralph-routes.js.map +1 -1
  301. package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
  302. package/dist/web/routes/readmymind-routes.js +3 -2
  303. package/dist/web/routes/readmymind-routes.js.map +1 -1
  304. package/dist/web/routes/respawn-routes.d.ts.map +1 -1
  305. package/dist/web/routes/respawn-routes.js +24 -0
  306. package/dist/web/routes/respawn-routes.js.map +1 -1
  307. package/dist/web/routes/session-routes.d.ts +21 -1
  308. package/dist/web/routes/session-routes.d.ts.map +1 -1
  309. package/dist/web/routes/session-routes.js +338 -55
  310. package/dist/web/routes/session-routes.js.map +1 -1
  311. package/dist/web/routes/system-routes.d.ts.map +1 -1
  312. package/dist/web/routes/system-routes.js +4 -2
  313. package/dist/web/routes/system-routes.js.map +1 -1
  314. package/dist/web/schemas.d.ts +92 -2
  315. package/dist/web/schemas.d.ts.map +1 -1
  316. package/dist/web/schemas.js +111 -10
  317. package/dist/web/schemas.js.map +1 -1
  318. package/dist/web/self-update.d.ts +9 -0
  319. package/dist/web/self-update.d.ts.map +1 -1
  320. package/dist/web/self-update.js +19 -3
  321. package/dist/web/self-update.js.map +1 -1
  322. package/dist/web/sensitive-path.d.ts.map +1 -1
  323. package/dist/web/sensitive-path.js +15 -0
  324. package/dist/web/sensitive-path.js.map +1 -1
  325. package/dist/web/server.d.ts +56 -0
  326. package/dist/web/server.d.ts.map +1 -1
  327. package/dist/web/server.js +285 -17
  328. package/dist/web/server.js.map +1 -1
  329. package/dist/web/session-wait-registry.d.ts +13 -0
  330. package/dist/web/session-wait-registry.d.ts.map +1 -1
  331. package/dist/web/session-wait-registry.js +16 -8
  332. package/dist/web/session-wait-registry.js.map +1 -1
  333. package/package.json +2 -2
  334. package/skills/codeman/SKILL.md +20 -4
  335. package/dist/web/public/app.fe8a6b4c.js.br +0 -0
  336. package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
  337. package/dist/web/public/constants.affcdde6.js.br +0 -0
  338. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  339. package/dist/web/public/i18n.a618ef4e.js +0 -2
  340. package/dist/web/public/i18n.a618ef4e.js.br +0 -0
  341. package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
  342. package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
  343. package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
  344. package/dist/web/public/mobile.1bbd2d67.css +0 -1
  345. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  346. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  347. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  348. package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
  349. package/dist/web/public/respawn-ui.d7adcf6a.js.br +0 -0
  350. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  351. package/dist/web/public/session-ui.426fb671.js.br +0 -0
  352. package/dist/web/public/session-ui.426fb671.js.gz +0 -0
  353. package/dist/web/public/settings-ui.a191154e.js +0 -106
  354. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  355. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  356. package/dist/web/public/styles.4f0c67a0.css +0 -1
  357. package/dist/web/public/styles.4f0c67a0.css.br +0 -0
  358. package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
  359. package/dist/web/public/terminal-ui.7e787829.js +0 -4
  360. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  361. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
@@ -4,7 +4,7 @@
4
4
  * auto-clear, auto-compact, image watcher, flicker filter, and logout.
5
5
  */
6
6
  import { claudeProjectDirName } from '../../case-rename.js';
7
- import { join, dirname, extname, basename, resolve as resolvePath } from 'node:path';
7
+ import { join, dirname, extname, basename, isAbsolute, resolve as resolvePath } from 'node:path';
8
8
  import { homedir } from 'node:os';
9
9
  import { existsSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
10
10
  import { execFile } from 'node:child_process';
@@ -14,13 +14,18 @@ import { randomBytes, randomUUID } from 'node:crypto';
14
14
  import { isUtf8 } from 'node:buffer';
15
15
  import { performance } from 'node:perf_hooks';
16
16
  import { ApiErrorCode, createErrorResponse, getErrorMessage, } from '../../types.js';
17
+ import { AdoptForeignSessionSchema } from '../schemas.js';
18
+ import { discoverForeignSessions, invalidateForeignCache, readAllDockerCases, readAllRemoteHosts, } from '../../foreign-tmux-discovery.js';
19
+ import { adoptTargetIdentity, foreignTargetKey, foreignViewSessionName } from '../../foreign-tmux.js';
20
+ import { requireAdmin } from '../route-helpers.js';
17
21
  import { GuardedInputBusyError, Session, cliTakesSessionModel, isAltScreenStripMode, isExternalCliMode, isMuxAltScreenOnlyStripMode, isMuxMouseStripMode, } from '../../session.js';
18
22
  import { SseEvent } from '../sse-events.js';
19
23
  import { webviewCapabilities } from '../../webview-capabilities.js';
20
24
  import { htmlViewCapabilities } from '../../html-view-capabilities.js';
21
25
  import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, AutoResumeSchema, KeepWarmSchema, GitFetchSettingsSchema, PinSessionSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, InteractiveStartSchema, SessionOrderUpdateSchema, SessionWaitQuerySchema, SessionWaitOutputQuerySchema, CustomModelSelectionSchema, } from '../schemas.js';
22
- import { readCustomModelHosts } from '../../custom-model-hosts.js';
26
+ import { readCustomModelHosts, resolveCustomModelContextLength } from '../../custom-model-hosts.js';
23
27
  import { applyCustomModelInjection, removeConfigDir } from '../../custom-model-injection-apply.js';
28
+ import { buildCustomModelInjection } from '../../custom-model-injection.js';
24
29
  import { getLlamaSwapStatus, triggerLlamaSwapLoad, exceedsSafeContextFloor, CLAUDE_MIN_SAFE_CONTEXT_TOKENS, } from './custom-model-routes.js';
25
30
  import { matchesPattern } from '../../config/cli-registry/patterns.js';
26
31
  import { ownerLayoutKey } from '../../tab-layout-persistence.js';
@@ -75,6 +80,41 @@ const CLAUDE_WORKTREE_PATH = /^(.*)\/\.claude\/worktrees\/([^/]+)\/?$/;
75
80
  function landedInGroup(layout, sessionId) {
76
81
  return !!layout?.groups.some((group) => group.refs.some((ref) => ref.kind === 'session' && ref.id === sessionId));
77
82
  }
83
+ /**
84
+ * Guard a session-supplied configDir home BEFORE `applyCustomModelInjection` runs.
85
+ *
86
+ * The apply resolves the CLI's real home from this session's env override and then runs
87
+ * synchronous `existsSync`/`mkdirSync` on it (deepseek `DSH_HOME`, any future
88
+ * `linkFromRealHome` recipe, and claude's `CLAUDE_CONFIG_DIR` for `projects`). A path on an
89
+ * unreachable network mount makes a synchronous stat wait forever, and this runs INSIDE a
90
+ * request handler, so it would freeze the whole web server (CLAUDE.md, the #516 rule on
91
+ * user-chosen paths). A relative home is left to the apply, which already refuses to join
92
+ * non-absolute values.
93
+ *
94
+ * Probe the session's own home with `probePathKind(..., { pastCap: true })` first: on
95
+ * `'unknown'` (a dead mount that never answered) hand the apply a lookup that hides that
96
+ * one env var, so linking falls through to the server's own environment and the unresponsive
97
+ * path is never touched. Any other entry's lookup passes through untouched. Boot recovery
98
+ * passes `probeOptions` of `{}`: it walks every saved session, so it keeps the bulk cap.
99
+ */
100
+ export async function guardSessionConfigDirHome(entry, baseLookup, probeOptions = { pastCap: true }) {
101
+ const recipe = entry.capabilities.customModelInjection;
102
+ const dirEnvVar = recipe?.kind === 'env'
103
+ ? recipe.configDirVar
104
+ : recipe?.kind === 'configDir' && recipe.linkFromRealHome?.length
105
+ ? recipe.dirEnvVar
106
+ : undefined;
107
+ if (!dirEnvVar)
108
+ return baseLookup;
109
+ const supplied = baseLookup(dirEnvVar)?.trim();
110
+ if (!supplied || !isAbsolute(supplied))
111
+ return baseLookup;
112
+ if ((await probePathKind(supplied, probeOptions)) === 'unknown') {
113
+ console.warn(`[custom-model] ${dirEnvVar} (${supplied}) did not answer; linking from the server's own home instead`);
114
+ return (envVar) => (envVar === dirEnvVar ? undefined : baseLookup(envVar));
115
+ }
116
+ return baseLookup;
117
+ }
78
118
  /**
79
119
  * The case a POST /api/sessions `workingDir` belongs to, for its default session
80
120
  * group: a direct child of the caller's case space, or (admin / single-user, as
@@ -143,7 +183,7 @@ const MOUSE_TRACKING_PATTERN = /\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[h
143
183
  * - `strip-full` (claude/codex/gemini): alt-screen toggles, `3J` and mouse DECSETs.
144
184
  * xterm obeys the toggles by switching to its scrollback-less alt buffer and wiping
145
185
  * saved lines, so history disappeared on tab switch.
146
- * - `strip-mux-and-mouse` (opencode, tmux-backed): alt-screen toggles and mouse DECSETs.
186
+ * - `strip-mux-and-mouse` (tmux-backed): alt-screen toggles and mouse DECSETs.
147
187
  * A replayed tracking enable parks xterm in report mode, where a drag goes to the CLI
148
188
  * instead of selecting text (and Ctrl+C without a selection is opencode's app_exit).
149
189
  * `3J` stays: a TUI is not a `clear` consumer.
@@ -1088,8 +1128,9 @@ options = {}) {
1088
1128
  const globalNice = await ctx.getGlobalNiceConfig();
1089
1129
  const modelConfig = await ctx.getModelConfig();
1090
1130
  const mode = body.mode || 'claude';
1091
- // Synced App Settings launch defaults (capabilities.launchDefaults, codex's model and
1092
- // effort today) fill the CLI's own config object where the caller left it unset.
1131
+ // Synced App Settings launch defaults (capabilities.launchDefaults, codex's model,
1132
+ // effort and network access today) fill the CLI's own config object where the caller
1133
+ // left it unset.
1093
1134
  // Local launches only: a remote attach runs whatever the remote pane already runs.
1094
1135
  const launchBody = remote ? body : await applyLaunchDefaults(mode, body);
1095
1136
  // Where a model override comes from is a capability, and the three answers are
@@ -1172,6 +1213,169 @@ options = {}) {
1172
1213
  ...(body.tabGroupId || landedInGroup(tabLayout, session.id) ? { tabLayout } : {}),
1173
1214
  };
1174
1215
  });
1216
+ // ========== Adopt a foreign tmux session ==========
1217
+ /**
1218
+ * The wrapper pane for an adopted session runs `tmux attach`, never the agent,
1219
+ * so it needs no workspace, and must not borrow the foreign one. Same value
1220
+ * `resolveMuxAttachCwd()` gives remote and docker panes.
1221
+ */
1222
+ const ADOPTED_WRAPPER_WORKING_DIR = '/tmp';
1223
+ /**
1224
+ * Targets with a wrapper being created right now. The live-session check alone
1225
+ * leaves a gap: between it and `addSession` the route awaits the host registries,
1226
+ * the history config and the owner's layout lock, so two devices opening one row
1227
+ * could each pass it.
1228
+ */
1229
+ const adoptsInFlight = new Set();
1230
+ /**
1231
+ * Wrap a tmux session a HUMAN started (local, in a container, or over ssh) in a
1232
+ * Codeman session, so it appears as a tab and can be driven from the browser.
1233
+ *
1234
+ * Four things make this safe, and each is load-bearing:
1235
+ *
1236
+ * 1. **The body carries only an opaque id.** The socket path, session name and
1237
+ * host are re-resolved by re-running discovery here. A browser therefore
1238
+ * never supplies a fragment of the command we are about to run, which is the
1239
+ * same rule that keeps docker-adopt and remote-attach injection-free.
1240
+ * 2. **The candidate must still exist.** Discovery is re-run rather than cached,
1241
+ * so a session that died between the listing and the click fails with a 404
1242
+ * instead of producing a wrapper attached to nothing.
1243
+ * 3. **One wrapper per target.** Two wrappers on one foreign session would each
1244
+ * create their own grouped view and each think they own the tab; the guard
1245
+ * is here rather than in the button's in-flight lock, which only stops a
1246
+ * double-click on one device.
1247
+ * 4. **Admin-only under multi-user.** Discovery already is (it exposes other
1248
+ * users' processes), and adopting someone's `shell` is arbitrary execution
1249
+ * as the server account, which is exactly what the `can-bypass-permissions`
1250
+ * grant gates elsewhere. The admin gate subsumes it, so there is deliberately
1251
+ * no second grant check here.
1252
+ */
1253
+ app.post('/api/sessions/adopt', async (req, reply) => {
1254
+ if (isMultiUserMode() && !requireAdmin(req, reply))
1255
+ return;
1256
+ const owner = ownerFor(req);
1257
+ const body = parseBody(AdoptForeignSessionSchema, req.body, 'Invalid request body');
1258
+ // Re-resolve rather than trust: point 1 and 2 above.
1259
+ const found = await discoverForeignSessions({
1260
+ local: true,
1261
+ force: true,
1262
+ dockerCases: body.docker ? await readAllDockerCases() : undefined,
1263
+ remoteHosts: body.remote ? await readAllRemoteHosts() : undefined,
1264
+ });
1265
+ const target = found.sessions.find((f) => f.id === body.id);
1266
+ if (!target) {
1267
+ // ⚠️ "Not in the re-resolve" has two very different causes and they must not
1268
+ // be reported as one. The session really being gone is the ordinary case;
1269
+ // the OTHER case is a location we could not reach this time, which on a
1270
+ // flaky link makes a perfectly live remote session read as deleted. Measured
1271
+ // against a real VM whose ssh path dropped ~10% of connections: clicking
1272
+ // Open failed with "no longer there" while the session was sitting right
1273
+ // there. Discovery already knows which it was: it wrote a note, so say so.
1274
+ const reach = found.notes.filter((n) => !/skipped/.test(n));
1275
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, reach.length
1276
+ ? `Could not reach it just now (${reach.join('; ')}). It may still be running, try again.`
1277
+ : 'That tmux session is no longer there. Refresh the list and try again.');
1278
+ }
1279
+ // Point 3: one wrapper per target. Live sessions and not mux records, which
1280
+ // only exist once the first attach starts the pane, so reading those let a
1281
+ // second adopt through before the first tab had started.
1282
+ const targetKey = foreignTargetKey(target);
1283
+ for (const session of ctx.sessions.values()) {
1284
+ if (session.adopt && foreignTargetKey(adoptTargetIdentity(session.adopt)) === targetKey) {
1285
+ return { session: ctx.getSessionStateWithRespawn(session), alreadyAdopted: true };
1286
+ }
1287
+ }
1288
+ if (adoptsInFlight.has(targetKey)) {
1289
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'That tmux session is already being opened');
1290
+ }
1291
+ // After the reuse above, which creates nothing and so must not be refused at capacity.
1292
+ const capMsg = sessionCapacityMessage(ctx.sessions, owner);
1293
+ if (capMsg)
1294
+ return createErrorResponse(ApiErrorCode.SESSION_BUSY, capMsg);
1295
+ adoptsInFlight.add(targetKey);
1296
+ try {
1297
+ return await createAdoptedSession(req, target, owner, body);
1298
+ }
1299
+ finally {
1300
+ adoptsInFlight.delete(targetKey);
1301
+ }
1302
+ });
1303
+ /** Everything past the guards in `POST /api/sessions/adopt`, held under its in-flight mark. */
1304
+ async function createAdoptedSession(req, target, owner, body) {
1305
+ // Connection facts are copied onto the session rather than referenced by id:
1306
+ // a wrapper restored after a server restart must be able to rebuild its
1307
+ // command even if the host registry was edited in the meantime.
1308
+ const adopt = {
1309
+ location: target.location,
1310
+ socketPath: target.socketPath,
1311
+ targetSession: target.sessionName,
1312
+ viewSession: '',
1313
+ paneCurrentPath: target.workingDir,
1314
+ };
1315
+ if (target.location === 'docker') {
1316
+ const hosts = await readDockerHosts(CODEMAN_CONFIG_DIR);
1317
+ const host = hosts.find((h) => h.id === target.hostId);
1318
+ if (!target.containerName) {
1319
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Container name missing for a docker candidate');
1320
+ }
1321
+ adopt.docker = {
1322
+ hostId: target.hostId ?? '',
1323
+ label: target.hostLabel ?? target.containerName,
1324
+ engine: host?.engine ?? 'docker',
1325
+ containerName: target.containerName,
1326
+ daemonHost: host?.daemonHost,
1327
+ context: host?.context,
1328
+ };
1329
+ }
1330
+ else if (target.location === 'remote') {
1331
+ const host = (await readRemoteHosts(CODEMAN_CONFIG_DIR)).find((h) => h.id === target.hostId);
1332
+ if (!host)
1333
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Remote host not found');
1334
+ adopt.remote = {
1335
+ hostId: host.id,
1336
+ label: host.label,
1337
+ host: host.host,
1338
+ username: host.username,
1339
+ port: host.port,
1340
+ identityFile: host.identityFile,
1341
+ socksProxy: host.socksProxy,
1342
+ jumpHost: host.jumpHost,
1343
+ extraSshOptions: host.extraSshOptions,
1344
+ };
1345
+ }
1346
+ const adoptHistoryConfig = await ctx.getTerminalHistoryConfig();
1347
+ // ⚠️ The wrapper's `workingDir` is a NEUTRAL LOCAL path, never the foreign
1348
+ // pane's cwd. That cwd is an observation about another machine: it can carry
1349
+ // characters outside SAFE_PATH_PATTERN (`~/c++`, `Program Files (x86)`), which
1350
+ // makes `createSession` throw so the tab never starts at all, and when it is a
1351
+ // container or ssh path every local consumer of `workingDir` (the Files panel,
1352
+ // the boot hook sweep, the fs watchers) reads it as a path on THIS host. The
1353
+ // foreign cwd stays on `adopt.paneCurrentPath`, which is for display only.
1354
+ // `/tmp` is the same neutral cwd `resolveMuxAttachCwd` already gives the pane.
1355
+ const session = new Session({
1356
+ workingDir: ADOPTED_WRAPPER_WORKING_DIR,
1357
+ mode: target.mode,
1358
+ name: body.name || target.sessionName,
1359
+ mux: ctx.mux,
1360
+ useMux: true,
1361
+ tmuxHistoryLimit: adoptHistoryConfig.tmuxHistoryLimit,
1362
+ adopt,
1363
+ owner,
1364
+ parentSessionId: resolveParentSessionId(ctx, req, body.parentSessionId, owner),
1365
+ });
1366
+ // The view session name is derived from the Codeman session id, so it can only
1367
+ // be filled once the Session exists.
1368
+ adopt.viewSession = foreignViewSessionName(session.id);
1369
+ await ctx.addSession(session);
1370
+ ctx.store.incrementSessionsCreated();
1371
+ ctx.persistSessionState(session);
1372
+ await ctx.setupSessionListeners(session);
1373
+ getLifecycleLog().log({ event: 'created', sessionId: session.id, name: session.name });
1374
+ invalidateForeignCache();
1375
+ const lightState = ctx.getSessionStateWithRespawn(session);
1376
+ ctx.broadcast(SseEvent.SessionCreated, lightState);
1377
+ return { session: lightState, adopted: true };
1378
+ }
1175
1379
  // ========== Rename Session ==========
1176
1380
  app.put('/api/sessions/:id/name', async (req) => {
1177
1381
  const { id } = req.params;
@@ -1225,6 +1429,10 @@ options = {}) {
1225
1429
  if (session.remote || session.docker) {
1226
1430
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Custom model endpoints are not supported for remote (SSH) or Docker sessions yet');
1227
1431
  }
1432
+ // Its own check: this restarts the CLI in the pane, and an adopted pane's CLI is not ours to restart.
1433
+ if (session.isAdopted) {
1434
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Custom model endpoints are not available for adopted sessions: Codeman did not start this agent');
1435
+ }
1228
1436
  if (session.isBusy()) {
1229
1437
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
1230
1438
  }
@@ -1247,8 +1455,29 @@ options = {}) {
1247
1455
  if (!endpoint) {
1248
1456
  return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Model endpoint not found');
1249
1457
  }
1250
- const contextLength = endpoint.modelContextLengths?.[body.modelId];
1458
+ // Discovery's per-model figure wins; the endpoint's user-set default covers models
1459
+ // whose size discovery could not determine (a gateway that states none, say). With
1460
+ // neither, the existing "no override, CLI assumes its own default" behaviour stands.
1461
+ const contextLength = resolveCustomModelContextLength(endpoint, body.modelId);
1251
1462
  const slotCount = endpoint.modelSlotCounts?.[body.modelId];
1463
+ // A CLI whose config alone cannot select the model also gets its `model` launch param
1464
+ // forced (pi/omp `custom/<id>`, grok's block name, and codex now carries its bare
1465
+ // selected id too). The argv engine DROPS a token that
1466
+ // fails its pattern rather than quoting it, which would silently launch the CLI on its
1467
+ // own default provider again, so refuse an id the pattern cannot carry up front.
1468
+ // The check runs on the PURE build result, before applyCustomModelInjection below:
1469
+ // the forced launch id only comes out of that build, and writing first would mean a
1470
+ // refused id already replaced the config dir a live codex session uses as CODEX_HOME.
1471
+ // It also runs before the two confirmations, so nobody agrees to evict a model only
1472
+ // to be told the id was never usable.
1473
+ const modelSpec = entry.launch.params.model;
1474
+ const injection = buildCustomModelInjection(entry, endpoint, body.modelId, contextLength);
1475
+ const forcedLaunchModel = injection.kind === 'unsupported' ? undefined : injection.launchModel;
1476
+ if (forcedLaunchModel !== undefined &&
1477
+ modelSpec?.type === 'token' &&
1478
+ !matchesPattern(modelSpec.pattern, forcedLaunchModel)) {
1479
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(body.modelId)} cannot be passed to ${session.mode} on its command line`);
1480
+ }
1252
1481
  // Some CLIs (today: only claude) carry enough of their own fixed system-prompt/tool-
1253
1482
  // schema overhead that a small enough real context guarantees a first-message failure
1254
1483
  // no matter what CLAUDE_CODE_MAX_CONTEXT_TOKENS says — confirmed live at ~36.4K tokens
@@ -1306,21 +1535,10 @@ options = {}) {
1306
1535
  return { requiresConfirmation: true, currentlyLoadedModel: currentlyLoaded, affectedSessions };
1307
1536
  }
1308
1537
  }
1309
- // A CLI whose config alone cannot select the model also gets its `model` launch param
1310
- // forced (pi/omp `custom/<id>`, grok's block name). The argv engine DROPS a token that
1311
- // fails its pattern rather than quoting it, which would silently launch the CLI on its
1312
- // own default provider again, so refuse an id the pattern cannot carry up front.
1313
- const modelSpec = entry.launch.params.model;
1314
- const applied = applyCustomModelInjection(entry, endpoint, body.modelId, session.id, contextLength, session.workingDir);
1538
+ const applied = applyCustomModelInjection(entry, endpoint, body.modelId, session.id, contextLength, session.workingDir, await guardSessionConfigDirHome(entry, (envVar) => session.ownEnvValue(envVar)));
1315
1539
  if (!applied) {
1316
1540
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${session.mode} has no known custom-model mechanism`);
1317
1541
  }
1318
- if (applied.launchModel !== undefined &&
1319
- modelSpec?.type === 'token' &&
1320
- !matchesPattern(modelSpec.pattern, applied.launchModel)) {
1321
- removeConfigDir(applied.configDir);
1322
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(body.modelId)} cannot be passed to ${session.mode} on its command line`);
1323
- }
1324
1542
  const { previousConfigDir } = session.setCustomModel({
1325
1543
  endpointId: endpoint.id,
1326
1544
  modelId: body.modelId,
@@ -1328,6 +1546,7 @@ options = {}) {
1328
1546
  envKeys: applied.envKeys,
1329
1547
  configDir: applied.configDir,
1330
1548
  launchModel: applied.launchModel,
1549
+ homeEnvVar: applied.homeEnvVar,
1331
1550
  }, applied.envOverrides);
1332
1551
  // Clean up the OLD config dir on disk, unless the new one happens to reuse the same
1333
1552
  // path (same session, configDir kind again) — never delete the dir we just wrote.
@@ -1361,7 +1580,9 @@ options = {}) {
1361
1580
  // Called for its existence/ownership 404 side effect only — demoteOrRemoveSession
1362
1581
  // below re-looks-up the record by id, so the returned SessionState is unused here.
1363
1582
  findPersistedSessionOrFail(ctx.store, id, req);
1364
- ctx.store.demoteOrRemoveSession(id);
1583
+ // The edit history goes with the record (same rule as `cleanupSession`).
1584
+ if (ctx.store.demoteOrRemoveSession(id) === 'removed')
1585
+ ctx.forgetEditHistory(id);
1365
1586
  // A detached session kept its agent inbox (server.ts _doCleanupSession drops it
1366
1587
  // only on killMux); retiring the row is the delete, so the mail goes now.
1367
1588
  agentInbox.drop(id);
@@ -1465,6 +1686,11 @@ options = {}) {
1465
1686
  const { id } = req.params;
1466
1687
  const { prompt } = parseBody(RunPromptSchema, req.body);
1467
1688
  const session = findSessionOrFail(ctx, id, req);
1689
+ // Its own check: `runPrompt` starts a new local claude, and the answer has to say so
1690
+ // here rather than as a `session:error` after a success.
1691
+ if (session.isAdopted) {
1692
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'One-shot prompts are not available for adopted sessions: Codeman did not start this agent');
1693
+ }
1468
1694
  if (session.isBusy()) {
1469
1695
  return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
1470
1696
  }
@@ -1505,6 +1731,10 @@ options = {}) {
1505
1731
  // about shell, and they disagree upstream too — reconciling them is a behaviour change
1506
1732
  // and belongs in its own PR, not in a refactor that is meant to change nothing.
1507
1733
  if (!isExternalCliMode(session.mode) &&
1734
+ // ⚠️ And never for an adopted session, whose mode can be claude while the
1735
+ // agent in that pane belongs to someone else. Kept as its own clause for
1736
+ // the same reason the respawn and Ralph routes keep theirs separate.
1737
+ !session.isAdopted &&
1508
1738
  ctx.store.getConfig().ralphEnabled &&
1509
1739
  !session.ralphTracker.autoEnableDisabled) {
1510
1740
  autoConfigureRalph(session, session.workingDir, ctx);
@@ -2372,7 +2602,10 @@ options = {}) {
2372
2602
  * effort: the tab rename has already happened and must not fail on this.
2373
2603
  */
2374
2604
  async function syncClaudeTitle(session) {
2375
- if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' || session.remote || session.docker)
2605
+ if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' ||
2606
+ session.remote ||
2607
+ session.docker ||
2608
+ session.isAdopted)
2376
2609
  return;
2377
2610
  try {
2378
2611
  const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
@@ -2451,15 +2684,24 @@ options = {}) {
2451
2684
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
2452
2685
  return candidates[0] ?? null;
2453
2686
  }
2454
- app.get('/api/sessions/:id/last-response', async (req) => {
2455
- const { id } = req.params;
2456
- const session = findSessionOrFail(ctx, id, req);
2687
+ // The body of GET /api/sessions/:id/last-response, so the Discord bridge reads the same transcript
2688
+ // the response viewer does (per-CLI reader, pane fallback) without a second copy of this logic.
2689
+ const lastResponseFor = async (session, context) => {
2690
+ // ⚠️ An adopted session has no transcript Codeman can name. Every reader below pairs one by
2691
+ // the id Codeman launched the CLI with or by the session's folder, and neither means anything
2692
+ // here: the CLI chose its own id, and the folder is the neutral `/tmp`, so the newest file for
2693
+ // that folder would be another conversation's answer shown as this one's. Nothing said yet is
2694
+ // the honest answer.
2695
+ if (session.isAdopted) {
2696
+ return context === 'full' || context === 'turn'
2697
+ ? { text: '', timestamp: '', messages: [] }
2698
+ : { text: '', timestamp: '' };
2699
+ }
2457
2700
  // Codex sessions don't write to ~/.claude/projects — their transcripts
2458
2701
  // live in ~/.codex/sessions/**. Branch to a Codex-specific reader so the
2459
2702
  // response-viewer works for Codex panes too.
2460
2703
  if (getCli(session.mode)?.capabilities.transcript === 'codex-rollout') {
2461
- const codexQuery = req.query;
2462
- return await readCodexLastResponse(session, codexQuery.context === 'full');
2704
+ return await readCodexLastResponse(session, context === 'full');
2463
2705
  }
2464
2706
  // DeepSeek Harness writes a real structured transcript under
2465
2707
  // `$DSH_HOME/sessions/**`, so read that rather than segmenting the pane.
@@ -2477,8 +2719,7 @@ options = {}) {
2477
2719
  // would starve on an answer that exists. Those configurations keep the
2478
2720
  // pane segmenter below: coarse, but the real conversation.
2479
2721
  if (getCli(session.mode)?.capabilities.transcript === 'deepseek-zstd' && !session.docker && !session.remote) {
2480
- const deepSeekQuery = req.query;
2481
- const full = deepSeekQuery.context === 'full';
2722
+ const full = context === 'full';
2482
2723
  const transcript = await readDeepSeekLastResponse(session, { blocks: full });
2483
2724
  if (transcript) {
2484
2725
  return {
@@ -2495,7 +2736,6 @@ options = {}) {
2495
2736
  // the pane IS the transcript for these CLIs. Codex is already handled above,
2496
2737
  // where a real rollout file is the better source.
2497
2738
  if (isExternalCliTranscriptMode(session.mode)) {
2498
- const externalQuery = req.query;
2499
2739
  // A repainting full-screen TUI (copilot) streams absolute cursor moves, not lines, so the
2500
2740
  // buffer cannot be segmented; read the pane's visible screen instead, and fall back to the
2501
2741
  // buffer when there is no tmux pane to ask (direct PTY, tests).
@@ -2507,7 +2747,7 @@ options = {}) {
2507
2747
  text: getLastTranscriptResponse(blocks),
2508
2748
  timestamp: '',
2509
2749
  hasContext: blocks.length > 0,
2510
- messages: externalQuery.context === 'full' ? blocks : undefined,
2750
+ messages: context === 'full' ? blocks : undefined,
2511
2751
  };
2512
2752
  }
2513
2753
  // Scan ~/.claude/projects/*/ for the transcript file
@@ -2528,11 +2768,10 @@ options = {}) {
2528
2768
  void persistDockerCaseClaudeSessionId(CODEMAN_CONFIG_DIR, session.docker.containerName, activeId).catch(() => { });
2529
2769
  }
2530
2770
  }
2531
- const query = req.query;
2532
2771
  // `turn` is the brief view's context: the last ANSWERED turn's assistant
2533
2772
  // messages, so a multi-row answer is not reduced to its final row. `text`
2534
2773
  // stays the last assistant row in every mode (agent pollers hash it).
2535
- const wantsMessages = query.context === 'full' || query.context === 'turn';
2774
+ const wantsMessages = context === 'full' || context === 'turn';
2536
2775
  const claudeSessionId = session.claudeSessionId || session.id;
2537
2776
  const transcript = await findClaudeTranscript(projectsDir, claudeSessionId, session.id);
2538
2777
  if (!transcript) {
@@ -2547,7 +2786,7 @@ options = {}) {
2547
2786
  try {
2548
2787
  const content = await fs.readFile(transcript.path, 'utf8');
2549
2788
  const parsed = parseClaudeResponseTranscript(content, wantsMessages);
2550
- if (query.context === 'turn') {
2789
+ if (context === 'turn') {
2551
2790
  return {
2552
2791
  text: parsed.text,
2553
2792
  timestamp: parsed.timestamp,
@@ -2559,6 +2798,12 @@ options = {}) {
2559
2798
  catch {
2560
2799
  return wantsMessages ? { text: '', timestamp: '', messages: [] } : { text: '', timestamp: '' };
2561
2800
  }
2801
+ };
2802
+ ctx.lastResponseFor = lastResponseFor;
2803
+ app.get('/api/sessions/:id/last-response', async (req) => {
2804
+ const { id } = req.params;
2805
+ const session = findSessionOrFail(ctx, id, req);
2806
+ return lastResponseFor(session, req.query.context);
2562
2807
  });
2563
2808
  function isCodexInjectedContext(text) {
2564
2809
  return (/^# AGENTS\.md instructions\b/i.test(text) ||
@@ -3120,6 +3365,13 @@ options = {}) {
3120
3365
  const { id } = req.params;
3121
3366
  const body = parseBody(AutoClearSchema, req.body, 'Invalid request body');
3122
3367
  const session = findSessionOrFail(ctx, id, req);
3368
+ // ⚠️ Separate from the external-CLI gate on purpose: an adopted session's mode
3369
+ // can perfectly well BE claude, and this drives the pane of a session someone
3370
+ // else is using. Merging the two checks means a future change to the
3371
+ // external-CLI rule silently reopens this one.
3372
+ if (session.isAdopted) {
3373
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Auto-clear is not available for adopted sessions: Codeman did not start this agent and must not drive it');
3374
+ }
3123
3375
  session.setAutoClear(body.enabled, body.threshold);
3124
3376
  persistAndBroadcastSession(ctx, session);
3125
3377
  return {
@@ -3137,6 +3389,13 @@ options = {}) {
3137
3389
  const { id } = req.params;
3138
3390
  const body = parseBody(AutoCompactSchema, req.body, 'Invalid request body');
3139
3391
  const session = findSessionOrFail(ctx, id, req);
3392
+ // ⚠️ Separate from the external-CLI gate on purpose: an adopted session's mode
3393
+ // can perfectly well BE claude, and this drives the pane of a session someone
3394
+ // else is using. Merging the two checks means a future change to the
3395
+ // external-CLI rule silently reopens this one.
3396
+ if (session.isAdopted) {
3397
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Auto-compact is not available for adopted sessions: Codeman did not start this agent and must not drive it');
3398
+ }
3140
3399
  session.setAutoCompact(body.enabled, body.threshold, body.prompt);
3141
3400
  persistAndBroadcastSession(ctx, session);
3142
3401
  return {
@@ -3188,8 +3447,8 @@ options = {}) {
3188
3447
  const { id } = req.params;
3189
3448
  const body = parseBody(GitFetchSettingsSchema, req.body, 'Invalid request body');
3190
3449
  const session = findSessionOrFail(ctx, id, req);
3191
- if (session.remote || session.docker) {
3192
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git fetch does not run for remote or Docker sessions');
3450
+ if (session.remote || session.docker || session.isAdopted) {
3451
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git fetch does not run for remote, Docker or adopted sessions');
3193
3452
  }
3194
3453
  const enabled = 'enabled' in body ? (body.enabled ?? undefined) : session.gitFetchEnabled;
3195
3454
  const intervalSeconds = 'intervalSeconds' in body ? (body.intervalSeconds ?? undefined) : session.gitFetchIntervalSeconds;
@@ -3210,6 +3469,13 @@ options = {}) {
3210
3469
  const { id } = req.params;
3211
3470
  const body = parseBody(AutoResumeSchema, req.body, 'Invalid request body');
3212
3471
  const session = findSessionOrFail(ctx, id, req);
3472
+ // ⚠️ Separate from the external-CLI gate on purpose: an adopted session's mode
3473
+ // can perfectly well BE claude, and this drives the pane of a session someone
3474
+ // else is using. Merging the two checks means a future change to the
3475
+ // external-CLI rule silently reopens this one.
3476
+ if (session.isAdopted) {
3477
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Auto-resume is not available for adopted sessions: Codeman did not start this agent and must not drive it');
3478
+ }
3213
3479
  session.setAutoResume(body.enabled);
3214
3480
  persistAndBroadcastSession(ctx, session);
3215
3481
  return {
@@ -3271,6 +3537,10 @@ options = {}) {
3271
3537
  if (body.enabled && session.remote) {
3272
3538
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for remote sessions');
3273
3539
  }
3540
+ // An adopted session's folder is the neutral wrapper folder, not the foreign pane's work.
3541
+ if (body.enabled && session.isAdopted) {
3542
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for adopted sessions');
3543
+ }
3274
3544
  if (body.enabled) {
3275
3545
  imageWatcher.watchSession(session.id, session.workingDir);
3276
3546
  }
@@ -3786,12 +4056,12 @@ options = {}) {
3786
4056
  // unknown endpoint, a model id the CLI's argv pattern can't carry) rather than trusting
3787
4057
  // a lighter version of them, since this is the same server-side authority reached a
3788
4058
  // different way, not a separate, less-checked path.
3789
- let qsCustomModelEnvOverrides = qsGatedEnvOverrides;
3790
- // Only the INJECTED keys (never the caller's envOverrides merged in) — this is what
3791
- // setCustomModel() bookkeeping must be given below. The Session constructor already
3792
- // applies qsCustomModelEnvOverrides (the full merged set) directly; re-merging that
3793
- // full set into setCustomModel() would put CLAUDE_CODE_EFFORT_LEVEL back after the
3794
- // constructor stripped it (see setCustomModel()'s own doc comment in session.ts).
4059
+ //
4060
+ // Only the INJECTED keys (never the caller's envOverrides merged in): setCustomModel()
4061
+ // merges these over what the Session constructor took, which is how it learns which of
4062
+ // the caller's values the selection replaced. Handing the constructor the merged set
4063
+ // would hide them, and giving setCustomModel() that set would put
4064
+ // CLAUDE_CODE_EFFORT_LEVEL back after the constructor stripped it.
3795
4065
  let qsCustomModelAppliedEnvOverrides;
3796
4066
  let qsCustomModelLaunchModel;
3797
4067
  let qsCustomModelSessionId;
@@ -3808,8 +4078,21 @@ options = {}) {
3808
4078
  const cmEndpoint = cmHosts.find((h) => h.id === customModel.endpointId);
3809
4079
  if (!cmEndpoint)
3810
4080
  return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Model endpoint not found');
3811
- const cmContextLength = cmEndpoint.modelContextLengths?.[customModel.modelId];
4081
+ // Per-model discovery figure first, user-set endpoint default second, nothing third
4082
+ // (see the dedicated route's own comment above).
4083
+ const cmContextLength = resolveCustomModelContextLength(cmEndpoint, customModel.modelId);
3812
4084
  const cmSlotCount = cmEndpoint.modelSlotCounts?.[customModel.modelId];
4085
+ // See the dedicated route: refused on the pure build, before either confirmation, so
4086
+ // nobody agrees to evict a model for an id that could never launch, and before anything
4087
+ // is written.
4088
+ const cmModelSpec = cmEntry.launch.params.model;
4089
+ const cmInjection = buildCustomModelInjection(cmEntry, cmEndpoint, customModel.modelId, cmContextLength);
4090
+ const cmForcedLaunchModel = cmInjection.kind === 'unsupported' ? undefined : cmInjection.launchModel;
4091
+ if (cmForcedLaunchModel !== undefined &&
4092
+ cmModelSpec?.type === 'token' &&
4093
+ !matchesPattern(cmModelSpec.pattern, cmForcedLaunchModel)) {
4094
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(customModel.modelId)} cannot be passed to ${mode} on its command line`);
4095
+ }
3813
4096
  // See the dedicated route's own comment for the full reasoning: some CLIs' own fixed
3814
4097
  // overhead can exceed a small enough real context on the very first message,
3815
4098
  // regardless of contextLengthVar. Warn before creating a session that's certain to
@@ -3861,18 +4144,10 @@ options = {}) {
3861
4144
  // with, not a placeholder: `new Session({ id: ... })` accepts an explicit id for
3862
4145
  // exactly this reason.
3863
4146
  qsCustomModelSessionId = randomUUID();
3864
- const cmApplied = applyCustomModelInjection(cmEntry, cmEndpoint, customModel.modelId, qsCustomModelSessionId, cmContextLength, workspacePath);
4147
+ const cmApplied = applyCustomModelInjection(cmEntry, cmEndpoint, customModel.modelId, qsCustomModelSessionId, cmContextLength, workspacePath, await guardSessionConfigDirHome(cmEntry, (envVar) => qsGatedEnvOverrides?.[envVar]));
3865
4148
  if (!cmApplied) {
3866
4149
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `${mode} has no known custom-model mechanism`);
3867
4150
  }
3868
- const cmModelSpec = cmEntry.launch.params.model;
3869
- if (cmApplied.launchModel !== undefined &&
3870
- cmModelSpec?.type === 'token' &&
3871
- !matchesPattern(cmModelSpec.pattern, cmApplied.launchModel)) {
3872
- removeConfigDir(cmApplied.configDir);
3873
- return createErrorResponse(ApiErrorCode.INVALID_INPUT, `Model id ${JSON.stringify(customModel.modelId)} cannot be passed to ${mode} on its command line`);
3874
- }
3875
- qsCustomModelEnvOverrides = { ...qsGatedEnvOverrides, ...cmApplied.envOverrides };
3876
4151
  qsCustomModelAppliedEnvOverrides = cmApplied.envOverrides;
3877
4152
  qsCustomModelLaunchModel = cmApplied.launchModel;
3878
4153
  qsCustomModelBookkeeping = {
@@ -3882,6 +4157,7 @@ options = {}) {
3882
4157
  envKeys: cmApplied.envKeys,
3883
4158
  configDir: cmApplied.configDir,
3884
4159
  launchModel: cmApplied.launchModel,
4160
+ homeEnvVar: cmApplied.homeEnvVar,
3885
4161
  };
3886
4162
  // Actually kick off llama-swap's load now — see the dedicated apply route's own
3887
4163
  // comment on triggerLlamaSwapLoad for why this can't just wait on the launched CLI's
@@ -3912,7 +4188,7 @@ options = {}) {
3912
4188
  copilotConfig: mode === 'copilot' ? qsGatedCopilotConfig : undefined,
3913
4189
  deepSeekConfig: mode === 'deepseek' ? qsGatedDeepSeekConfig : undefined,
3914
4190
  ompConfig: qsResolvedOmpConfig,
3915
- envOverrides: qsCustomModelEnvOverrides,
4191
+ envOverrides: qsGatedEnvOverrides,
3916
4192
  effort,
3917
4193
  advisorModel,
3918
4194
  remote,
@@ -3941,10 +4217,10 @@ options = {}) {
3941
4217
  }
3942
4218
  const session = new Session(qsSessionOptions);
3943
4219
  // Records the selection for session.customModel/getCustomModelForPersist() and future
3944
- // clear/switch calls — the actual env vars and launch-model config are already part of
3945
- // the launch above (constructor envOverrides, piConfig/grokConfig/ompConfig.model), so
3946
- // this is bookkeeping only, never a restart: setCustomModel() is synchronous state, no
3947
- // tmux IO of its own (see its own doc comment in session.ts).
4220
+ // clear/switch calls, and merges the injected env, which the launch below reads along
4221
+ // with the launch-model config set above (piConfig/grokConfig/ompConfig.model). Nothing
4222
+ // has started yet, so this is never a restart: setCustomModel() is synchronous state,
4223
+ // no tmux IO of its own (see its own doc comment in session.ts).
3948
4224
  if (qsCustomModelBookkeeping) {
3949
4225
  session.setCustomModel(qsCustomModelBookkeeping, qsCustomModelAppliedEnvOverrides);
3950
4226
  }
@@ -5016,6 +5292,13 @@ options = {}) {
5016
5292
  reply.code(400);
5017
5293
  return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Prompt uploads are not supported for remote (SSH) sessions');
5018
5294
  }
5295
+ // An adopted session has no workspace of ours: its folder is the neutral `/tmp`, shared with
5296
+ // whatever else runs there, and an agent in a container or on an ssh host could not read it.
5297
+ // The foreign pane's own folder is not ours to write into either.
5298
+ if (session.isAdopted) {
5299
+ reply.code(400);
5300
+ return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Prompt uploads are not supported for adopted sessions');
5301
+ }
5019
5302
  // The upload is written under session.workingDir (ensureUploadDir below), and
5020
5303
  // a live session's workspace can sit on a network mount that stopped answering.
5021
5304
  // Probe it bounded before the first disk touch, as #516 requires for a path the