thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +257 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +24 -1
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
@@ -1,248 +0,0 @@
1
- /**
2
- * mcp/transport-http.mjs — MCP HTTP + SSE transport (Streamable HTTP)
3
- */
4
- import { rpcId, CALL_TIMEOUT_MS, ENDPOINT_WAIT_MS, INIT_TIMEOUT_MS, withTimeout } from "./helpers.mjs"
5
-
6
- /** Create an MCP HTTP+SSE transport for Streamable HTTP servers */
7
- export function httpTransport(baseURL, extraHeaders = {}) {
8
- const url = baseURL.replace(/\/+$/, "")
9
- let sessionId = null
10
- let closed = false
11
- let eventSource = null
12
- // 2026-09-01 MCP.md §4 D-1:GET SSE 不可用(405/不支持)降级后的纯 POST 模式标记。
13
- // POST-only server(如 glm-websearch)无流可断:isAlive 不得因 eventSource == null 判死。
14
- let postOnly = false
15
- let abortController = null
16
- let postUrl = url
17
- let legacySSE = false
18
- let deadFired = false
19
- let deadListeners = new Set()
20
-
21
- /** 2026-08-31 MCP 会诊 P5:意外死亡通知(SSE 流断/error,非主动 close)。 */
22
- const fireDead = (msg) => {
23
- if (deadFired) return
24
- deadFired = true
25
- for (const cb of deadListeners) { try { cb(msg) } catch { /* listener error */ } }
26
- }
27
- const onDead = (cb) => { deadListeners.add(cb); return () => deadListeners.delete(cb) }
28
-
29
- const headers = () => {
30
- const h = { "Content-Type": "application/json", Accept: "text/event-stream, application/json", ...extraHeaders }
31
- if (sessionId) h["Mcp-Session-Id"] = sessionId
32
- return h
33
- }
34
-
35
- const pending = new Map()
36
-
37
- async function* parseSSE(response) {
38
- const reader = response.body.getReader()
39
- const decoder = new TextDecoder()
40
- let buf = ""
41
- let current = { data: "", event: "message" }
42
- try {
43
- while (true) {
44
- const { done, value } = await reader.read()
45
- if (done) break
46
- buf += decoder.decode(value, { stream: true })
47
- const lines = buf.split("\n")
48
- buf = lines.pop() ?? ""
49
- for (const raw of lines) {
50
- const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw
51
- if (line === "") {
52
- if (current.data) {
53
- yield { event: current.event, data: current.data.trimEnd() }
54
- current = { data: "", event: "message" }
55
- }
56
- } else if (line.startsWith("data:")) {
57
- current.data += (current.data ? "\n" : "") + line.slice(5).replace(/^ /, "")
58
- } else if (line.startsWith("event:")) {
59
- current.event = line.slice(6).trim()
60
- }
61
- }
62
- }
63
- } finally {
64
- reader.releaseLock()
65
- }
66
- }
67
-
68
- async function openSSE() {
69
- if (closed) return
70
- abortController?.abort()
71
- abortController = new AbortController()
72
- const signal = AbortSignal.any([abortController.signal, AbortSignal.timeout(INIT_TIMEOUT_MS)])
73
- const resp = await fetch(url, {
74
- method: "GET",
75
- headers: { Accept: "text/event-stream", ...extraHeaders },
76
- signal,
77
- })
78
- if (!resp.ok) throw new Error(`SSE connect failed: HTTP ${resp.status}`)
79
- eventSource = parseSSE(resp)
80
- let endpointReady
81
- const gotEndpoint = new Promise((resolve) => { endpointReady = resolve })
82
-
83
- ;(async () => {
84
- try {
85
- for await (const { event, data } of eventSource) {
86
- if (closed) break
87
- if (event === "endpoint") {
88
- postUrl = new URL(data.trim(), url).href
89
- legacySSE = true
90
- endpointReady()
91
- continue
92
- }
93
- try {
94
- const msg = JSON.parse(data)
95
- const resolver = pending.get(msg.id)
96
- if (resolver) {
97
- pending.delete(msg.id)
98
- resolver(msg)
99
- }
100
- } catch { /* not JSON, ignore */ }
101
- }
102
- // 正常走完 = server 关闭了流(网络/对端退出)→ 非主动关闭视为死亡
103
- if (!closed) fireDead("SSE stream ended")
104
- } catch (error) {
105
- const wasClosed = closed
106
- if (!wasClosed) {
107
- for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: `SSE error: ${error.message}` } })
108
- pending.clear()
109
- fireDead(`SSE error: ${error.message}`)
110
- }
111
- }
112
- })()
113
-
114
- const wait = new Promise((resolve) => {
115
- const t = setTimeout(resolve, ENDPOINT_WAIT_MS)
116
- t.unref?.()
117
- })
118
- await Promise.race([gotEndpoint, wait])
119
- }
120
-
121
- async function postRequest(method, params) {
122
- const id = rpcId()
123
- const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
124
-
125
- if (legacySSE) {
126
- // 2026-08-31 MCP 会诊 #10:legacy 模式下 POST 若直接带回 JSON-RPC body(违规 server,
127
- // 规范是响应经 GET SSE 流回)——原实现对 resp.ok 什么都不做 → pending 挂满 120s。
128
- // 空 body = 规范行为(等待 SSE 流);非空 JSON = 违规 server,直接解析 resolve。
129
- return new Promise((resolve) => {
130
- pending.set(id, resolve)
131
- fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
132
- .then(async (resp) => {
133
- if (!resp.ok) {
134
- pending.delete(id)
135
- resolve({ id, error: { code: -32000, message: `POST failed: HTTP ${resp.status}` } })
136
- return
137
- }
138
- const raw = await resp.text().catch(() => "")
139
- if (raw.trim()) {
140
- try {
141
- const msg = JSON.parse(raw)
142
- if (msg.id === id) { pending.delete(id); resolve(msg); return }
143
- } catch { /* ignore — pending 交给 SSE 流 */ }
144
- }
145
- // 空体或非本次 id 的 JSON:保持 pending 等 GET SSE 流回包
146
- })
147
- .catch((e) => {
148
- pending.delete(id)
149
- resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
150
- })
151
- }).finally(() => pending.delete(id))
152
- }
153
-
154
- // 2026-08-31 MCP 会诊 P4:Streamable HTTP 规范路径(POST→202→GET SSE 回包)。
155
- // 原实现非 legacy 分支从不注册 pending:202 空 body → resp.json() 抛 SyntaxError,
156
- // 而 SSE 流里 pending.get(id) 永远 miss——每次调用挂满 120s。
157
- // 现在两类通道统一:先注册 pending,POST 得到的结果(直接 body / SSE / 202 等待)
158
- // 都经 pending resolve;SSE 流由 openSSE() 转发。200+JSON 直接 body 解析。
159
- return new Promise((resolve) => {
160
- pending.set(id, resolve)
161
- fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
162
- .then(async (resp) => {
163
- const ct = resp.headers.get("content-type") ?? ""
164
- const newSessionId = resp.headers.get("Mcp-Session-Id")
165
- if (newSessionId) sessionId = newSessionId
166
- // 202: 已接受,响应将经 GET SSE 流回——pending 保持,由 openSSE 转发 resolve
167
- if (resp.status === 202) return
168
- if (!resp.ok) {
169
- pending.delete(id)
170
- resolve({ id, error: { code: -32000, message: `HTTP ${resp.status}` } })
171
- return
172
- }
173
- if (ct.includes("text/event-stream")) {
174
- // 响应体本身就是一条 SSE(一次性流):解析匹配该 id
175
- try {
176
- for await (const { data } of parseSSE(resp)) {
177
- try {
178
- const msg = JSON.parse(data)
179
- if (msg.id === id) { pending.delete(id); resolve(msg); return }
180
- } catch { /* skip */ }
181
- }
182
- pending.delete(id)
183
- resolve({ id, error: { code: -32000, message: "No JSON-RPC response in SSE stream" } })
184
- } catch (e) {
185
- pending.delete(id)
186
- resolve({ id, error: { code: -32000, message: `SSE response failed: ${e.message}` } })
187
- }
188
- return
189
- }
190
- // 直接 JSON body:拿掉 pending 立即解析
191
- pending.delete(id)
192
- try {
193
- resolve(await resp.json())
194
- } catch (e) {
195
- resolve({ id, error: { code: -32000, message: `invalid JSON body: ${e.message}` } })
196
- }
197
- })
198
- .catch((e) => {
199
- if (pending.delete(id)) {
200
- resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
201
- }
202
- })
203
- }).finally(() => pending.delete(id))
204
- }
205
-
206
- const send = async (method, params) => withTimeout(postRequest(method, params), CALL_TIMEOUT_MS)
207
-
208
- const notify = (method, params) => {
209
- fetch(postUrl, {
210
- method: "POST",
211
- headers: headers(),
212
- body: JSON.stringify({ jsonrpc: "2.0", method, params }),
213
- signal: AbortSignal.timeout(10_000),
214
- }).catch((e) => {
215
- // notify is fire-and-forget by design, but log network errors for debugging
216
- if (e.name !== "AbortError") console.error(`[mcp] notify failed: ${e.message}`)
217
- })
218
- }
219
-
220
- const close = () => {
221
- closed = true
222
- abortController?.abort()
223
- if (sessionId) {
224
- fetch(postUrl, {
225
- method: "DELETE",
226
- headers: { "Mcp-Session-Id": sessionId, ...extraHeaders },
227
- signal: AbortSignal.timeout(5_000),
228
- }).catch((e) => {
229
- // close is best-effort cleanup; log but don't throw
230
- if (e.name !== "AbortError") console.error(`[mcp] close DELETE failed: ${e.message}`)
231
- })
232
- sessionId = null
233
- }
234
- for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: "Connection closed" } })
235
- pending.clear()
236
- }
237
-
238
- /** D-1:openSSE 降级(GET 405/不支持)后由 connect 链调用——标记纯 Streamable POST 模式 */
239
- const markPostOnly = () => { postOnly = true }
240
-
241
- return {
242
- send, notify, close, openSSE, url, headers: extraHeaders,
243
- // F1:POST-only 降级(postOnly)与 legacy SSE 流(eventSource)都算活连接——
244
- // 不得因 eventSource == null 误判死(glm-websearch "reconnect failed" 根因)。
245
- isAlive: () => !closed && (eventSource != null || postOnly),
246
- onDead, markPostOnly,
247
- }
248
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * mcp/transport-stdio.mjs — MCP stdio transport
3
- */
4
- import { spawn } from "node:child_process"
5
- import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
6
-
7
- /** Kill the child AND its whole process tree.
8
- * win32: cmd.exe 包装 spawn 的孙进程(npx/node)必须 taskkill /T /F 才能杀净
9
- * (2026-08-31 MCP 会诊 P2:此前只 child.kill() 杀 cmd.exe 壳,真 server 成僵尸
10
- * 并在 Windows 团队每人每次断开/重连泄漏一批);
11
- * POSIX: SIGTERM 后 2s 未退 SIGKILL 兜底。 */
12
- function killTree(child) {
13
- if (!child.pid) return
14
- if (process.platform === "win32") {
15
- try { spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }) } catch { /* best effort */ }
16
- return
17
- }
18
- try {
19
- child.kill("SIGTERM")
20
- setTimeout(() => { try { child.kill("SIGKILL") } catch { /* already gone */ } }, 2000).unref?.()
21
- } catch { /* best effort */ }
22
- }
23
-
24
- /** Create an MCP stdio transport over a spawned child process.
25
- * @param {Object} [env] — extra environment variables merged on top of process.env */
26
- export function stdioTransport(command, args, env) {
27
- const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env, ...env } }
28
- const child =
29
- process.platform === "win32" && !/\.exe$/i.test(command)
30
- ? spawn("cmd.exe", ["/d", "/s", "/c", [command, ...(args ?? [])].map(quoteArg).join(" ")], {
31
- ...spawnOptions,
32
- windowsVerbatimArguments: true,
33
- })
34
- : spawn(command, args ?? [], spawnOptions)
35
-
36
- const pending = new Map()
37
- const decoder = new TextDecoder()
38
- let buffer = ""
39
- let stderrTail = ""
40
- let spawnError = null
41
- let closed = false
42
- let deadFired = false
43
- let deadListeners = new Set()
44
-
45
- /** 2026-08-31 MCP 会诊 P5:意外死亡通知(进程自杀/崩溃,非主动 close)。
46
- * 主动 close() 先置 closed → 后续 close 事件不触发。 */
47
- const fireDead = (msg) => {
48
- if (deadFired) return
49
- deadFired = true
50
- for (const cb of deadListeners) { try { cb(msg) } catch { /* listener error */ } }
51
- }
52
- const onDead = (cb) => { deadListeners.add(cb); return () => deadListeners.delete(cb) }
53
-
54
- const failAll = (message) => {
55
- for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message } })
56
- pending.clear()
57
- }
58
-
59
- child.stdout.on("data", (chunk) => {
60
- buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true })
61
- const lines = buffer.split("\n")
62
- buffer = lines.pop() ?? ""
63
- for (const line of lines) {
64
- if (!line.trim()) continue
65
- try {
66
- const msg = JSON.parse(line)
67
- const resolver = pending.get(msg.id)
68
- if (resolver) {
69
- pending.delete(msg.id)
70
- resolver(msg)
71
- }
72
- } catch { /* non-JSON line, ignore */ }
73
- }
74
- })
75
-
76
- child.stderr.on("data", (chunk) => {
77
- stderrTail = (stderrTail + chunk.toString()).slice(-2000)
78
- })
79
-
80
- child.stdin.on("error", () => {})
81
- child.on("error", (error) => {
82
- spawnError = error
83
- closed = true
84
- failAll(`spawn failed: ${error.message}`)
85
- })
86
- child.on("close", () => {
87
- const wasClosed = closed // 主动 close() 先置 closed → 意外事件不重复
88
- closed = true
89
- const lastLine = stderrTail.trim().split("\n").pop()
90
- failAll(`Connection closed${lastLine ? ` | stderr: ${lastLine}` : ""}`)
91
- if (!wasClosed) fireDead(`MCP server process exited${lastLine ? ` | stderr: ${lastLine}` : ""}`)
92
- })
93
-
94
- const send = (method, params, signal) => {
95
- if (spawnError) return Promise.resolve({ id: null, error: { code: -32000, message: `spawn failed: ${spawnError.message}` } })
96
- if (closed) return Promise.reject(new Error("MCP connection closed"))
97
- const id = rpcId()
98
- let resolveFn
99
- const promise = new Promise((resolve) => { resolveFn = resolve; pending.set(id, resolve) })
100
- // 2026-08-31 MCP 会诊 P7:上层 signal 中断时即刻作废 pending(原实现等满
101
- // CALL_TIMEOUT_MS 才清)并向 server 发 cancelled 通知。
102
- const onAbort = () => {
103
- pending.delete(id)
104
- try { notify("notifications/cancelled", { requestId: id }) } catch { /* ignore */ }
105
- resolveFn({ id, error: { code: -32000, message: "Request cancelled by user" } })
106
- }
107
- if (signal) {
108
- if (signal.aborted) return Promise.reject((signal.reason instanceof Error) ? signal.reason : new DOMException("Aborted", "AbortError"))
109
- signal.addEventListener("abort", onAbort, { once: true })
110
- promise.finally(() => signal.removeEventListener("abort", onAbort)).catch?.(() => {})
111
- }
112
- try {
113
- child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n")
114
- } catch (error) {
115
- pending.delete(id)
116
- return Promise.resolve({ id: null, error: { code: -32000, message: `stdin write failed: ${error.message}` } })
117
- }
118
- return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
119
- }
120
-
121
- const notify = (method, params) => {
122
- if (closed) return
123
- try {
124
- child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n")
125
- } catch { /* ignore */ }
126
- }
127
-
128
- return {
129
- send, notify,
130
- close: () => {
131
- // 2026-08-31 MCP 会诊 P2:close ≠ 只杀进程——先置 closed + failAll(在途请求
132
- // 立即得到 "Connection closed" 而非挂满 120s),再杀整个进程树。
133
- closed = true
134
- failAll("Connection closed")
135
- if (child.exitCode === null) killTree(child)
136
- },
137
- isAlive: () => !closed && !spawnError && child.exitCode === null,
138
- onDead,
139
- }
140
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * mcp/transport-ws.mjs — MCP WebSocket transport
3
- */
4
- import { rpcId, INIT_TIMEOUT_MS, CALL_TIMEOUT_MS, withTimeout, withAuthToken } from "./helpers.mjs"
5
-
6
- /** Create an MCP WebSocket transport */
7
- export function wsTransport(wsUrl, extraHeaders = {}) {
8
- const pending = new Map()
9
- let closed = false
10
- let ws = null
11
- let deadFired = false
12
- let deadListeners = new Set()
13
-
14
- /** 2026-08-31 MCP 会诊 P5:意外死亡通知(非主动 close 的连接断开)。 */
15
- const fireDead = (msg) => {
16
- if (deadFired) return
17
- deadFired = true
18
- for (const cb of deadListeners) { try { cb(msg) } catch { /* listener error */ } }
19
- }
20
- const onDead = (cb) => { deadListeners.add(cb); return () => deadListeners.delete(cb) }
21
-
22
- const failAll = (message) => {
23
- for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message } })
24
- pending.clear()
25
- }
26
-
27
- const connect = () => {
28
- if (closed) throw new Error("MCP WebSocket connection closed")
29
- // 2026-08-31 MCP 会诊 #10:Authorization 经 subprotocol(bearer.<token>)传递,
30
- // 不注入 URL query(防代理/网关日志泄露);无认证时 protocols 为空数组。
31
- const { url: wsUrlResolved, protocols } = withAuthToken(wsUrl, extraHeaders.Authorization)
32
- ws = new WebSocket(wsUrlResolved, protocols.length ? protocols : undefined)
33
-
34
- return new Promise((resolve, reject) => {
35
- let settled = false
36
- const settleErr = (err) => { if (!settled) { settled = true; reject(err) } }
37
- const timeout = setTimeout(() => {
38
- settleErr(new Error(`WebSocket connect timeout: ${wsUrl}`))
39
- try { ws.close() } catch { /* ignore */ }
40
- }, INIT_TIMEOUT_MS)
41
-
42
- ws.addEventListener("open", () => {
43
- if (settled) return
44
- settled = true
45
- clearTimeout(timeout)
46
- resolve()
47
- })
48
-
49
- ws.addEventListener("message", (event) => {
50
- try {
51
- const msg = JSON.parse(event.data.toString())
52
- const resolver = pending.get(msg.id)
53
- if (resolver) {
54
- pending.delete(msg.id)
55
- resolver(msg)
56
- }
57
- } catch { /* not JSON, ignore */ }
58
- })
59
-
60
- ws.addEventListener("error", (event) => {
61
- const wasClosed = closed
62
- clearTimeout(timeout)
63
- closed = true
64
- const errMsg = event.message || event.error?.message || "WebSocket error"
65
- settleErr(new Error(errMsg))
66
- failAll(errMsg)
67
- if (!wasClosed) fireDead(errMsg)
68
- })
69
-
70
- ws.addEventListener("close", () => {
71
- const wasClosed = closed
72
- clearTimeout(timeout)
73
- closed = true
74
- // 2026-08-31 MCP 会诊 P1:握手期间 disconnect(close 先于 open 且无 error)——
75
- // 原实现 close 只 clearTimeout + failAll,connect promise 永不 settle(启动挂死)。
76
- // settled 标志保证 close 未开成必 reject。
77
- settleErr(new Error("WebSocket closed before connection established"))
78
- failAll("WebSocket closed")
79
- if (!wasClosed) fireDead("WebSocket closed")
80
- })
81
- })
82
- }
83
-
84
- const send = (method, params, signal) => {
85
- if (closed) return Promise.reject(new Error("MCP WebSocket connection closed"))
86
- const id = rpcId()
87
- let resolveFn
88
- const promise = new Promise((resolve) => { resolveFn = resolve; pending.set(id, resolve) })
89
- // 2026-08-31 MCP 会诊 P7:abort 即刻作废 pending + 发 cancelled(原等满 120s)
90
- if (signal) {
91
- if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"))
92
- const onAbort = () => {
93
- pending.delete(id)
94
- try { notify("notifications/cancelled", { requestId: id }) } catch { /* ignore */ }
95
- resolveFn({ id, error: { code: -32000, message: "Request cancelled by user" } })
96
- }
97
- signal.addEventListener("abort", onAbort, { once: true })
98
- promise.finally(() => signal.removeEventListener("abort", onAbort)).catch?.(() => {})
99
- }
100
- try {
101
- ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }))
102
- } catch (error) {
103
- pending.delete(id)
104
- return Promise.resolve({ id: null, error: { code: -32000, message: `ws send failed: ${error.message}` } })
105
- }
106
- return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
107
- }
108
-
109
- const notify = (method, params) => {
110
- if (!closed && ws?.readyState === WebSocket.OPEN) {
111
- ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }))
112
- }
113
- }
114
-
115
- const close = () => {
116
- closed = true
117
- failAll("Connection closed")
118
- try { ws?.close() } catch { /* ignore */ }
119
- }
120
-
121
- return { send, notify, close, connect, isAlive: () => !closed && ws?.readyState === WebSocket.OPEN, onDead }
122
- }