thincoder 0.12.62 → 0.12.63

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 (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  4. package/package.json +6 -5
  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 +240 -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 +59 -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 +25 -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-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. 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
- }