commandmate 0.34.0 → 0.34.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 (208) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +136 -136
  3. package/.next/app-path-routes-manifest.json +27 -27
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/prerender-manifest.json +12 -12
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/api/agents/update/route_client-reference-manifest.js +1 -1
  9. package/.next/server/app/api/agents/versions/route_client-reference-manifest.js +1 -1
  10. package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/app/update-check/route.js +1 -1
  12. package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/capabilities/route.js +1 -1
  24. package/.next/server/app/api/capabilities/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/relays/[relayId]/cancel/route.js +1 -1
  41. package/.next/server/app/api/relays/[relayId]/cancel/route.js.nft.json +1 -1
  42. package/.next/server/app/api/relays/[relayId]/cancel/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/relays/route.js +1 -1
  44. package/.next/server/app/api/relays/route.js.nft.json +1 -1
  45. package/.next/server/app/api/relays/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/remote/pair/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
  58. package/.next/server/app/api/settings/default-agents/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/settings/default-surface-mode/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
  61. package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
  64. package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
  67. package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
  70. package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
  73. package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
  76. package/.next/server/app/api/worktrees/[id]/clear-composer/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/worktrees/[id]/cli-reference/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/worktrees/[id]/env/route_client-reference-manifest.js +1 -1
  82. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
  84. package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
  87. package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
  88. package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
  90. package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
  94. package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
  100. package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
  105. package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
  106. package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/api/worktrees/[id]/instances/notes/route_client-reference-manifest.js +1 -1
  109. package/.next/server/app/api/worktrees/[id]/instances/opencode/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
  111. package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
  112. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
  114. package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
  115. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
  116. package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
  117. package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
  118. package/.next/server/app/api/worktrees/[id]/opencode/diff/route_client-reference-manifest.js +1 -1
  119. package/.next/server/app/api/worktrees/[id]/opencode/session/route_client-reference-manifest.js +1 -1
  120. package/.next/server/app/api/worktrees/[id]/opencode/share/route_client-reference-manifest.js +1 -1
  121. package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/api/worktrees/[id]/resolve-target/route_client-reference-manifest.js +1 -1
  123. package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
  124. package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
  125. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
  126. package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
  127. package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
  128. package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
  129. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  130. package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
  131. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
  132. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
  133. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
  134. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
  135. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
  136. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
  137. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
  138. package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
  139. package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
  140. package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
  141. package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
  142. package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
  143. package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
  144. package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
  145. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  146. package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
  147. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
  148. package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
  149. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
  150. package/.next/server/app/api/worktrees/[id]/verify/config/route_client-reference-manifest.js +1 -1
  151. package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
  152. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/cancel/route_client-reference-manifest.js +1 -1
  153. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
  154. package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
  155. package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
  156. package/.next/server/app/api/worktrees/apply-default-agents/route_client-reference-manifest.js +1 -1
  157. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  158. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  159. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  160. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  161. package/.next/server/app/more/page_client-reference-manifest.js +1 -1
  162. package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  163. package/.next/server/app/page_client-reference-manifest.js +1 -1
  164. package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
  165. package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
  166. package/.next/server/app/review/page_client-reference-manifest.js +1 -1
  167. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  168. package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
  169. package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
  170. package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  171. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  172. package/.next/server/app/worktrees/[id]/page.js +1 -1
  173. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  174. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  175. package/.next/server/app-paths-manifest.json +27 -27
  176. package/.next/server/chunks/3077.js +1 -1
  177. package/.next/server/chunks/434.js +1 -0
  178. package/.next/server/chunks/595.js +3 -3
  179. package/.next/server/chunks/6130.js +1 -1
  180. package/.next/server/chunks/8807.js +10 -10
  181. package/.next/server/middleware-manifest.json +5 -5
  182. package/.next/server/pages/500.html +1 -1
  183. package/.next/server/server-reference-manifest.json +1 -1
  184. package/.next/server/src/middleware.js +1 -1
  185. package/.next/server/src/middleware.js.map +1 -1
  186. package/.next/static/chunks/{4718-2d9f0397a8f931c4.js → 4718-29cfcfb806724d9a.js} +1 -1
  187. package/.next/static/chunks/app/worktrees/[id]/{page-a39ec2a0732d11a6.js → page-60eab4e24c37cb77.js} +1 -1
  188. package/.next/trace +13 -13
  189. package/README.md +141 -24
  190. package/dist/cli/commands/remote.d.ts +93 -3
  191. package/dist/cli/commands/remote.d.ts.map +1 -1
  192. package/dist/cli/commands/remote.js +232 -9
  193. package/dist/cli/types/index.d.ts +9 -0
  194. package/dist/cli/types/index.d.ts.map +1 -1
  195. package/dist/cli/utils/api-client.d.ts.map +1 -1
  196. package/dist/cli/utils/api-client.js +11 -1
  197. package/dist/cli/utils/remote-state.d.ts +32 -0
  198. package/dist/cli/utils/remote-state.d.ts.map +1 -1
  199. package/dist/cli/utils/remote-state.js +12 -0
  200. package/dist/server/server.js +204 -16
  201. package/dist/server/src/lib/db/agent-instances-db.js +11 -0
  202. package/dist/server/src/lib/hooks/sources/opencode/ports.js +6 -0
  203. package/dist/server/src/lib/relay/relay-session-ref.js +15 -8
  204. package/dist/server/src/lib/ws-server.js +74 -5
  205. package/package.json +1 -1
  206. package/.next/server/chunks/1049.js +0 -1
  207. /package/.next/static/{gniPBCKoXtbIOQey8kNcx → oDWX0JcwfhUYrMnnqDWVC}/_buildManifest.js +0 -0
  208. /package/.next/static/{gniPBCKoXtbIOQey8kNcx → oDWX0JcwfhUYrMnnqDWVC}/_ssgManifest.js +0 -0
@@ -87,6 +87,83 @@ const session_cleanup_1 = require("./src/lib/session-cleanup");
87
87
  const dev = process.env.NODE_ENV !== 'production';
88
88
  const hostname = (0, env_1.getEnvByKey)('CM_BIND') || '127.0.0.1';
89
89
  const port = parseInt((0, env_1.getEnvByKey)('CM_PORT') || '3000', 10);
90
+ /**
91
+ * Issue #2489: the `CM_AUTH_SCOPE` value that exempts the local listener.
92
+ *
93
+ * Written out here rather than imported from `src/lib/ws-server.ts`, which
94
+ * exports the same constant for the Node-runtime side, for the same reason the
95
+ * `x-cm-raw-url` header name below is written out: this is read at MODULE SCOPE,
96
+ * before `app.prepare()`, and everything server.ts evaluates that early has to
97
+ * stand on its own. `tests/integration/remote-ingress-auth-2489.test.ts` holds
98
+ * the two spellings equal, so a rename cannot leave one of them behind.
99
+ */
100
+ const REMOTE_ONLY_AUTH_SCOPE = 'remote-only';
101
+ /**
102
+ * Issue #2489: the loopback address the Provider's upstream is pointed at.
103
+ *
104
+ * `commandmate remote --auth remote-only` asks for "the phone authenticates,
105
+ * the PC in front of the machine does not". The listener a request landed on is
106
+ * the only unforgeable way to tell those apart (see the header of
107
+ * `src/lib/ws-server.ts`'s ingress section), so this bind is deliberately the
108
+ * literal loopback address rather than `hostname`: the second listener must
109
+ * never be reachable from the LAN, whatever `CM_BIND` says.
110
+ */
111
+ const REMOTE_INGRESS_BIND = '127.0.0.1';
112
+ /**
113
+ * Issue #2489: whether `CM_BIND` keeps the local listener off the network.
114
+ *
115
+ * `remote-only` makes the local listener unauthenticated, so a non-loopback
116
+ * bind would extend "local means trusted" to every host on the LAN. The CLI
117
+ * refuses that combination up front (exit 2); this is the second, independent
118
+ * check, because `CM_BIND` can also be set in `.env` or exported after the fact.
119
+ */
120
+ function isLoopbackBind(bind) {
121
+ return bind === '127.0.0.1' || bind === 'localhost' || bind === '::1' || bind === '[::1]';
122
+ }
123
+ /**
124
+ * Fall back to authenticating every listener, and say why.
125
+ *
126
+ * Writes the decision back into `process.env` rather than only returning it:
127
+ * `middleware.ts` and `ws-server.ts` read `CM_AUTH_SCOPE` per request, so a
128
+ * degradation that lived in a local variable would leave the local listener
129
+ * exempt while this process believed it had closed the door.
130
+ */
131
+ function refuseRemoteOnly(reason) {
132
+ console.error(`[Security] CM_AUTH_SCOPE=${REMOTE_ONLY_AUTH_SCOPE} refused: ${reason}. ` +
133
+ 'Every listener will require authentication.');
134
+ process.env.CM_AUTH_SCOPE = 'all';
135
+ return null;
136
+ }
137
+ /**
138
+ * Resolve the port of the remote-only ingress listener, or null for one listener.
139
+ *
140
+ * Fail-closed at every branch: anything this cannot verify turns into
141
+ * {@link refuseRemoteOnly}, which is the pre-#2489 behaviour (auth everywhere).
142
+ *
143
+ * @returns The loopback port to open for the Provider, or null
144
+ */
145
+ function resolveRemoteIngressPort() {
146
+ if (process.env.CM_AUTH_SCOPE !== REMOTE_ONLY_AUTH_SCOPE)
147
+ return null;
148
+ if (!isLoopbackBind(hostname)) {
149
+ return refuseRemoteOnly(`CM_BIND is ${hostname}, which is not loopback`);
150
+ }
151
+ const raw = process.env.CM_REMOTE_INGRESS_PORT;
152
+ if (!raw) {
153
+ return refuseRemoteOnly('CM_REMOTE_INGRESS_PORT is not set');
154
+ }
155
+ const parsed = Number.parseInt(raw, 10);
156
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
157
+ return refuseRemoteOnly(`CM_REMOTE_INGRESS_PORT is not a valid port: ${raw}`);
158
+ }
159
+ if (parsed === port) {
160
+ // One socket cannot be two ingresses, and "the Provider dials the port the
161
+ // user dials" is exactly the confusion this whole mechanism exists to avoid.
162
+ return refuseRemoteOnly('CM_REMOTE_INGRESS_PORT is the same port the server serves on');
163
+ }
164
+ return parsed;
165
+ }
166
+ const remoteIngressPort = resolveRemoteIngressPort();
90
167
  // Issue #331: HTTPS configuration
91
168
  const certPath = process.env.CM_HTTPS_CERT;
92
169
  const keyPath = process.env.CM_HTTPS_KEY;
@@ -162,14 +239,27 @@ async function resolveWorktreeAliasRedirect(url) {
162
239
  // All WebSocket upgrades are handled by ws-server.ts (our own upgrade listener).
163
240
  app.setupWebSocketHandler = () => { };
164
241
  app.prepare().then(() => {
165
- // Request handler for both HTTP and HTTPS
166
- const requestHandler = async (req, res) => {
242
+ // Request handler for both HTTP and HTTPS.
243
+ //
244
+ // Issue #2489: a factory rather than a single handler, because the ONE thing a
245
+ // request cannot lie about is the listener it arrived on, and that fact is
246
+ // only known here — at the point the listener is constructed. Every listener
247
+ // is built through this function, so every request is stamped; a listener
248
+ // wired up any other way would leave the caller's own `x-cm-ingress` in place,
249
+ // which is the single way this scheme can fail open.
250
+ const requestHandlerFor = (ingress) => async (req, res) => {
167
251
  // Guard: res must be a proper HTTP ServerResponse (not a raw net.Socket).
168
252
  // Defense-in-depth: normally not needed after the setupWebSocketHandler fix above,
169
253
  // but kept for safety in case any other path passes a non-ServerResponse object.
170
254
  if (typeof res?.setHeader !== 'function') {
171
255
  return;
172
256
  }
257
+ // Issue #2489: overwrite, unconditionally and before anything reads it.
258
+ // `middleware.ts` and `ws-server.ts` decide whether to demand a token from
259
+ // this value alone — deliberately NOT from `X-Real-IP` / `X-Forwarded-For` /
260
+ // `Host`, all of which a tunnelled request carries identically to a local
261
+ // one (a Provider's upstream IS 127.0.0.1).
262
+ (0, ws_server_1.stampIngress)(req.headers, ingress);
173
263
  // Issue #332: Inject X-Real-IP header for IP restriction
174
264
  // [S3-005] This applies to HTTP requests only. WebSocket upgrade requests
175
265
  // are skipped below and handled by ws-server.ts (which uses socket.remoteAddress directly).
@@ -241,15 +331,16 @@ app.prepare().then(() => {
241
331
  }
242
332
  };
243
333
  // Issue #331: Create HTTP or HTTPS server
244
- let server;
334
+ let tlsOptions = null;
245
335
  let protocol = 'http';
246
336
  if (certPath && keyPath) {
247
337
  const validatedCertPath = validateCertPath(certPath, 'Certificate');
248
338
  const validatedKeyPath = validateCertPath(keyPath, 'Key');
249
339
  try {
250
- const cert = (0, fs_1.readFileSync)(validatedCertPath);
251
- const key = (0, fs_1.readFileSync)(validatedKeyPath);
252
- server = (0, https_1.createServer)({ cert, key }, requestHandler);
340
+ tlsOptions = {
341
+ cert: (0, fs_1.readFileSync)(validatedCertPath),
342
+ key: (0, fs_1.readFileSync)(validatedKeyPath),
343
+ };
253
344
  protocol = 'https';
254
345
  console.log('HTTPS server created with TLS certificates');
255
346
  }
@@ -258,11 +349,35 @@ app.prepare().then(() => {
258
349
  process.exit(2);
259
350
  }
260
351
  }
261
- else {
262
- server = (0, http_1.createServer)(requestHandler);
263
- }
352
+ /**
353
+ * Issue #2489: build one listener, stamped with its own ingress.
354
+ *
355
+ * The TLS material is read once above and shared, so the remote listener is
356
+ * the same server in every respect except which socket it answers on — which
357
+ * is the only difference that is allowed to matter.
358
+ */
359
+ const createListener = (ingress) => tlsOptions === null
360
+ ? (0, http_1.createServer)(requestHandlerFor(ingress))
361
+ : (0, https_1.createServer)(tlsOptions, requestHandlerFor(ingress));
362
+ const server = createListener('local');
363
+ /**
364
+ * Issue #2489: the listener `commandmate remote --auth remote-only` points the
365
+ * Provider at. Null in every other configuration, which is what keeps
366
+ * `--auth all` (and a plain `commandmate start`) on exactly one socket.
367
+ */
368
+ const remoteServer = remoteIngressPort === null ? null : createListener('remote');
369
+ // Issue #2489: stamp WebSocket upgrades too, and BEFORE `setupWebSocket`
370
+ // registers its own 'upgrade' listener — Node fires listeners in registration
371
+ // order, so this is what guarantees the auth check downstream reads a value
372
+ // this process wrote. HTTP and WS must agree: a phone drives the terminal over
373
+ // this socket, so a WS path that exempted more than the HTTP path would hand
374
+ // the tunnel a live shell while the screen behind it still asked for a token.
375
+ server.on('upgrade', (request) => (0, ws_server_1.stampIngress)(request.headers, 'local'));
376
+ remoteServer?.on('upgrade', (request) => (0, ws_server_1.stampIngress)(request.headers, 'remote'));
264
377
  // Setup WebSocket server
265
- (0, ws_server_1.setupWebSocket)(server);
378
+ (0, ws_server_1.setupWebSocket)(server, {
379
+ additionalServers: remoteServer === null ? [] : [remoteServer],
380
+ });
266
381
  // Scan and sync worktrees on startup
267
382
  async function initializeWorktrees() {
268
383
  try {
@@ -466,9 +581,35 @@ app.prepare().then(() => {
466
581
  }
467
582
  process.exit(1);
468
583
  });
584
+ // Issue #2489: open the Provider's door on its own loopback socket.
585
+ //
586
+ // Started before the main `listen` so the CLI's readiness probe on this port
587
+ // cannot outrun it. An error here is NOT fatal to the process — killing the
588
+ // server would take the user's local session down with the remote one, which
589
+ // is the same reason `--expires` closes only the Provider (§5.3) — but it IS
590
+ // fail-closed: without this socket there is no "remote" ingress, so the local
591
+ // exemption is withdrawn and every listener goes back to demanding a token.
592
+ // `commandmate remote` then fails its readiness probe on this port and rolls
593
+ // the whole session back, which is where the user sees the error.
594
+ if (remoteServer !== null && remoteIngressPort !== null) {
595
+ remoteServer.on('error', (err) => {
596
+ console.error(`[Security] Remote ingress listener on ${REMOTE_INGRESS_BIND}:${remoteIngressPort} failed ` +
597
+ `(${err.code ?? err.message}). Every listener will require authentication.`);
598
+ process.env.CM_AUTH_SCOPE = 'all';
599
+ });
600
+ remoteServer.listen(remoteIngressPort, REMOTE_INGRESS_BIND, () => {
601
+ console.log(`> Remote ingress ready on ${protocol}://${REMOTE_INGRESS_BIND}:${remoteIngressPort} (authentication required)`);
602
+ });
603
+ }
469
604
  server.listen(port, hostname, async () => {
470
605
  console.log(`> Ready on ${protocol}://${hostname}:${port}`);
471
606
  console.log(`> WebSocket server ready`);
607
+ if (remoteIngressPort !== null) {
608
+ // Say it on the main "Ready" line too: "the PC does not need to log in" is
609
+ // a security-relevant departure from the default, and a server log that
610
+ // only mentions it in a line above is a log nobody reads.
611
+ console.log(`> Auth scope: ${REMOTE_ONLY_AUTH_SCOPE} — ${protocol}://${hostname}:${port} does not require a token`);
612
+ }
472
613
  // Issue #2113: verify that the URL the documentation advertises
473
614
  // (`http://localhost:<port>`) actually reaches THIS process. It does not when
474
615
  // another process holds `::1:<port>`: `localhost` resolves ::1 before 127.0.0.1
@@ -647,12 +788,59 @@ app.prepare().then(() => {
647
788
  console.log('Graceful shutdown timeout, forcing exit...');
648
789
  process.exit(1);
649
790
  }, 3000);
650
- // Try graceful HTTP server close
651
- server.close(() => {
652
- clearTimeout(forceExitTimeout);
653
- console.log('Server closed gracefully');
654
- process.exit(0);
655
- });
791
+ // Try graceful HTTP server close.
792
+ //
793
+ // Issue #2489: EVERY listener this process opened, not just the main one.
794
+ // `remoteServer` was absent from this function entirely, and the cost was
795
+ // asymmetry on the door the user cannot see: `server.close()`'s callback
796
+ // fires once the LOCAL connections have drained, and the `process.exit(0)`
797
+ // inside it then cut off any request still in flight through the provider.
798
+ // The local listener had three seconds of grace and the remote one had
799
+ // none. (The accept window between the teardown above and this point is the
800
+ // same for both listeners, and effectively zero — everything between the
801
+ // signal and here is synchronous, so the event loop never turns to accept a
802
+ // connection. The in-flight requests are the real loss.)
803
+ //
804
+ // The invariant, rather than the fix: whatever shutdown does to one
805
+ // listener it does to all of them. Anything added here belongs INSIDE this
806
+ // loop rather than on `server` alone — applying it to `server` only would
807
+ // put the asymmetry straight back on the listener nobody is looking at.
808
+ const listeners = remoteServer === null ? [server] : [server, remoteServer];
809
+ // `close()` stops each accept loop immediately and calls back once that
810
+ // listener's own connections have ended, so both doors shut now and the
811
+ // exit waits for the last one to drain. A listener that never reached
812
+ // `listen()` (EADDRINUSE on the remote port) calls its callback with an
813
+ // Error instead of throwing, which counts as drained here — which is what
814
+ // we want: a door that never opened cannot be holding anything.
815
+ let draining = listeners.length;
816
+ for (const listener of listeners) {
817
+ listener.close(() => {
818
+ draining -= 1;
819
+ if (draining > 0)
820
+ return;
821
+ clearTimeout(forceExitTimeout);
822
+ console.log('Server closed gracefully');
823
+ process.exit(0);
824
+ });
825
+ // Issue #2488: close() stops accepting immediately, but its callback only
826
+ // fires once every ESTABLISHED connection on that listener is gone. A
827
+ // browser tab left on the dashboard holds keep-alive sockets with no
828
+ // request in flight, and nothing ever closes them, so the callback never
829
+ // ran and the process lived the full 3 seconds to the force-exit — with
830
+ // its listening socket already closed. That window is what the stop
831
+ // scripts used to mistake for "stopped" (they looked for a listener,
832
+ // found none, and returned while this process was still alive and still
833
+ // named by logs/server.pid).
834
+ //
835
+ // closeIdleConnections() ends exactly those sockets. A connection with a
836
+ // request in flight is NOT idle and is left alone: it still gets the 3
837
+ // seconds above, so an in-flight response is no more truncated than
838
+ // before. The stop scripts no longer depend on this being fast — they
839
+ // wait for the PID — but the common case now exits in milliseconds
840
+ // instead of 3 seconds. Per #2489's invariant this runs on EVERY
841
+ // listener, so the remote door drains on the same terms as the local one.
842
+ listener.closeIdleConnections();
843
+ }
656
844
  }
657
845
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
658
846
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
@@ -136,6 +136,17 @@ function getAgentInstance(db, worktreeId, instanceId) {
136
136
  * conflict rather than silently overriding it: the roster is user-maintained
137
137
  * and a mismatch means one of the two is wrong. Callers surface it as an error.
138
138
  *
139
+ * @deprecated Issue #1925 made `resolveSessionTarget`
140
+ * (`src/lib/session/resolve-session-target.ts`) the one authority, and Issue
141
+ * #2491 moved the last two callers — `POST /send` and `resolveRelaySession` —
142
+ * onto it. **Do not give this function a new caller.** Step 2 above is exactly
143
+ * what #2487 corrected: with no roster row it takes `requestedCliTool` even
144
+ * when `instanceId` itself names a different tool, so a caller wired here gets
145
+ * `mcbd-<requested>-<wt>-<tool-named-id>` back instead of a conflict. It is
146
+ * kept only because its #1629 unit test
147
+ * (`tests/unit/db/agent-instances-resolve-cli-tool.test.ts`) still pins that
148
+ * behaviour; removing both is a separate change.
149
+ *
139
150
  * @param db - Database instance
140
151
  * @param worktreeId - Worktree ID
141
152
  * @param instanceId - Targeted agent instance ID (omitted for the primary instance)
@@ -82,6 +82,12 @@ function keyOf(target) {
82
82
  * not throw — it spins the event loop forever, so the `try/catch` around it
83
83
  * never runs. A file path is checked the same way a directory is (the match is
84
84
  * on the prefix), so the override is refused here and the default is used.
85
+ *
86
+ * Issue #2490: the default is also what a *test* gets when it says nothing, and
87
+ * the writers below are read-modify-write, so an unredirected suite rewrites
88
+ * the ledger a live server is using. `tests/setup.ts` pins the variable for the
89
+ * whole suite for that reason; `tests/unit/lib/hooks/sources/opencode/port-file-isolation-2490.test.ts`
90
+ * is the guard on that pin.
85
91
  */
86
92
  function getOpencodePortFilePath() {
87
93
  const fallback = (0, path_1.join)((0, os_1.homedir)(), '.commandmate', 'opencode-ports.json');
@@ -19,6 +19,7 @@ exports.resolveRelaySession = resolveRelaySession;
19
19
  exports.relayWorktreeExists = relayWorktreeExists;
20
20
  const agent_instances_db_1 = require("../../lib/db/agent-instances-db");
21
21
  const worktree_db_1 = require("../../lib/db/worktree-db");
22
+ const resolve_session_target_1 = require("../../lib/session/resolve-session-target");
22
23
  const logger_1 = require("../../lib/logger");
23
24
  const logger = (0, logger_1.createLogger)('relay-session-ref');
24
25
  /** The tool assumed when neither the roster nor the worktree declares one. */
@@ -26,23 +27,29 @@ const FALLBACK_CLI_TOOL = 'claude';
26
27
  /**
27
28
  * Resolve one end of a relay.
28
29
  *
29
- * The tool comes from `resolveInstanceCliTool` — the same authority
30
+ * The tool comes from `resolveSessionTarget` — the same authority
30
31
  * `POST /api/worktrees/:id/send` uses, so a relay cannot address a session the
31
32
  * send route would address differently (#1629 is the record of what two
32
33
  * authorities on that cost) — falling back to the worktree's own tool and then
33
34
  * to claude.
35
+ *
36
+ * Issue #2491: that sentence was true of `resolveInstanceCliTool` when this
37
+ * module was written and stopped being true when `send` moved to the shared
38
+ * resolver. The two answer identically here — a relay never names a tool, and
39
+ * with `requestedCliTool` absent both walk roster → primary anchor → worktree
40
+ * default → claude — so this is the comment made true again, not a behaviour
41
+ * change. `resolveSessionTarget` folds in the worktree-default and claude
42
+ * stages this function used to spell out for itself; `FALLBACK_CLI_TOOL` stays
43
+ * because the catch below still needs an answer when the roster cannot be read
44
+ * at all.
34
45
  */
35
46
  function resolveRelaySession(db, endpoint) {
36
47
  let cliToolId = FALLBACK_CLI_TOOL;
37
48
  let alias = endpoint.instanceId;
38
49
  try {
39
- const resolution = (0, agent_instances_db_1.resolveInstanceCliTool)(db, endpoint.worktreeId, endpoint.instanceId);
40
- if (resolution.ok && resolution.cliToolId) {
41
- cliToolId = resolution.cliToolId;
42
- }
43
- else {
44
- cliToolId = (0, worktree_db_1.getWorktreeById)(db, endpoint.worktreeId)?.cliToolId ?? FALLBACK_CLI_TOOL;
45
- }
50
+ cliToolId = (0, resolve_session_target_1.resolveSessionTarget)(db, endpoint.worktreeId, {
51
+ instanceId: endpoint.instanceId,
52
+ }).cliToolId;
46
53
  const rosterRow = (0, agent_instances_db_1.getAgentInstance)(db, endpoint.worktreeId, endpoint.instanceId);
47
54
  if (rosterRow?.alias)
48
55
  alias = rosterRow.alias;
@@ -5,7 +5,9 @@
5
5
  * Issue #331: WebSocket authentication via Cookie header
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.__internal = exports.WORKTREE_RENAMED_EVENT_TYPE = void 0;
8
+ exports.__internal = exports.WORKTREE_RENAMED_EVENT_TYPE = exports.REMOTE_ONLY_AUTH_SCOPE = exports.CM_INGRESS_HEADER = void 0;
9
+ exports.stampIngress = stampIngress;
10
+ exports.isIngressAuthExempt = isIngressAuthExempt;
9
11
  exports.handleProxyUpgrade = handleProxyUpgrade;
10
12
  exports.setupWebSocket = setupWebSocket;
11
13
  exports.broadcast = broadcast;
@@ -61,6 +63,59 @@ const TERMINAL_FALLBACK_CAPTURE_LINES = -200;
61
63
  * Issue #671.
62
64
  */
63
65
  const PROXY_ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1']);
66
+ /**
67
+ * The header `server.ts` overwrites on every request and upgrade.
68
+ *
69
+ * Written out as a literal in `server.ts` would be a second spelling of the same
70
+ * name, so it lives here and `server.ts` imports it — that file already imports
71
+ * this module at eval time (`setupWebSocket`), which is why this is the one
72
+ * place the constant can live without adding a module graph to `server.ts`'s
73
+ * eval-time graph (see the `x-cm-raw-url` comment there for what that costs).
74
+ * `middleware.ts` cannot import it: it runs on the Edge runtime and this module
75
+ * is Node-only, so it inlines the same literal under the C001 rule that already
76
+ * duplicates the auth constants.
77
+ */
78
+ exports.CM_INGRESS_HEADER = 'x-cm-ingress';
79
+ /**
80
+ * Value of `CM_AUTH_SCOPE` that exempts the local listener from authentication.
81
+ *
82
+ * Any other value — absent, misspelt, `all` — leaves every listener
83
+ * authenticated, which is the pre-#2489 behaviour and the fail-closed default.
84
+ */
85
+ exports.REMOTE_ONLY_AUTH_SCOPE = 'remote-only';
86
+ /**
87
+ * Stamp the listener identity onto a request, discarding whatever the client
88
+ * sent under the same name.
89
+ *
90
+ * A plain assignment, not a merge: Node joins a repeated unknown header into
91
+ * one comma-separated string, and assigning replaces that string wholesale.
92
+ * Called for EVERY request on EVERY listener — a listener that forgot to stamp
93
+ * would leave the caller's own value in place, which is the one way this scheme
94
+ * can fail open.
95
+ *
96
+ * @param headers - The mutable `req.headers` of an incoming request or upgrade
97
+ * @param ingress - The listener the request arrived on
98
+ */
99
+ function stampIngress(headers, ingress) {
100
+ headers[exports.CM_INGRESS_HEADER] = ingress;
101
+ }
102
+ /**
103
+ * Whether this request may skip authentication because of where it arrived.
104
+ *
105
+ * Fail-closed on both halves: the scope has to be exactly `remote-only`, and
106
+ * the stamp has to be exactly `local`. An unstamped request (no listener
107
+ * claimed it), an array value (a repeated header that somehow survived) and an
108
+ * unknown value all authenticate.
109
+ *
110
+ * @param headers - Request headers, after `server.ts` has stamped them
111
+ * @param env - Environment to read `CM_AUTH_SCOPE` from; defaults to the process's
112
+ * @returns true when the request came in on the local listener of a `remote-only` server
113
+ */
114
+ function isIngressAuthExempt(headers, env = process.env) {
115
+ if (env.CM_AUTH_SCOPE !== exports.REMOTE_ONLY_AUTH_SCOPE)
116
+ return false;
117
+ return headers[exports.CM_INGRESS_HEADER] === 'local';
118
+ }
64
119
  /**
65
120
  * Write a minimal HTTP error response to a raw upgrade socket and destroy it.
66
121
  * Used for 4xx / 5xx rejections in the /proxy/<prefix> upgrade branch.
@@ -239,6 +294,7 @@ function isExpectedWebSocketError(error) {
239
294
  * Issue #331: Added auth check on WebSocket upgrade
240
295
  *
241
296
  * @param server - HTTP or HTTPS server instance
297
+ * @param options - Extra listeners to serve the same WebSocket from (Issue #2489)
242
298
  *
243
299
  * @example
244
300
  * ```typescript
@@ -247,7 +303,7 @@ function isExpectedWebSocketError(error) {
247
303
  * server.listen(3000);
248
304
  * ```
249
305
  */
250
- function setupWebSocket(server) {
306
+ function setupWebSocket(server, options) {
251
307
  wss = new ws_1.WebSocketServer({ noServer: true });
252
308
  // Issue #2220: this instance now owns the sockets, so publish the capability
253
309
  // the other module instances have to borrow. Registered before anything can
@@ -262,7 +318,7 @@ function setupWebSocket(server) {
262
318
  // map, which is what a broadcast has to reach.
263
319
  (0, waiting_broadcast_1.startWaitingStatusBroadcast)(handleBroadcast);
264
320
  // Handle upgrade requests - only accept app WebSocket connections, not Next.js HMR
265
- server.on('upgrade', (request, socket, head) => {
321
+ const handleUpgrade = (request, socket, head) => {
266
322
  const pathname = request.url || '/';
267
323
  // Let Next.js handle its own HMR WebSocket connections in development.
268
324
  // In production there are no /_next/ WebSocket connections (no HMR).
@@ -292,7 +348,13 @@ function setupWebSocket(server) {
292
348
  }
293
349
  }
294
350
  // Issue #331: WebSocket authentication via Cookie header
295
- if ((0, auth_1.isAuthEnabled)()) {
351
+ //
352
+ // Issue #2489: `isIngressAuthExempt` is the ONLY thing that can skip this,
353
+ // and it reads the same stamped header `middleware.ts` reads. HTTP and WS
354
+ // have to agree here: a phone drives the terminal over this socket, so a WS
355
+ // path that exempted more than the HTTP path would hand the tunnel a live
356
+ // shell while the screen behind it still asked for a password.
357
+ if ((0, auth_1.isAuthEnabled)() && !isIngressAuthExempt(request.headers)) {
296
358
  const cookieHeader = request.headers.cookie || '';
297
359
  const cookies = (0, auth_1.parseCookies)(cookieHeader);
298
360
  const token = cookies[auth_1.AUTH_COOKIE_NAME];
@@ -334,7 +396,14 @@ function setupWebSocket(server) {
334
396
  wss.handleUpgrade(request, socket, head, (ws) => {
335
397
  wss.emit('connection', ws, request);
336
398
  });
337
- });
399
+ };
400
+ // Issue #2489: every listener gets the same handler, and therefore the same
401
+ // auth decision. `server.ts` has already stamped `x-cm-ingress` by the time
402
+ // this runs (its stamping listener is registered first, and listeners fire in
403
+ // registration order), so the handler never has to know which socket it is on.
404
+ for (const target of [server, ...(options?.additionalServers ?? [])]) {
405
+ target.on('upgrade', handleUpgrade);
406
+ }
338
407
  // Handle WebSocket server errors (e.g., invalid frames from clients)
339
408
  wss.on('error', (error) => {
340
409
  logger.error('server:error', { error: error.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commandmate",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "A local control plane for agent CLIs — orchestration and visibility for Claude Code, Codex, Gemini CLI, and more across Git worktrees",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1 +0,0 @@
1
- "use strict";exports.id=1049,exports.ids=[964,1049,8583],exports.modules={3421:(a,b,c)=>{Object.defineProperty(b,"I",{enumerable:!0,get:function(){return g}});let d=c(71237),e=c(55088),f=c(17679);async function g(a,b,c,g){if((0,d.isNodeNextResponse)(b)){var h;b.statusCode=c.status,b.statusMessage=c.statusText;let d=["set-cookie","www-authenticate","proxy-authenticate","vary"];null==(h=c.headers)||h.forEach((a,c)=>{if("x-middleware-set-cookie"!==c.toLowerCase())if("set-cookie"===c.toLowerCase())for(let d of(0,f.splitCookiesString)(a))b.appendHeader(c,d);else{let e=void 0!==b.getHeader(c);(d.includes(c.toLowerCase())||!e)&&b.appendHeader(c,a)}});let{originalResponse:i}=b;c.body&&"HEAD"!==a.method?await (0,e.pipeToNodeResponse)(c.body,i,g):i.end()}}},7329:(a,b,c)=>{c.d(b,{Hv:()=>m,W3:()=>k,uK:()=>l});var d=c(90787),e=c(52660),f=c(18201),g=c(23854),h=c(49798),i=c(67164);let j=(0,c(72626).h)("relay-service");function k(a,b){for(let c of[b.from.worktreeId,b.to.worktreeId])if(!(0,h.r)(a,c))return{ok:!1,refusal:{code:"RELAY_WORKTREE_NOT_FOUND",message:`Worktree '${c}' not found`,worktreeId:c}};let c=(0,g.$G)({from:b.from,to:b.to,parentHops:function(a,b){try{let g=(0,h.S)(a,b),i=(0,e.getMessages)(a,b.worktreeId,{limit:5,limitUnit:"pairs",cliToolId:g.cliToolId,instanceId:b.instanceId,matchResolvedInstance:!0}).filter(a=>"user"===a.role),j=i.findIndex(a=>"relay"===a.messageType);if(-1===j)return null;let k=i[j];for(let a=0;a<j;a+=1){var c;if(c=i[a],c.timestamp.getTime()<k.timestamp.getTime()||(0,f.pk)(c.content)!==(0,f.pk)(k.content))return null}let l=(0,e.parseRelayRequestId)(k.requestId);if(!l)return null;return(0,d.Ip)(a,l)?.hops??null}catch(a){return j.warn("relay-parent-lookup-failed",{worktreeId:b.worktreeId,instanceId:b.instanceId,error:a instanceof Error?a.message:String(a)}),null}}(a,b.from),allowRelayChain:!0===b.allowRelayChain,openBetween:(0,d.Oz)(a,b.from,b.to)});if((0,g.Qe)(c))return{ok:!1,refusal:c};let k=b.now??Date.now(),l=(0,d.CR)(a,{from:b.from,to:b.to,hops:c.hops,expiresAt:k+(0,g.AL)(b.ttlMs),now:k});return j.info("relay-opened",{relayId:l.id,from:`${l.from.worktreeId}/${l.from.instanceId}`,to:`${l.to.worktreeId}/${l.to.instanceId}`,hops:l.hops}),(0,i._)(a,l.id,"requested",(0,h.S)(a,l.from),(0,h.S)(a,l.to),k),{ok:!0,relay:l}}function l(a,b,c=Date.now()){let e=(0,d.Ip)(a,b);return e?(0,d.m_)(a,b,c)?(j.info("relay-cancelled",{relayId:b}),{ok:!0,relay:(0,d.Ip)(a,b)??e}):{ok:!1,reason:"already_closed",relay:e}:{ok:!1,reason:"not_found",relay:null}}function m(a,b){return(0,d.M4)(a,b)}},18201:(a,b,c)=>{c.d(b,{Ce:()=>g,pk:()=>e});let d=(0,c(72626).h)("lib/history/user-turn-recorder");function e(a){return a.replace(/\r\n?/g,"\n").split("\n").map(a=>a.replace(/[ \t ]+$/u,"")).join("\n").trim()}function f(a){return{worktreeId:a.worktreeId,cliToolId:a.cliToolId,instanceId:a.instanceId??a.cliToolId}}async function g(a,b,h,i,j={}){let k=a.instanceId??a.cliToolId,l=e(h);if(0===b.length||0===l.length)return{outcome:"skipped",messageId:null,timestampMs:null};try{let[{getDbInstance:g},m,{broadcastMessage:n}]=await Promise.all([Promise.resolve().then(c.bind(c,88807)),Promise.resolve().then(c.bind(c,52660)),Promise.resolve().then(c.bind(c,49640))]),o=g(),p=m.findMessageByRequestId(o,a.worktreeId,b);if(p)return{outcome:"already-recorded",messageId:p.id,timestampMs:p.timestamp.getTime()};let q=i>0?i:Date.now(),r=function(a,b,c,g,h,i,j,k){var l,m,n;let o=Math.min(j-12e4,k.adoptionFromMs??1/0),p=(l=a.findUnkeyedUserMessages(b,{worktreeId:c.worktreeId,cliToolId:c.cliToolId,instanceId:g,fromMs:o,toMs:j+12e4,includeRelayDelivered:!0}),m=i,n=j,l.filter(a=>e(a.content)===m).sort((a,b)=>Math.abs(a.timestamp.getTime()-n)-Math.abs(b.timestamp.getTime()-n)));for(let e of p)if(!e.requestId&&a.setMessageRequestId(b,e.id,h))return d.info("user-turn-adopted",{...f(c),requestId:h,messageId:e.id,driftMs:e.timestamp.getTime()-j}),{outcome:"adopted",messageId:e.id,timestampMs:e.timestamp.getTime()};let q=p.find(a=>a.requestId);return q?(d.info("user-turn-already-relayed",{...f(c),requestId:h,messageId:q.id,relayRequestId:q.requestId??null,driftMs:q.timestamp.getTime()-j}),{outcome:"already-recorded",messageId:q.id,timestampMs:q.timestamp.getTime()}):null}(m,o,a,k,b,l,q,j);if(r)return r;let s=m.createMessage(o,{worktreeId:a.worktreeId,role:"user",content:h,messageType:"normal",timestamp:new Date(q),cliToolId:a.cliToolId,instanceId:k,requestId:b});return n("message",{worktreeId:a.worktreeId,message:s}),d.info("user-turn-recorded",{...f(a),requestId:b,contentLength:h.length}),{outcome:"inserted",messageId:s.id,timestampMs:q}}catch(c){return d.error("user-turn-record-failed",{...f(a),requestId:b,error:c instanceof Error?c.message:String(c)}),{outcome:"failed",messageId:null,timestampMs:null}}}},40964:(a,b,c)=>{c.d(b,{m:()=>g});var d=c(15194);let e=/^[a-zA-Z0-9_-]+$/;function f(a){if(!e.test(a))throw Error(`Invalid session name format: ${a}`)}function g(a,b,c){let e=`mcbd-${a}-${b}`;if(!c||c===a)return f(e),e;let g=(0,d.pL)(c,a),h=g?`${e}-${g}`:e;return f(h),h}},95736:(a,b,c)=>{a.exports=c(44870)}};