aicodeman 1.41.0 → 1.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +58 -8
  2. package/dist/attachment-registry.d.ts +2 -0
  3. package/dist/attachment-registry.d.ts.map +1 -1
  4. package/dist/attachment-registry.js +14 -3
  5. package/dist/attachment-registry.js.map +1 -1
  6. package/dist/cli-agent.d.ts +2 -0
  7. package/dist/cli-agent.d.ts.map +1 -1
  8. package/dist/cli-agent.js +12 -8
  9. package/dist/cli-agent.js.map +1 -1
  10. package/dist/cli.d.ts +25 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +97 -11
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config/buffer-limits.d.ts +9 -0
  15. package/dist/config/buffer-limits.d.ts.map +1 -1
  16. package/dist/config/buffer-limits.js +9 -0
  17. package/dist/config/buffer-limits.js.map +1 -1
  18. package/dist/config/cli-registry/schema.d.ts +8 -0
  19. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  20. package/dist/config/cli-registry/schema.js +12 -0
  21. package/dist/config/cli-registry/schema.js.map +1 -1
  22. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  23. package/dist/config/cli-registry/stock.js +42 -0
  24. package/dist/config/cli-registry/stock.js.map +1 -1
  25. package/dist/config/cli-registry/types.d.ts +35 -0
  26. package/dist/config/cli-registry/types.d.ts.map +1 -1
  27. package/dist/folder-listing.d.ts +94 -0
  28. package/dist/folder-listing.d.ts.map +1 -0
  29. package/dist/folder-listing.js +241 -0
  30. package/dist/folder-listing.js.map +1 -0
  31. package/dist/hooks-config.d.ts +36 -16
  32. package/dist/hooks-config.d.ts.map +1 -1
  33. package/dist/hooks-config.js +45 -20
  34. package/dist/hooks-config.js.map +1 -1
  35. package/dist/html-view-capabilities.d.ts +54 -0
  36. package/dist/html-view-capabilities.d.ts.map +1 -0
  37. package/dist/html-view-capabilities.js +103 -0
  38. package/dist/html-view-capabilities.js.map +1 -0
  39. package/dist/mcp-server.d.ts +114 -0
  40. package/dist/mcp-server.d.ts.map +1 -0
  41. package/dist/mcp-server.js +917 -0
  42. package/dist/mcp-server.js.map +1 -0
  43. package/dist/mcp-sync.d.ts +5 -0
  44. package/dist/mcp-sync.d.ts.map +1 -1
  45. package/dist/mcp-sync.js +51 -5
  46. package/dist/mcp-sync.js.map +1 -1
  47. package/dist/mux-interface.d.ts +36 -1
  48. package/dist/mux-interface.d.ts.map +1 -1
  49. package/dist/remote-files.d.ts +37 -0
  50. package/dist/remote-files.d.ts.map +1 -1
  51. package/dist/remote-files.js +103 -0
  52. package/dist/remote-files.js.map +1 -1
  53. package/dist/remote-pane-history.d.ts +103 -0
  54. package/dist/remote-pane-history.d.ts.map +1 -0
  55. package/dist/remote-pane-history.js +258 -0
  56. package/dist/remote-pane-history.js.map +1 -0
  57. package/dist/remote-ssh-limiter.d.ts +16 -3
  58. package/dist/remote-ssh-limiter.d.ts.map +1 -1
  59. package/dist/remote-ssh-limiter.js +37 -6
  60. package/dist/remote-ssh-limiter.js.map +1 -1
  61. package/dist/session.d.ts +29 -1
  62. package/dist/session.d.ts.map +1 -1
  63. package/dist/session.js +53 -0
  64. package/dist/session.js.map +1 -1
  65. package/dist/tab-layout-legacy-order.d.ts.map +1 -1
  66. package/dist/tab-layout-legacy-order.js +72 -13
  67. package/dist/tab-layout-legacy-order.js.map +1 -1
  68. package/dist/tab-layout-service.d.ts +33 -2
  69. package/dist/tab-layout-service.d.ts.map +1 -1
  70. package/dist/tab-layout-service.js +102 -7
  71. package/dist/tab-layout-service.js.map +1 -1
  72. package/dist/tab-layout.d.ts +9 -0
  73. package/dist/tab-layout.d.ts.map +1 -1
  74. package/dist/tab-layout.js +15 -2
  75. package/dist/tab-layout.js.map +1 -1
  76. package/dist/tmux-manager.d.ts +71 -2
  77. package/dist/tmux-manager.d.ts.map +1 -1
  78. package/dist/tmux-manager.js +169 -18
  79. package/dist/tmux-manager.js.map +1 -1
  80. package/dist/types/session.d.ts +53 -0
  81. package/dist/types/session.d.ts.map +1 -1
  82. package/dist/types/session.js.map +1 -1
  83. package/dist/usage-telemetry.d.ts +44 -7
  84. package/dist/usage-telemetry.d.ts.map +1 -1
  85. package/dist/usage-telemetry.js +72 -7
  86. package/dist/usage-telemetry.js.map +1 -1
  87. package/dist/web/approval-inbox.d.ts +12 -0
  88. package/dist/web/approval-inbox.d.ts.map +1 -1
  89. package/dist/web/approval-inbox.js +1 -0
  90. package/dist/web/approval-inbox.js.map +1 -1
  91. package/dist/web/middleware/auth.d.ts.map +1 -1
  92. package/dist/web/middleware/auth.js +33 -0
  93. package/dist/web/middleware/auth.js.map +1 -1
  94. package/dist/web/ports/session-port.d.ts +8 -1
  95. package/dist/web/ports/session-port.d.ts.map +1 -1
  96. package/dist/web/public/admin-ui.js.gz +0 -0
  97. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  98. package/dist/web/public/app.24689381.js +43 -0
  99. package/dist/web/public/app.24689381.js.br +0 -0
  100. package/dist/web/public/app.24689381.js.gz +0 -0
  101. package/dist/web/public/approvals-ui.js +4 -0
  102. package/dist/web/public/approvals-ui.js.br +0 -0
  103. package/dist/web/public/approvals-ui.js.gz +0 -0
  104. package/dist/web/public/{constants.bc9c1f63.js → constants.17d77b65.js} +395 -7
  105. package/dist/web/public/constants.17d77b65.js.br +0 -0
  106. package/dist/web/public/constants.17d77b65.js.gz +0 -0
  107. package/dist/web/public/cron-ui.js.gz +0 -0
  108. package/dist/web/public/entrance-animations.js.gz +0 -0
  109. package/dist/web/public/gesture/gesture-codeman.js +112 -6
  110. package/dist/web/public/git-status-ui.js.gz +0 -0
  111. package/dist/web/public/home-sessions.js.gz +0 -0
  112. package/dist/web/public/host-wake-ui.js.gz +0 -0
  113. package/dist/web/public/i18n.9afd8e4d.js +2 -0
  114. package/dist/web/public/i18n.9afd8e4d.js.br +0 -0
  115. package/dist/web/public/i18n.9afd8e4d.js.gz +0 -0
  116. package/dist/web/public/{image-input.e00471c6.js → image-input.9ed1d0d2.js} +186 -16
  117. package/dist/web/public/image-input.9ed1d0d2.js.br +0 -0
  118. package/dist/web/public/image-input.9ed1d0d2.js.gz +0 -0
  119. package/dist/web/public/index.html +105 -23
  120. package/dist/web/public/index.html.br +0 -0
  121. package/dist/web/public/index.html.gz +0 -0
  122. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  123. package/dist/web/public/{keyboard-accessory.c1e7c7b7.js → keyboard-accessory.c458eea8.js} +115 -35
  124. package/dist/web/public/keyboard-accessory.c458eea8.js.br +0 -0
  125. package/dist/web/public/keyboard-accessory.c458eea8.js.gz +0 -0
  126. package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
  127. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  128. package/dist/web/public/mobile-overview.js +11 -2
  129. package/dist/web/public/mobile-overview.js.br +0 -0
  130. package/dist/web/public/mobile-overview.js.gz +0 -0
  131. package/dist/web/public/mobile.2dff1580.css +1 -0
  132. package/dist/web/public/mobile.2dff1580.css.br +0 -0
  133. package/dist/web/public/mobile.2dff1580.css.gz +0 -0
  134. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  135. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  136. package/dist/web/public/{panels-ui.71846fcb.js → panels-ui.73ec0137.js} +87 -87
  137. package/dist/web/public/panels-ui.73ec0137.js.br +0 -0
  138. package/dist/web/public/panels-ui.73ec0137.js.gz +0 -0
  139. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  140. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  141. package/dist/web/public/readmymind-ui.js.gz +0 -0
  142. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  143. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  144. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  145. package/dist/web/public/session-lineage.js.gz +0 -0
  146. package/dist/web/public/{session-ui.f731602c.js → session-ui.dbc6819b.js} +33 -33
  147. package/dist/web/public/session-ui.dbc6819b.js.br +0 -0
  148. package/dist/web/public/session-ui.dbc6819b.js.gz +0 -0
  149. package/dist/web/public/{settings-ui.4e33390f.js → settings-ui.81ac264e.js} +9 -9
  150. package/dist/web/public/settings-ui.81ac264e.js.br +0 -0
  151. package/dist/web/public/settings-ui.81ac264e.js.gz +0 -0
  152. package/dist/web/public/spreadsheet-preview-worker.js +11 -0
  153. package/dist/web/public/spreadsheet-preview-worker.js.br +0 -0
  154. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  155. package/dist/web/public/{spreadsheet-preview.af82fa2f.js → spreadsheet-preview.75c751de.js} +1 -1
  156. package/dist/web/public/spreadsheet-preview.75c751de.js.br +0 -0
  157. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  158. package/dist/web/public/spreadsheet-xlsx-core.js +110 -0
  159. package/dist/web/public/spreadsheet-xlsx-core.js.br +0 -0
  160. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  161. package/dist/web/public/styles.900a28e4.css +1 -0
  162. package/dist/web/public/styles.900a28e4.css.br +0 -0
  163. package/dist/web/public/styles.900a28e4.css.gz +0 -0
  164. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  165. package/dist/web/public/sw.js +6 -2
  166. package/dist/web/public/sw.js.br +0 -0
  167. package/dist/web/public/sw.js.gz +0 -0
  168. package/dist/web/public/tab-layout-browser.2d6189f7.js +1 -0
  169. package/dist/web/public/tab-layout-browser.2d6189f7.js.br +0 -0
  170. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  171. package/dist/web/public/{tab-rail-resize.a5867793.js → tab-rail-resize.7157e0bd.js} +1 -1
  172. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  173. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  174. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  175. package/dist/web/public/terminal-split.js.gz +0 -0
  176. package/dist/web/public/terminal-tile.js +44 -24
  177. package/dist/web/public/terminal-tile.js.br +0 -0
  178. package/dist/web/public/terminal-tile.js.gz +0 -0
  179. package/dist/web/public/terminal-ui.858f5da5.js +2 -0
  180. package/dist/web/public/terminal-ui.858f5da5.js.br +0 -0
  181. package/dist/web/public/terminal-ui.858f5da5.js.gz +0 -0
  182. package/dist/web/public/tile-grid.js +439 -18
  183. package/dist/web/public/tile-grid.js.br +0 -0
  184. package/dist/web/public/tile-grid.js.gz +0 -0
  185. package/dist/web/public/ultracode-panel.js.gz +0 -0
  186. package/dist/web/public/ultracode-windows.js.gz +0 -0
  187. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  188. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  189. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  190. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  191. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  192. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  193. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  194. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  195. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  196. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
  197. package/dist/web/public/vendor/xterm.css.gz +0 -0
  198. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  199. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  200. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  201. package/dist/web/public/webview-tabs.js +12 -0
  202. package/dist/web/public/webview-tabs.js.br +0 -0
  203. package/dist/web/public/webview-tabs.js.gz +0 -0
  204. package/dist/web/route-helpers.d.ts +8 -0
  205. package/dist/web/route-helpers.d.ts.map +1 -1
  206. package/dist/web/route-helpers.js +8 -0
  207. package/dist/web/route-helpers.js.map +1 -1
  208. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  209. package/dist/web/routes/admin-routes.js +4 -1
  210. package/dist/web/routes/admin-routes.js.map +1 -1
  211. package/dist/web/routes/approval-routes.d.ts.map +1 -1
  212. package/dist/web/routes/approval-routes.js +6 -8
  213. package/dist/web/routes/approval-routes.js.map +1 -1
  214. package/dist/web/routes/case-routes.js +1 -1
  215. package/dist/web/routes/case-routes.js.map +1 -1
  216. package/dist/web/routes/file-routes.d.ts +46 -1
  217. package/dist/web/routes/file-routes.d.ts.map +1 -1
  218. package/dist/web/routes/file-routes.js +214 -27
  219. package/dist/web/routes/file-routes.js.map +1 -1
  220. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  221. package/dist/web/routes/hook-event-routes.js +8 -1
  222. package/dist/web/routes/hook-event-routes.js.map +1 -1
  223. package/dist/web/routes/html-view-routes.d.ts +60 -0
  224. package/dist/web/routes/html-view-routes.d.ts.map +1 -0
  225. package/dist/web/routes/html-view-routes.js +288 -0
  226. package/dist/web/routes/html-view-routes.js.map +1 -0
  227. package/dist/web/routes/index.d.ts +1 -0
  228. package/dist/web/routes/index.d.ts.map +1 -1
  229. package/dist/web/routes/index.js +1 -0
  230. package/dist/web/routes/index.js.map +1 -1
  231. package/dist/web/routes/reboot-restore-routes.js +2 -2
  232. package/dist/web/routes/reboot-restore-routes.js.map +1 -1
  233. package/dist/web/routes/session-routes.d.ts +1 -0
  234. package/dist/web/routes/session-routes.d.ts.map +1 -1
  235. package/dist/web/routes/session-routes.js +278 -56
  236. package/dist/web/routes/session-routes.js.map +1 -1
  237. package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
  238. package/dist/web/routes/status-telemetry-routes.js +16 -2
  239. package/dist/web/routes/status-telemetry-routes.js.map +1 -1
  240. package/dist/web/schemas.d.ts +29 -0
  241. package/dist/web/schemas.d.ts.map +1 -1
  242. package/dist/web/schemas.js +65 -0
  243. package/dist/web/schemas.js.map +1 -1
  244. package/dist/web/server.d.ts +6 -1
  245. package/dist/web/server.d.ts.map +1 -1
  246. package/dist/web/server.js +62 -14
  247. package/dist/web/server.js.map +1 -1
  248. package/dist/web/sse-events.d.ts +5 -2
  249. package/dist/web/sse-events.d.ts.map +1 -1
  250. package/dist/web/sse-events.js +5 -2
  251. package/dist/web/sse-events.js.map +1 -1
  252. package/package.json +2 -1
  253. package/skills/codeman/SKILL.md +29 -12
  254. package/dist/web/public/app.6f8552e1.js +0 -43
  255. package/dist/web/public/app.6f8552e1.js.br +0 -0
  256. package/dist/web/public/app.6f8552e1.js.gz +0 -0
  257. package/dist/web/public/constants.bc9c1f63.js.br +0 -0
  258. package/dist/web/public/constants.bc9c1f63.js.gz +0 -0
  259. package/dist/web/public/i18n.a9840ea2.js +0 -2
  260. package/dist/web/public/i18n.a9840ea2.js.br +0 -0
  261. package/dist/web/public/i18n.a9840ea2.js.gz +0 -0
  262. package/dist/web/public/image-input.e00471c6.js.br +0 -0
  263. package/dist/web/public/image-input.e00471c6.js.gz +0 -0
  264. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.br +0 -0
  265. package/dist/web/public/keyboard-accessory.c1e7c7b7.js.gz +0 -0
  266. package/dist/web/public/mobile.111b187d.css +0 -1
  267. package/dist/web/public/mobile.111b187d.css.br +0 -0
  268. package/dist/web/public/mobile.111b187d.css.gz +0 -0
  269. package/dist/web/public/panels-ui.71846fcb.js.br +0 -0
  270. package/dist/web/public/panels-ui.71846fcb.js.gz +0 -0
  271. package/dist/web/public/session-ui.f731602c.js.br +0 -0
  272. package/dist/web/public/session-ui.f731602c.js.gz +0 -0
  273. package/dist/web/public/settings-ui.4e33390f.js.br +0 -0
  274. package/dist/web/public/settings-ui.4e33390f.js.gz +0 -0
  275. package/dist/web/public/spreadsheet-preview.af82fa2f.js.br +0 -0
  276. package/dist/web/public/spreadsheet-preview.af82fa2f.js.gz +0 -0
  277. package/dist/web/public/styles.81a72906.css +0 -1
  278. package/dist/web/public/styles.81a72906.css.br +0 -0
  279. package/dist/web/public/styles.81a72906.css.gz +0 -0
  280. package/dist/web/public/tab-layout-browser.065fcba4.js +0 -1
  281. package/dist/web/public/tab-layout-browser.065fcba4.js.br +0 -0
  282. package/dist/web/public/tab-layout-browser.065fcba4.js.gz +0 -0
  283. package/dist/web/public/tab-rail-resize.a5867793.js.br +0 -0
  284. package/dist/web/public/tab-rail-resize.a5867793.js.gz +0 -0
  285. package/dist/web/public/terminal-ui.b7267005.js +0 -2
  286. package/dist/web/public/terminal-ui.b7267005.js.br +0 -0
  287. package/dist/web/public/terminal-ui.b7267005.js.gz +0 -0
@@ -0,0 +1,917 @@
1
+ /**
2
+ * @fileoverview `codeman mcp`: a Model Context Protocol server on stdio, so any MCP
3
+ * client (Claude Code, Claude Desktop, Codex, Gemini CLI, Cursor, ...) can start Codeman
4
+ * sessions in any installed agent CLI, prompt them, wait for them, read their answers
5
+ * and clean up.
6
+ *
7
+ * A CLIENT of the HTTP API, like `codeman agent` (src/cli-agent.ts), whose transport and
8
+ * guards it reuses: no new route, no second server transport (the agent-control plan's
9
+ * "Socket API" non-goal stands), auth and ownership unchanged.
10
+ *
11
+ * Protocol: dual-era, per the 2026-07-28 revision. A legacy client opens with
12
+ * `initialize` (revisions 2025-11-25 and earlier) and is served that way; a modern client
13
+ * sends per-request `_meta` (`io.modelcontextprotocol/protocolVersion` and
14
+ * `.../clientCapabilities`) and may probe with `server/discover`. Messages are
15
+ * newline-delimited JSON-RPC on stdin/stdout; stdout carries protocol messages ONLY,
16
+ * every log line goes to stderr.
17
+ *
18
+ * Invariants (each asserted in test/mcp-server.test.ts):
19
+ * 1. Never guesses a server: the URL comes from `--url` or `CODEMAN_API_URL`. Without
20
+ * one the server still starts and lists its tools (registries introspect that way),
21
+ * and every tool call answers with how to configure it.
22
+ * 2. Prompting, interrupting and deleting are limited to sessions this process started,
23
+ * or (inside a Codeman session) sessions that session spawned, unless the user passed
24
+ * `--allow-any-session`. Listing, reading and waiting are open. The allowlist lives
25
+ * in process memory on purpose: a restart forgets it, which fails CLOSED.
26
+ * 3. Never acts on its own session (inside Codeman), never sends a control byte
27
+ * (`inputRefusal`), and keeps a wait under the client's tool timeout by default
28
+ * (45 s; Codex's MCP default is 60 s), with a 600 s ceiling.
29
+ * 4. A failed tool is a result with `isError: true`, which the model can act on;
30
+ * JSON-RPC errors are for protocol faults only.
31
+ *
32
+ * @module mcp-server
33
+ */
34
+ import { createInterface } from 'node:readline';
35
+ import { randomBytes } from 'node:crypto';
36
+ import { buildInterruptBody, buildSendBody, composerReadyMark, describeFailure, httpRequest, inputRefusal, isSelfSession, MIN_ID_PREFIX_LENGTH, stripAnsi, } from './cli-agent.js';
37
+ import { credentialsFrom, readCodemanEnvFile } from './codeman-credentials.js';
38
+ import { listClis } from './config/cli-registry/registry.js';
39
+ import { getErrorMessage } from './types.js';
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+ // Protocol constants
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ /** Revisions served with per-request `_meta` (no handshake). */
44
+ export const MODERN_PROTOCOL_VERSIONS = ['2026-07-28'];
45
+ /** Revisions served after an `initialize` handshake, newest first. */
46
+ export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
47
+ const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
48
+ const META_CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
49
+ const META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
50
+ /** JSON-RPC error codes this server emits. */
51
+ export const RPC_ERROR = {
52
+ parse: -32700,
53
+ invalidRequest: -32600,
54
+ methodNotFound: -32601,
55
+ invalidParams: -32602,
56
+ internal: -32603,
57
+ unsupportedProtocolVersion: -32022,
58
+ };
59
+ /** Default wait: under Codex's 60 s MCP tool timeout, so a default call never gets cut off. */
60
+ export const DEFAULT_WAIT_MS = 45_000;
61
+ /** The server's own ceiling for one wait (`MAX_WAIT_MS` in config/agent-wait.ts). */
62
+ export const MAX_WAIT_MS = 600_000;
63
+ /** Cap on text handed back to the model, so one read cannot flood its context. */
64
+ export const MAX_OUTPUT_CHARS = 40_000;
65
+ /** Agent-scratch label for a case directory a start_session CREATES (agent-case-marker.ts). */
66
+ const MCP_AGENT_ORIGIN = 'codeman-mcp';
67
+ const WAIT_SIGNALS = ['stop', 'idle', 'exit', 'working', 'blocked'];
68
+ /**
69
+ * Caching hints the 2026-07-28 revision REQUIRES on `server/discover` and `tools/list`
70
+ * results (Claude Code 2.1.296 rejects a modern `tools/list` without them, so the
71
+ * server connects and shows no tools: measured). The tool list is fixed for the life of
72
+ * this process and the same for every caller, hence a long TTL and public scope.
73
+ */
74
+ const CACHE_HINTS = { ttlMs: 3_600_000, cacheScope: 'public' };
75
+ const SERVER_INSTRUCTIONS = 'Codeman runs AI coding agent CLIs (Claude Code, Codex, Gemini CLI, OpenCode, Antigravity, Grok, DeepSeek, OMP) ' +
76
+ "and shells in persistent tmux sessions on the user's machine, shown as tabs in a web dashboard. Typical flow: " +
77
+ 'start_session (one fresh case per worker that edits files), send_prompt, then wait_for_session or read_output, ' +
78
+ 'and delete_session for every worker you started. Sessions you did not start can be listed and read but not ' +
79
+ 'prompted, interrupted or deleted, unless the user started this server with --allow-any-session.';
80
+ /**
81
+ * Resolve the configuration from flags and the environment. Never guesses a URL: only
82
+ * `--url` or `CODEMAN_API_URL` set one. A session id counts only inside a Codeman pane
83
+ * (`CODEMAN_MUX=1`); a desktop client's environment has no session of its own.
84
+ */
85
+ export function resolveMcpConfig(options, env = process.env, envFile = readCodemanEnvFile) {
86
+ const raw = (options.url ?? env.CODEMAN_API_URL ?? '').trim();
87
+ let apiUrl;
88
+ let urlError;
89
+ if (raw) {
90
+ try {
91
+ const url = new URL(raw);
92
+ if (url.protocol === 'http:' || url.protocol === 'https:')
93
+ apiUrl = raw.replace(/\/+$/, '');
94
+ else
95
+ urlError = `"${raw}" is not an http(s) URL`;
96
+ }
97
+ catch {
98
+ urlError = `"${raw}" is not a valid URL`;
99
+ }
100
+ }
101
+ const selfId = env.CODEMAN_MUX === '1' ? env.CODEMAN_SESSION_ID?.trim() || undefined : undefined;
102
+ const credentials = credentialsFrom(env, envFile());
103
+ return {
104
+ apiUrl,
105
+ urlError,
106
+ selfId,
107
+ auth: credentials.password ? credentials : undefined,
108
+ allowAnySession: Boolean(options.allowAnySession),
109
+ version: options.version,
110
+ };
111
+ }
112
+ const sessionIdProperty = {
113
+ type: 'string',
114
+ minLength: MIN_ID_PREFIX_LENGTH,
115
+ description: 'Session id from list_sessions or start_session (the full id, or its first 8 characters).',
116
+ };
117
+ const timeoutProperty = {
118
+ type: 'integer',
119
+ minimum: 1000,
120
+ maximum: MAX_WAIT_MS,
121
+ description: `How long to wait, in ms (default ${DEFAULT_WAIT_MS}, max ${MAX_WAIT_MS}). Keep it under your own tool-call timeout; call again to keep waiting.`,
122
+ };
123
+ /**
124
+ * The tools, in a fixed order (the spec asks for deterministic `tools/list` output, which
125
+ * also keeps a client's prompt cache warm). `modes` are the run modes start_session
126
+ * offers, from the CLI registry.
127
+ */
128
+ export function buildToolDefinitions(modes) {
129
+ return [
130
+ {
131
+ name: 'list_sessions',
132
+ title: 'List Codeman sessions',
133
+ description: 'List the Codeman sessions you can see. Each one is an AI coding agent CLI (Claude Code, Codex, Gemini CLI, ' +
134
+ 'OpenCode, Grok, DeepSeek, ...) or a shell, running in a persistent tmux pane on the Codeman host and shown ' +
135
+ 'to the user as a tab. Returns id, name, run mode, status, folder, and whether this server may prompt it. ' +
136
+ 'The status is a hint for humans, not something to synchronize on.',
137
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
138
+ annotations: { readOnlyHint: true, openWorldHint: false },
139
+ },
140
+ {
141
+ name: 'start_session',
142
+ title: 'Start a worker session',
143
+ description: 'Start a new worker: an AI coding agent CLI or a shell in a persistent tmux pane on the Codeman host, ' +
144
+ "visible to the user as a new tab in their Codeman dashboard and running on the user's own CLI logins. Use " +
145
+ 'it to run work in parallel, get a second opinion from a different CLI or model, or hand off a long task. ' +
146
+ "`case` names a Codeman case (a folder): a new name creates a fresh scratch folder in Codeman's cases " +
147
+ 'directory, an existing case name runs the worker in that project. Give every worker that edits files its ' +
148
+ 'own fresh case, and put absolute paths in its prompt. Waits until the CLI can take a prompt where the CLI ' +
149
+ 'shows a ready mark. Returns the session id for send_prompt, wait_for_session, read_output and delete_session.',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ case: {
154
+ type: 'string',
155
+ minLength: 1,
156
+ maxLength: 100,
157
+ description: 'Case name: a fresh name such as "review-auth-1", or an existing case.',
158
+ },
159
+ mode: {
160
+ type: 'string',
161
+ enum: [...modes],
162
+ default: 'claude',
163
+ description: 'Which agent CLI runs in the session. Only CLIs installed on the Codeman host start.',
164
+ },
165
+ name: { type: 'string', maxLength: 100, description: 'Optional tab name shown in the dashboard.' },
166
+ wait_ready: {
167
+ type: 'boolean',
168
+ default: true,
169
+ description: 'Wait until the CLI can take a prompt (default true).',
170
+ },
171
+ timeout_ms: timeoutProperty,
172
+ },
173
+ required: ['case'],
174
+ additionalProperties: false,
175
+ },
176
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
177
+ },
178
+ {
179
+ name: 'send_prompt',
180
+ title: 'Send a prompt to a session',
181
+ description: 'Type a one-line prompt into a session this server started and press Enter. With wait (default true) it ' +
182
+ 'blocks until the worker finishes its turn or timeout_ms passes, and reports finished, still_working (call ' +
183
+ 'wait_for_session to keep waiting) or exited. Only claude and deepseek sessions report a definite end of ' +
184
+ 'turn; for the other CLIs "finished" is inferred from a quiet screen, so for a reliable result ask the ' +
185
+ 'worker to print a unique marker when done and use wait_for_session with match. Newlines and control ' +
186
+ 'characters are refused: put long instructions in a file and send its path.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ session_id: sessionIdProperty,
191
+ text: { type: 'string', minLength: 1, description: 'The prompt: one line of printable text.' },
192
+ wait: { type: 'boolean', default: true, description: 'Block until the turn ends (default true).' },
193
+ timeout_ms: timeoutProperty,
194
+ },
195
+ required: ['session_id', 'text'],
196
+ additionalProperties: false,
197
+ },
198
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
199
+ },
200
+ {
201
+ name: 'wait_for_session',
202
+ title: 'Wait for a session',
203
+ description: 'Wait until a session reaches a state (until: stop = finished its turn, blocked = waiting on a permission ' +
204
+ 'prompt, idle, working, exit) or prints a literal marker (match). Returns timed_out when timeout_ms passes: ' +
205
+ 'call again to keep waiting. Your own prompt is echoed into the output, so a marker written whole in the ' +
206
+ 'prompt matches at once: ask for it in two halves ("print DONE followed by _7f3a") and match the joined ' +
207
+ 'form (DONE_7f3a), one word without spaces. stop and blocked exist only for claude and deepseek sessions.',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {
211
+ session_id: sessionIdProperty,
212
+ until: {
213
+ type: 'array',
214
+ items: { type: 'string', enum: [...WAIT_SIGNALS] },
215
+ minItems: 1,
216
+ uniqueItems: true,
217
+ description: 'Signals to wait for, for example ["stop", "exit"].',
218
+ },
219
+ match: { type: 'string', minLength: 1, maxLength: 200, description: 'Literal text to wait for (no regex).' },
220
+ from: {
221
+ type: 'string',
222
+ enum: ['buffer', 'now'],
223
+ default: 'buffer',
224
+ description: 'With match: also search output already printed (buffer, default) or only new output (now).',
225
+ },
226
+ timeout_ms: timeoutProperty,
227
+ },
228
+ required: ['session_id'],
229
+ additionalProperties: false,
230
+ },
231
+ annotations: { readOnlyHint: true, openWorldHint: false },
232
+ },
233
+ {
234
+ name: 'read_output',
235
+ title: "Read a session's output",
236
+ description: "Read what a session produced. last_answer (default) is the worker's last reply, conversation the whole " +
237
+ 'exchange, terminal the last tail_bytes of its screen with colors stripped (works for every CLI, use it ' +
238
+ `when the others come back empty). Output is capped at ${MAX_OUTPUT_CHARS} characters.`,
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ session_id: sessionIdProperty,
243
+ source: {
244
+ type: 'string',
245
+ enum: ['last_answer', 'conversation', 'terminal'],
246
+ default: 'last_answer',
247
+ description: 'What to read.',
248
+ },
249
+ tail_bytes: {
250
+ type: 'integer',
251
+ minimum: 100,
252
+ maximum: 200_000,
253
+ default: 4000,
254
+ description: 'With source terminal: how many bytes of the screen to read (default 4000).',
255
+ },
256
+ },
257
+ required: ['session_id'],
258
+ additionalProperties: false,
259
+ },
260
+ annotations: { readOnlyHint: true, openWorldHint: false },
261
+ },
262
+ {
263
+ name: 'interrupt_session',
264
+ title: 'Interrupt a session',
265
+ description: 'Press Escape in a session this server started, to stop its current turn. The conversation is kept. One ' +
266
+ 'Escape does not always land, so read_output before the next prompt.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: { session_id: sessionIdProperty },
270
+ required: ['session_id'],
271
+ additionalProperties: false,
272
+ },
273
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
274
+ },
275
+ {
276
+ name: 'delete_session',
277
+ title: 'Delete a session',
278
+ description: 'Close a session this server started: ends its CLI and removes its tab. The case folder stays on disk. ' +
279
+ 'Delete every worker you started once you have its answer.',
280
+ inputSchema: {
281
+ type: 'object',
282
+ properties: { session_id: sessionIdProperty },
283
+ required: ['session_id'],
284
+ additionalProperties: false,
285
+ },
286
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
287
+ },
288
+ ];
289
+ }
290
+ /** Run modes start_session offers: every enabled registry entry, in registry order. */
291
+ export function registryModes() {
292
+ return listClis()
293
+ .filter((cli) => cli.enabled !== false)
294
+ .map((cli) => cli.id);
295
+ }
296
+ /** A refusal or failure the model can act on: becomes an `isError` result, never a JSON-RPC error. */
297
+ export class ToolError extends Error {
298
+ }
299
+ const FULL_ID_LENGTH = 36;
300
+ /**
301
+ * Output handed to the model as plain text and nothing else: no structuredContent beside
302
+ * it, because a client that prefers structuredContent (Claude Code does) would then show
303
+ * the model the metadata instead of the output (measured: a terminal read came back
304
+ * "empty"). Over the cap, the kept end is marked in the text itself.
305
+ */
306
+ function readableText(text, keep) {
307
+ if (text.length <= MAX_OUTPUT_CHARS)
308
+ return text;
309
+ return keep === 'tail'
310
+ ? `[showing the last ${MAX_OUTPUT_CHARS} of ${text.length} characters]\n${text.slice(text.length - MAX_OUTPUT_CHARS)}`
311
+ : `${text.slice(0, MAX_OUTPUT_CHARS)}\n[showing the first ${MAX_OUTPUT_CHARS} of ${text.length} characters]`;
312
+ }
313
+ /** A pane capture for reading: CRLF to LF, blank runs collapsed, trailing blank lines dropped. */
314
+ function tidyTerminal(text) {
315
+ return text
316
+ .replace(/\r\n?/g, '\n')
317
+ .replace(/[ \t]+\n/g, '\n')
318
+ .replace(/\n{3,}/g, '\n\n')
319
+ .trimEnd();
320
+ }
321
+ /** `structured` as pretty JSON text: the spec asks for a text copy of structured content. */
322
+ function structuredResult(structured, isError = false) {
323
+ return { text: JSON.stringify(structured, null, 2), structured, isError };
324
+ }
325
+ // Argument readers: the schema in tools/list is advisory to the client, so every value
326
+ // is checked again here and a bad one is an isError result the model can correct.
327
+ function readString(args, key, opts = {}) {
328
+ const value = args[key];
329
+ if (value === undefined || value === null) {
330
+ if (opts.required)
331
+ throw new ToolError(`${key} is required`);
332
+ return undefined;
333
+ }
334
+ if (typeof value !== 'string')
335
+ throw new ToolError(`${key} must be a string`);
336
+ if (opts.required && value.length === 0)
337
+ throw new ToolError(`${key} must not be empty`);
338
+ if (opts.max !== undefined && value.length > opts.max)
339
+ throw new ToolError(`${key} is longer than ${opts.max}`);
340
+ return value;
341
+ }
342
+ function readBool(args, key, fallback) {
343
+ const value = args[key];
344
+ if (value === undefined || value === null)
345
+ return fallback;
346
+ if (typeof value !== 'boolean')
347
+ throw new ToolError(`${key} must be true or false`);
348
+ return value;
349
+ }
350
+ function readInt(args, key, fallback, min, max) {
351
+ const value = args[key];
352
+ if (value === undefined || value === null)
353
+ return fallback;
354
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) {
355
+ throw new ToolError(`${key} must be an integer from ${min} to ${max}`);
356
+ }
357
+ return value;
358
+ }
359
+ function readEnum(args, key, allowed, fallback) {
360
+ const value = args[key];
361
+ if (value === undefined || value === null)
362
+ return fallback;
363
+ if (typeof value !== 'string' || !allowed.includes(value)) {
364
+ throw new ToolError(`${key} must be one of: ${allowed.join(', ')}`);
365
+ }
366
+ return value;
367
+ }
368
+ export function createToolRunner(deps) {
369
+ const { config } = deps;
370
+ // The server drops any seq at or below the last one it saw for this clientId (and
371
+ // still answers 200), so two inputs in one millisecond, or a clock stepped back,
372
+ // must not reuse a seq: keep it strictly increasing for the life of the process.
373
+ let lastSeq = 0;
374
+ const nextSeq = () => (lastSeq = Math.max(lastSeq + 1, (deps.now ?? Date.now)()));
375
+ function context() {
376
+ if (!config.apiUrl) {
377
+ throw new ToolError(config.urlError
378
+ ? `Codeman URL refused: ${config.urlError}.`
379
+ : 'This MCP server does not know where Codeman runs. Set CODEMAN_API_URL in its environment (for example ' +
380
+ 'https://127.0.0.1:3000, the address you open Codeman at) or pass --url, plus CODEMAN_PASSWORD when the ' +
381
+ 'server has one, then restart the MCP server.');
382
+ }
383
+ return { apiUrl: config.apiUrl, selfId: config.selfId ?? '', auth: config.auth };
384
+ }
385
+ async function call(options, signal) {
386
+ const ctx = context();
387
+ try {
388
+ return await deps.request(ctx, { ...options, signal });
389
+ }
390
+ catch (err) {
391
+ if (signal?.aborted)
392
+ throw err;
393
+ throw new ToolError(`Cannot reach Codeman at ${ctx.apiUrl}: ${getErrorMessage(err)}. Check that it is running. If every call ` +
394
+ 'fails at once, a sandbox around this MCP server may be blocking network access to this machine.');
395
+ }
396
+ }
397
+ function ensureOk(res) {
398
+ if (!res.json?.success)
399
+ throw new ToolError(describeFailure(res));
400
+ }
401
+ async function listRows(signal) {
402
+ const res = await call({ method: 'GET', path: '/api/v1/sessions' }, signal);
403
+ ensureOk(res);
404
+ return res.json?.data ?? [];
405
+ }
406
+ /** The session an id names: the full id or a unique prefix of at least 8 characters. */
407
+ async function findSession(id, signal) {
408
+ if (id.length < MIN_ID_PREFIX_LENGTH) {
409
+ throw new ToolError(`session_id "${id}" is shorter than ${MIN_ID_PREFIX_LENGTH} characters`);
410
+ }
411
+ const rows = await listRows(signal);
412
+ const exact = rows.find((row) => row.id === id);
413
+ if (exact)
414
+ return exact;
415
+ const matches = id.length < FULL_ID_LENGTH ? rows.filter((row) => row.id.startsWith(id)) : [];
416
+ if (matches.length === 1)
417
+ return matches[0];
418
+ if (matches.length === 0)
419
+ throw new ToolError(`No session ${id} (list_sessions shows the current ones)`);
420
+ throw new ToolError(`session_id "${id}" is ambiguous: ${matches.map((row) => row.id.slice(0, 13)).join(', ')}`);
421
+ }
422
+ function isSelf(id) {
423
+ return config.selfId !== undefined && isSelfSession(config.selfId, id);
424
+ }
425
+ function mayControl(row) {
426
+ if (config.allowAnySession)
427
+ return true;
428
+ if (deps.started.has(row.id))
429
+ return true;
430
+ return config.selfId !== undefined && row.parentSessionId === config.selfId;
431
+ }
432
+ function guardControl(row, verb) {
433
+ if (isSelf(row.id))
434
+ throw new ToolError(`Refusing to ${verb} session ${row.id}: it is the session this server runs in.`);
435
+ if (!mayControl(row)) {
436
+ throw new ToolError(`Refusing to ${verb} session ${row.id.slice(0, 8)}${row.name ? ` (${row.name})` : ''}: this server did not ` +
437
+ 'start it. Tools may only prompt, interrupt or delete sessions started through them. If the user wants ' +
438
+ 'you to drive their existing sessions, they can restart this MCP server with --allow-any-session.');
439
+ }
440
+ }
441
+ async function waitCall(sessionId, query, path, timeoutMs, signal) {
442
+ const res = await call({
443
+ method: 'GET',
444
+ path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/${path}`,
445
+ query: { ...query, timeout: timeoutMs },
446
+ timeoutMs: timeoutMs + 10_000,
447
+ }, signal);
448
+ ensureOk(res);
449
+ return res.json?.data ?? {};
450
+ }
451
+ /** One word for a wait's outcome, the shape every waiting tool reports. */
452
+ function waitOutcome(wait) {
453
+ if (!wait)
454
+ return 'unknown';
455
+ if (wait.signal === 'exit')
456
+ return 'exited';
457
+ if (wait.ended)
458
+ return 'ended';
459
+ if (wait.timedOut)
460
+ return 'timed_out';
461
+ if (wait.matched !== undefined)
462
+ return wait.matched ? 'matched' : 'timed_out';
463
+ return 'signal';
464
+ }
465
+ const tools = {
466
+ async list_sessions(_args, signal) {
467
+ const rows = await listRows(signal);
468
+ const sessions = rows.map((row) => ({
469
+ id: row.id,
470
+ short_id: row.id.slice(0, 8),
471
+ name: row.name ?? '',
472
+ mode: row.mode ?? '',
473
+ status: row.status ?? '',
474
+ folder: row.workingDir ?? '',
475
+ you: isSelf(row.id),
476
+ controllable: !isSelf(row.id) && mayControl(row),
477
+ }));
478
+ return structuredResult({ sessions, count: sessions.length });
479
+ },
480
+ async start_session(args, signal) {
481
+ const caseName = readString(args, 'case', { required: true, max: 100 });
482
+ const modes = registryModes();
483
+ const mode = readEnum(args, 'mode', modes, 'claude');
484
+ const name = readString(args, 'name', { max: 100 });
485
+ const waitReady = readBool(args, 'wait_ready', true);
486
+ const timeoutMs = readInt(args, 'timeout_ms', DEFAULT_WAIT_MS, 1000, MAX_WAIT_MS);
487
+ const body = { caseName, mode };
488
+ if (config.selfId)
489
+ body.parentSessionId = config.selfId;
490
+ if (name)
491
+ body.sessionName = name;
492
+ const res = await call({
493
+ method: 'POST',
494
+ path: '/api/v1/quick-start',
495
+ body,
496
+ headers: { 'X-Codeman-Agent-Origin': MCP_AGENT_ORIGIN },
497
+ timeoutMs: 120_000,
498
+ }, signal);
499
+ ensureOk(res);
500
+ const data = res.json?.data;
501
+ if (!data?.sessionId)
502
+ throw new ToolError('Codeman answered without a session id');
503
+ deps.started.add(data.sessionId);
504
+ let ready = null;
505
+ let note = 'Ready for send_prompt.';
506
+ const mark = composerReadyMark(mode);
507
+ if (waitReady && mark) {
508
+ let wait;
509
+ try {
510
+ ({ wait } = await waitCall(data.sessionId, { match: mark, from: 'buffer' }, 'wait-output', timeoutMs, signal));
511
+ }
512
+ catch (err) {
513
+ // The worker is already running: a failed readiness CALL (429, a network blip)
514
+ // must still hand the model its id, or it cannot prompt or delete it.
515
+ if (!(err instanceof ToolError))
516
+ throw err;
517
+ return structuredResult({
518
+ session_id: data.sessionId,
519
+ mode,
520
+ case: data.caseName ?? caseName,
521
+ case_path: data.casePath ?? '',
522
+ ready: null,
523
+ note: `Started, but the readiness check failed (${err.message}): read_output with source terminal before send_prompt.`,
524
+ });
525
+ }
526
+ const outcome = waitOutcome(wait);
527
+ if (outcome === 'exited' || outcome === 'ended') {
528
+ ready = false;
529
+ note = 'The CLI exited during startup: read_output with source terminal shows why.';
530
+ }
531
+ else {
532
+ ready = outcome === 'matched';
533
+ if (!ready) {
534
+ note =
535
+ `Not ready within ${timeoutMs} ms (a startup dialog, or a slow first start): read_output with source ` +
536
+ 'terminal, then send_prompt once it shows its prompt.';
537
+ }
538
+ }
539
+ }
540
+ else if (waitReady) {
541
+ note = `${mode} has no ready mark: give it a few seconds, then use markers with wait_for_session to synchronize.`;
542
+ }
543
+ return structuredResult({
544
+ session_id: data.sessionId,
545
+ mode,
546
+ case: data.caseName ?? caseName,
547
+ case_path: data.casePath ?? '',
548
+ ready,
549
+ note,
550
+ });
551
+ },
552
+ async send_prompt(args, signal) {
553
+ const id = readString(args, 'session_id', { required: true });
554
+ const text = readString(args, 'text', { required: true });
555
+ const wait = readBool(args, 'wait', true);
556
+ const timeoutMs = readInt(args, 'timeout_ms', DEFAULT_WAIT_MS, 1000, MAX_WAIT_MS);
557
+ const refusal = inputRefusal(text);
558
+ if (refusal)
559
+ throw new ToolError(refusal);
560
+ const row = await findSession(id, signal);
561
+ guardControl(row, 'prompt');
562
+ const body = buildSendBody(text, {
563
+ enter: true,
564
+ clientId: deps.clientId,
565
+ seq: nextSeq(),
566
+ wait: wait ? true : undefined,
567
+ waitTimeout: wait ? timeoutMs : undefined,
568
+ });
569
+ const res = await call({
570
+ method: 'POST',
571
+ path: `/api/v1/sessions/${encodeURIComponent(row.id)}/input`,
572
+ body,
573
+ timeoutMs: timeoutMs + 10_000,
574
+ }, signal);
575
+ ensureOk(res);
576
+ const data = res.json?.data;
577
+ if (data?.dropped) {
578
+ throw new ToolError("Dropped: the session's host is waking and its input buffer is full. Send again later.");
579
+ }
580
+ if (data?.delivered === false && !data.duplicate) {
581
+ return structuredResult({ session_id: row.id, outcome: 'not_delivered', note: 'The session has no live CLI (its pane exited).' }, true);
582
+ }
583
+ if (!wait) {
584
+ return structuredResult({
585
+ session_id: row.id,
586
+ outcome: data?.buffered ? 'buffered' : 'sent',
587
+ note: 'Sent without waiting: use wait_for_session or read_output.',
588
+ });
589
+ }
590
+ const outcome = waitOutcome(data?.wait);
591
+ const definite = data?.wait?.until?.includes('stop') ?? false;
592
+ const result = {
593
+ session_id: row.id,
594
+ outcome: outcome === 'signal' ? 'finished' : outcome === 'timed_out' ? 'still_working' : outcome,
595
+ signal: data?.wait?.signal ?? null,
596
+ };
597
+ if (outcome === 'signal') {
598
+ result.note = definite
599
+ ? 'The worker finished its turn: read_output for its answer.'
600
+ : 'The screen went quiet, which for this CLI usually means done but is not definite: read_output to check, ' +
601
+ 'or ask for a marker and use wait_for_session with match.';
602
+ }
603
+ else if (outcome === 'timed_out') {
604
+ result.note = 'Still working: call wait_for_session to keep waiting.';
605
+ }
606
+ return structuredResult(result);
607
+ },
608
+ async wait_for_session(args, signal) {
609
+ const id = readString(args, 'session_id', { required: true });
610
+ const match = readString(args, 'match', { max: 200 });
611
+ const rawUntil = args.until;
612
+ const timeoutMs = readInt(args, 'timeout_ms', DEFAULT_WAIT_MS, 1000, MAX_WAIT_MS);
613
+ const from = readEnum(args, 'from', ['buffer', 'now'], 'buffer');
614
+ let until;
615
+ if (rawUntil !== undefined && rawUntil !== null) {
616
+ if (!Array.isArray(rawUntil) || rawUntil.length === 0)
617
+ throw new ToolError('until must be a non-empty list');
618
+ for (const signalName of rawUntil) {
619
+ if (typeof signalName !== 'string' || !WAIT_SIGNALS.includes(signalName)) {
620
+ throw new ToolError(`until entries must be among: ${WAIT_SIGNALS.join(', ')}`);
621
+ }
622
+ }
623
+ until = rawUntil;
624
+ }
625
+ if (until && match)
626
+ throw new ToolError('Use either until or match, not both.');
627
+ if (!until && !match)
628
+ throw new ToolError('Give until (signals) or match (a literal marker).');
629
+ const row = await findSession(id, signal);
630
+ const data = match
631
+ ? await waitCall(row.id, { match, from }, 'wait-output', timeoutMs, signal)
632
+ : await waitCall(row.id, { until: until.join(',') }, 'wait', timeoutMs, signal);
633
+ const outcome = waitOutcome(data.wait);
634
+ const result = { session_id: row.id, outcome, signal: data.wait?.signal ?? null };
635
+ if (data.wait?.snippet)
636
+ result.snippet = data.wait.snippet;
637
+ if (data.limitPaused)
638
+ result.note = 'The session is paused on a usage limit, so a timeout is expected.';
639
+ else if (outcome === 'timed_out')
640
+ result.note = 'Not yet: call wait_for_session again to keep waiting.';
641
+ return structuredResult(result);
642
+ },
643
+ async read_output(args, signal) {
644
+ const id = readString(args, 'session_id', { required: true });
645
+ const source = readEnum(args, 'source', ['last_answer', 'conversation', 'terminal'], 'last_answer');
646
+ const tailBytes = readInt(args, 'tail_bytes', 4000, 100, 200_000);
647
+ const row = await findSession(id, signal);
648
+ const sid = encodeURIComponent(row.id);
649
+ if (source === 'terminal') {
650
+ const res = await call({ method: 'GET', path: `/api/v1/sessions/${sid}/terminal`, query: { tail: tailBytes } }, signal);
651
+ ensureOk(res);
652
+ const buffer = res.json?.data?.terminalBuffer ?? '';
653
+ return { text: readableText(tidyTerminal(stripAnsi(buffer)), 'tail') || '(the terminal is empty)' };
654
+ }
655
+ const res = await call({
656
+ method: 'GET',
657
+ path: `/api/v1/sessions/${sid}/last-response`,
658
+ query: { context: source === 'conversation' ? 'full' : undefined },
659
+ }, signal);
660
+ ensureOk(res);
661
+ const data = res.json?.data;
662
+ const raw = source === 'conversation' && data?.messages
663
+ ? data.messages.map((message) => `${message.role}: ${message.text}`).join('\n\n')
664
+ : (data?.text ?? '');
665
+ if (!raw.trim()) {
666
+ return { text: 'Nothing to read yet (no answer, or none the server could cut out). Try source terminal.' };
667
+ }
668
+ return { text: readableText(raw, source === 'conversation' ? 'tail' : 'head') };
669
+ },
670
+ async interrupt_session(args, signal) {
671
+ const id = readString(args, 'session_id', { required: true });
672
+ const row = await findSession(id, signal);
673
+ guardControl(row, 'interrupt');
674
+ const res = await call({
675
+ method: 'POST',
676
+ path: `/api/v1/sessions/${encodeURIComponent(row.id)}/input`,
677
+ body: buildInterruptBody(deps.clientId, nextSeq()),
678
+ }, signal);
679
+ ensureOk(res);
680
+ return structuredResult({
681
+ session_id: row.id,
682
+ outcome: 'escape_sent',
683
+ note: 'One Escape does not always land: read_output before the next prompt.',
684
+ });
685
+ },
686
+ async delete_session(args, signal) {
687
+ const id = readString(args, 'session_id', { required: true });
688
+ const row = await findSession(id, signal);
689
+ guardControl(row, 'delete');
690
+ const res = await call({ method: 'DELETE', path: `/api/v1/sessions/${encodeURIComponent(row.id)}` }, signal);
691
+ ensureOk(res);
692
+ deps.started.delete(row.id);
693
+ return structuredResult({
694
+ session_id: row.id,
695
+ outcome: 'deleted',
696
+ note: 'Its case folder stays on disk.',
697
+ });
698
+ },
699
+ };
700
+ return {
701
+ has(name) {
702
+ return Object.prototype.hasOwnProperty.call(tools, name);
703
+ },
704
+ async run(name, args, signal) {
705
+ try {
706
+ return await tools[name](args, signal);
707
+ }
708
+ catch (err) {
709
+ if (err instanceof ToolError)
710
+ return { text: err.message, isError: true };
711
+ throw err;
712
+ }
713
+ },
714
+ };
715
+ }
716
+ function isObject(value) {
717
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
718
+ }
719
+ /**
720
+ * The protocol core, transport-free so tests drive it message by message. Requests run
721
+ * concurrently (a long wait must not hold up a list), each answered once, by id.
722
+ */
723
+ export function createMcpServer(options) {
724
+ const { config, write } = options;
725
+ const log = options.log ?? (() => { });
726
+ const serverInfo = { name: 'codeman', title: 'Codeman', version: config.version };
727
+ const tools = buildToolDefinitions(options.modes ?? registryModes());
728
+ const runner = createToolRunner({
729
+ config,
730
+ request: options.request ?? httpRequest,
731
+ started: new Set(),
732
+ clientId: `codeman-mcp-${randomBytes(4).toString('hex')}`,
733
+ now: options.now,
734
+ });
735
+ const inflight = new Map();
736
+ let closed = false;
737
+ function send(message) {
738
+ if (!closed)
739
+ write({ jsonrpc: '2.0', ...message });
740
+ }
741
+ function sendError(id, code, message, data) {
742
+ send({ id, error: data === undefined ? { code, message } : { code, message, data } });
743
+ }
744
+ /** A result, decorated for the era the request was made in. */
745
+ function sendResult(id, result, modern) {
746
+ if (!modern) {
747
+ send({ id, result });
748
+ return;
749
+ }
750
+ const meta = isObject(result._meta) ? result._meta : {};
751
+ send({ id, result: { resultType: 'complete', ...result, _meta: { ...meta, [META_SERVER_INFO]: serverInfo } } });
752
+ }
753
+ async function handleRequest(id, method, params) {
754
+ if (method === 'initialize') {
755
+ // Legacy handshake: answer with the client's revision when we serve it, else our newest legacy one.
756
+ const requested = params.protocolVersion;
757
+ const protocolVersion = typeof requested === 'string' && LEGACY_PROTOCOL_VERSIONS.includes(requested)
758
+ ? requested
759
+ : LEGACY_PROTOCOL_VERSIONS[0];
760
+ sendResult(id, {
761
+ protocolVersion,
762
+ capabilities: { tools: { listChanged: false } },
763
+ serverInfo,
764
+ instructions: SERVER_INSTRUCTIONS,
765
+ }, false);
766
+ return;
767
+ }
768
+ const meta = isObject(params._meta) ? params._meta : undefined;
769
+ const version = meta?.[META_PROTOCOL_VERSION];
770
+ const modern = version !== undefined;
771
+ if (modern) {
772
+ if (typeof version !== 'string' || !MODERN_PROTOCOL_VERSIONS.includes(version)) {
773
+ sendError(id, RPC_ERROR.unsupportedProtocolVersion, 'Unsupported protocol version', {
774
+ supported: [...MODERN_PROTOCOL_VERSIONS],
775
+ requested: version,
776
+ });
777
+ return;
778
+ }
779
+ if (!isObject(meta?.[META_CLIENT_CAPABILITIES])) {
780
+ sendError(id, RPC_ERROR.invalidParams, `Missing _meta["${META_CLIENT_CAPABILITIES}"]`);
781
+ return;
782
+ }
783
+ }
784
+ switch (method) {
785
+ case 'server/discover':
786
+ sendResult(id, {
787
+ supportedVersions: [...MODERN_PROTOCOL_VERSIONS],
788
+ capabilities: { tools: { listChanged: false } },
789
+ instructions: SERVER_INSTRUCTIONS,
790
+ ...CACHE_HINTS,
791
+ }, true);
792
+ return;
793
+ case 'ping':
794
+ sendResult(id, {}, modern);
795
+ return;
796
+ case 'tools/list':
797
+ sendResult(id, modern ? { tools, ...CACHE_HINTS } : { tools }, modern);
798
+ return;
799
+ case 'tools/call': {
800
+ const name = params.name;
801
+ if (typeof name !== 'string' || !runner.has(name)) {
802
+ sendError(id, RPC_ERROR.invalidParams, `Unknown tool: ${String(name)}`);
803
+ return;
804
+ }
805
+ const args = params.arguments === undefined ? {} : params.arguments;
806
+ if (!isObject(args)) {
807
+ sendError(id, RPC_ERROR.invalidParams, 'arguments must be an object');
808
+ return;
809
+ }
810
+ const controller = new AbortController();
811
+ inflight.set(id, controller);
812
+ try {
813
+ const result = await runner.run(name, args, controller.signal);
814
+ if (controller.signal.aborted)
815
+ return; // cancelled: no further message for this id
816
+ const payload = {
817
+ content: [{ type: 'text', text: result.text }],
818
+ isError: Boolean(result.isError),
819
+ };
820
+ if (result.structured)
821
+ payload.structuredContent = result.structured;
822
+ sendResult(id, payload, modern);
823
+ }
824
+ catch (err) {
825
+ if (controller.signal.aborted)
826
+ return;
827
+ log(`codeman mcp: ${name} failed: ${getErrorMessage(err)}`);
828
+ sendError(id, RPC_ERROR.internal, `Internal error in ${name}: ${getErrorMessage(err)}`);
829
+ }
830
+ finally {
831
+ inflight.delete(id);
832
+ }
833
+ return;
834
+ }
835
+ default:
836
+ sendError(id, RPC_ERROR.methodNotFound, `Method not found: ${method}`);
837
+ }
838
+ }
839
+ function handleNotification(method, params) {
840
+ if (method === 'notifications/cancelled') {
841
+ const requestId = params.requestId;
842
+ if (typeof requestId === 'string' || typeof requestId === 'number')
843
+ inflight.get(requestId)?.abort();
844
+ }
845
+ // notifications/initialized and anything else: nothing to do, and never an answer.
846
+ }
847
+ return {
848
+ /** One stdin line. Resolves when that message is fully handled. */
849
+ async handleLine(line) {
850
+ const trimmed = line.trim();
851
+ if (!trimmed)
852
+ return;
853
+ let message;
854
+ try {
855
+ message = JSON.parse(trimmed);
856
+ }
857
+ catch {
858
+ sendError(null, RPC_ERROR.parse, 'Parse error');
859
+ return;
860
+ }
861
+ if (!isObject(message)) {
862
+ sendError(null, RPC_ERROR.invalidRequest, 'Expected one JSON-RPC message object per line');
863
+ return;
864
+ }
865
+ const msg = message;
866
+ const params = isObject(msg.params) ? msg.params : {};
867
+ if (typeof msg.method !== 'string')
868
+ return; // a response to nothing we sent: ignore
869
+ const hasId = msg.id !== undefined;
870
+ if (!hasId) {
871
+ handleNotification(msg.method, params);
872
+ return;
873
+ }
874
+ if (typeof msg.id !== 'string' && typeof msg.id !== 'number') {
875
+ sendError(null, RPC_ERROR.invalidRequest, 'Request id must be a string or a number');
876
+ return;
877
+ }
878
+ if (msg.jsonrpc !== '2.0') {
879
+ sendError(msg.id, RPC_ERROR.invalidRequest, 'jsonrpc must be "2.0"');
880
+ return;
881
+ }
882
+ await handleRequest(msg.id, msg.method, params);
883
+ },
884
+ /** Stop: abort every in-flight call and write nothing more. */
885
+ close() {
886
+ for (const controller of inflight.values())
887
+ controller.abort();
888
+ inflight.clear();
889
+ closed = true;
890
+ },
891
+ inflightCount() {
892
+ return inflight.size;
893
+ },
894
+ };
895
+ }
896
+ /**
897
+ * Serve MCP on this process's stdin/stdout until stdin closes (the client's shutdown
898
+ * signal), then abort whatever is still in flight.
899
+ */
900
+ export async function runMcpStdio(config, io = process) {
901
+ const log = (line) => io.stderr.write(`${line}\n`);
902
+ const server = createMcpServer({
903
+ config,
904
+ write: (message) => io.stdout.write(`${JSON.stringify(message)}\n`),
905
+ log,
906
+ });
907
+ log(`codeman mcp ${config.version}: ${config.apiUrl ? `driving ${config.apiUrl}` : `not configured (${config.urlError ?? 'set CODEMAN_API_URL or --url'})`}` +
908
+ `${config.selfId ? `, inside session ${config.selfId.slice(0, 8)}` : ''}` +
909
+ `${config.allowAnySession ? ', --allow-any-session' : ''}`);
910
+ const lines = createInterface({ input: io.stdin, crlfDelay: Infinity });
911
+ lines.on('line', (line) => {
912
+ void server.handleLine(line).catch((err) => log(`codeman mcp: ${getErrorMessage(err)}`));
913
+ });
914
+ await new Promise((resolve) => lines.on('close', () => resolve()));
915
+ server.close();
916
+ }
917
+ //# sourceMappingURL=mcp-server.js.map