thincoder 0.12.61 → 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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  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 -371
  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 +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  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-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  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 +31 -4
  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 +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -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
- }
package/src/mcp.mjs DELETED
@@ -1,295 +0,0 @@
1
- /**
2
- * mcp.mjs — MCP (Model Context Protocol) client
3
- * config: { command, args?, name } or { url, name, headers? } or { wsUrl, name, headers? }
4
- */
5
- import { INIT_TIMEOUT_MS, withTimeout, sanitizeToolName } from "./mcp/helpers.mjs"
6
- import { stdioTransport } from "./mcp/transport-stdio.mjs"
7
- import { httpTransport } from "./mcp/transport-http.mjs"
8
- import { wsTransport } from "./mcp/transport-ws.mjs"
9
-
10
- /** F6/D-5:config.token → `Authorization: Bearer <token>` 合成(仅当 headers 未显式给
11
- * Authorization——显式优先,向后兼容)。合成发生在传给 transport 前,不写回 config。 */
12
- export function withBearerToken(config) {
13
- if (!config?.token || config.headers?.Authorization) return config
14
- return { ...config, headers: { ...config.headers, Authorization: `Bearer ${config.token}` } }
15
- }
16
-
17
- /** Race a pending MCP request against an abort signal — a hung MCP server must not
18
- * hold the turn hostage. signal absent → passthrough. */
19
- async function sendWithSignal(promise, signal) {
20
- if (!signal) return promise
21
- return new Promise((resolve, reject) => {
22
- const onAbort = () => {
23
- const e = new DOMException("The operation was aborted", "AbortError")
24
- e.reason = signal.reason
25
- reject(e)
26
- }
27
- if (signal.aborted) return onAbort()
28
- signal.addEventListener("abort", onAbort, { once: true })
29
- promise.then(
30
- (v) => { signal.removeEventListener("abort", onAbort); resolve(v) },
31
- (e) => { signal.removeEventListener("abort", onAbort); reject(e) },
32
- )
33
- })
34
- }
35
-
36
- // ---- MCP lifecycle ----
37
-
38
- /** 2026-08-31 MCP 会诊 P5:CLI session 注册表(serverName → session)。
39
- * session.state.transport 可变(重连替换);buildTools 的 execute 动态取
40
- * session.state.transport——server 崩溃后无需重建 agent.tools 即自愈。
41
- * 2026-09-01 MCP.md §4:导出给 /mcp test 的零副作用断言用(probe 后 _sessions 不增)。 */
42
- export const _sessions = new Map()
43
- /** 退避重连进行中(serverName → promise)——与 vscode 语义对齐;延迟表见 _mcpHooks。 */
44
- const _reconnecting = new Map()
45
-
46
- /** 2026-08-31 MCP 会诊 P5:测试钩子(退避延迟可替换,惯例同 rate.mjs _rateHooks)。
47
- * scheduleReconnect 与 ensureAlive 读这里的 delay/reconnectDelays——测试可注入微秒级延迟。 */
48
- export const _mcpHooks = {
49
- delay: (ms) => new Promise((r) => setTimeout(r, ms)),
50
- reconnectDelays: [1000, 2000, 4000, 8000],
51
- }
52
-
53
- /** 按 config 创建并完成握手的 transport(findTransportConfig 与 vscode 对齐)。 */
54
- async function createConnectedTransport(rawConfig, serverName) {
55
- const config = withBearerToken(rawConfig) // F6/D-5:token 合成(不写回原 config)
56
- let transport
57
- if (config.wsUrl) {
58
- transport = wsTransport(config.wsUrl, config.headers ?? {})
59
- await transport.connect()
60
- } else if (config.url) {
61
- transport = httpTransport(config.url, config.headers ?? {})
62
- try {
63
- await transport.openSSE()
64
- } catch {
65
- // Server doesn't support GET SSE — degrade to pure Streamable HTTP POST mode.
66
- // MCP.md §4 D-1:显式标记 postOnly——POST-only server(glm-websearch 类)isAlive
67
- // 不得因 eventSource == null 误判死(否则 ensureAlive 触发无意义重连循环)。
68
- transport.markPostOnly()
69
- }
70
- } else {
71
- transport = stdioTransport(config.command, config.args ?? [], config.env)
72
- }
73
- const mcpTools = await doInitialize(transport, serverName)
74
- return { transport, mcpTools }
75
- }
76
-
77
- /** F4/D-2:一次性探活——createConnectedTransport(initialize + tools/list)+ 计时。
78
- * 零副作用:不进 _sessions、不动 agent.tools、无 onDead 挂钩;finally close
79
- * (closed=true 使 onDead 重连不会触发)。initialize 与 tools/list 同受
80
- * INIT_TIMEOUT_MS 约束(见 doInitialize)。不复用 connectMcpServer(避免污染
81
- * session 幂等表)。 */
82
- export async function probeMcpServer(config) {
83
- const start = Date.now()
84
- let transport
85
- let mcpTools
86
- try {
87
- ;({ transport, mcpTools } = await createConnectedTransport(config, config.name ?? config.command ?? config.url ?? config.wsUrl))
88
- return { ok: true, toolCount: mcpTools.length, latencyMs: Date.now() - start }
89
- } catch (error) {
90
- return { ok: false, error: error?.message ?? String(error) }
91
- } finally {
92
- try { transport?.close() } catch { /* ignore */ }
93
- }
94
- }
95
-
96
- /** onDead → 后台退避重连;成功替换 session.state.transport(tools 闭包动态引用,
97
- * agent.tools 无需重建);失败静默(下次 execute 前置检查再试)。 */
98
- function scheduleReconnect(name, config) {
99
- if (_reconnecting.has(name)) return _reconnecting.get(name)
100
- const p = (async () => {
101
- let lastErr
102
- for (const delayMs of _mcpHooks.reconnectDelays) {
103
- await _mcpHooks.delay(delayMs)
104
- try {
105
- const { transport } = await createConnectedTransport(config, name)
106
- const session = _sessions.get(name)
107
- if (!session || session.closed) { transport.close(); return false }
108
- attachSession(session, transport)
109
- return true
110
- } catch (error) {
111
- lastErr = error
112
- }
113
- }
114
- console.error(`[mcp] ${name} reconnect failed after ${_mcpHooks.reconnectDelays.length} attempts: ${lastErr?.message ?? lastErr}`)
115
- return false
116
- })().finally(() => _reconnecting.delete(name))
117
- _reconnecting.set(name, p)
118
- return p
119
- }
120
-
121
- /** 给 session 绑定一个新 transport(建连 onDead 钩子 → 自愈链)。 */
122
- function attachSession(session, transport) {
123
- session.state.transport = transport
124
- transport.onDead?.(() => {
125
- if (session.state.transport !== transport) return // 已再替换,旧钩子作废
126
- scheduleReconnect(session.config.name ?? session.config.command ?? session.config.url ?? session.config.wsUrl, session.config)
127
- })
128
- }
129
-
130
- function buildTools(mcpTools, session, config) {
131
- const prefix = config.name ? `${config.name}_` : "mcp_"
132
- // 2026-08-31 MCP 会诊 P6:sanitize 碰撞/空名防御 + schema/description 类型守卫 + 输出防御
133
- const seen = new Set()
134
- const out = []
135
- for (const t of mcpTools) {
136
- const rawName = sanitizeToolName(prefix + t.name)
137
- if (!rawName || rawName === "mcp_") continue
138
- let name = rawName
139
- for (let n = 2; seen.has(name); n++) name = `${rawName}_${n}`
140
- seen.add(name)
141
- out.push({
142
- name,
143
- description: typeof t.description === "string" ? t.description : `MCP tool: ${t.name}`,
144
- parameters: (t.inputSchema && typeof t.inputSchema === "object") ? t.inputSchema : { type: "object", properties: {} },
145
- readonly: false,
146
- async execute(args, ctx) {
147
- // 2026-08-31 会诊 #11:上层 signal 中断 + send 第 3 参底层取消(pending 即刻作废)
148
- // P5:执行前动态取 session.transport——死亡时等重连 promise/触发一次重连再试
149
- const transport = await ensureAlive(session)
150
- const send = transport.send("tools/call", { name: t.name, arguments: args }, ctx?.signal)
151
- const resp = await sendWithSignal(send, ctx?.signal)
152
- if (resp.error) throw new Error(`MCP tool "${t.name}": ${resp.error.message}`)
153
- if (resp.result?.isError) throw new Error(`MCP tool "${t.name}": ${extractMcpText(resp.result.content) || "(server reported an error)"}`)
154
- return truncateMcpOutput(extractMcpText(resp.result?.content ?? [])) || "(no output)"
155
- },
156
- _mcpTransport: session.state.transport,
157
- _mcpName: config.name,
158
- })
159
- }
160
- return out
161
- }
162
-
163
- /** P5:拿到活的 transport——死连接等待/触发重连;耗尽后抛错(工具错误透给模型)。 */
164
- async function ensureAlive(session) {
165
- const t = session.state.transport
166
- if (t?.isAlive?.()) return t
167
- const name = session.config.name ?? session.config.command ?? session.config.url ?? session.config.wsUrl
168
- let reconnect = _reconnecting.get(name)
169
- if (!reconnect) reconnect = scheduleReconnect(name, session.config)
170
- const ok = await reconnect
171
- if (!ok || !session.state.transport?.isAlive?.()) {
172
- throw new Error(`MCP server "${name}" is unavailable (reconnect failed)`)
173
- }
174
- return session.state.transport
175
- }
176
-
177
- /** MCP content 数组 → 文本(非数组/元素非对象防御)。 */
178
- function extractMcpText(content) {
179
- if (!Array.isArray(content)) return typeof content === "string" ? content : JSON.stringify(content)
180
- return content
181
- .filter((c) => c && typeof c === "object")
182
- .map((c) => (c.type === "text" ? c.text : c.type === "resource" ? `[resource: ${c.resource?.uri}]` : JSON.stringify(c)))
183
- .join("\n")
184
- }
185
-
186
- /** 输出截断(32KB 上限,防 server 回 10MB 撑爆上下文)。 */
187
- function truncateMcpOutput(text) {
188
- if (text.length <= 32_000) return text
189
- return text.slice(0, 32_000) + "\n[… truncated: " + (text.length - 32_000) + " chars omitted]"
190
- }
191
-
192
- async function doInitialize(transport, _name) {
193
- const initResp = await withTimeout(
194
- transport.send("initialize", {
195
- protocolVersion: "2024-11-05",
196
- capabilities: {},
197
- clientInfo: { name: "thincoder", version: "1.0.0" },
198
- }),
199
- INIT_TIMEOUT_MS,
200
- )
201
- if (initResp.error) throw new Error(`initialize error: ${initResp.error.message}`)
202
- transport.notify?.("notifications/initialized", {})
203
-
204
- // 2026-08-31 MCP 会诊 P6:tools/list 分页被忽略(nextCursor 多页工具静默丢失)——
205
- // 循环跟随 cursor 直到 server 不再返回(上限 20 页防死循环)。
206
- // MCP.md §4 评审 #8:每页同受 INIT_TIMEOUT_MS 约束(否则 probe 的延迟统计无界)。
207
- const tools = []
208
- let cursor
209
- for (let page = 0; page < 20; page++) {
210
- const toolsResp = await withTimeout(
211
- transport.send("tools/list", cursor ? { cursor } : {}),
212
- INIT_TIMEOUT_MS,
213
- )
214
- if (toolsResp.error) throw new Error(`tools/list failed: ${toolsResp.error.message}`)
215
- tools.push(...(toolsResp.result?.tools ?? []))
216
- cursor = toolsResp.result?.nextCursor
217
- if (!cursor) break
218
- }
219
- return tools
220
- }
221
-
222
- /** Connect to an MCP server (stdio/http/ws), initialize, and return built tool wrappers.
223
- * 2026-08-31 MCP 会诊 P5:session 幂等——同 name 活连接复用(make-agent 每进程一次,
224
- * cmd-mcp 重复 add 同一 server 不会双实例),崩溃后下次调用/execute 自愈。 */
225
- export async function connectMcpServer(config) {
226
- if (!config || (!config.command && !config.url && !config.wsUrl))
227
- throw new Error(`MCP server "${config?.name ?? ""}": needs either 'wsUrl' (websocket), 'command' (stdio), or 'url' (http)`)
228
- const name = config.name ?? config.command ?? config.url ?? config.wsUrl
229
- // MCP.md §4 D-5:fingerprint 计入 token 字段——/mcp edit 改 token → 指纹变更 →
230
- // 旧连接主动关闭重建(T13)。
231
- const configFingerprint = JSON.stringify([config.command ?? null, config.args ?? null, config.url ?? null, config.wsUrl ?? null, config.env ?? null, config.headers ?? null, config.token ?? null])
232
-
233
- const existing = _sessions.get(name)
234
- if (existing && !existing.closed) {
235
- const sameConfig = existing.configFingerprint === configFingerprint
236
- if (sameConfig && existing.state.transport?.isAlive?.()) {
237
- return existing.state.tools
238
- }
239
- if (!sameConfig) {
240
- // config 变更:主动关闭旧连接(不触发 onDead 重连)并丢弃 session
241
- existing.closed = true
242
- try { existing.state.transport?.close() } catch { /* ignore */ }
243
- _sessions.delete(name)
244
- }
245
- }
246
-
247
- let transport
248
- let mcpTools
249
- try {
250
- ;({ transport, mcpTools } = await createConnectedTransport(config, name))
251
- } catch (error) {
252
- // MCP.md §4 评审 #7:握手失败不泄漏 transport(GET SSE 降级成功但 POST initialize
253
- // 失败时,openSSE 若开了流会留一个悬挂的 reader/请求)。
254
- try { transport?.close() } catch { /* ignore */ }
255
- throw error
256
- }
257
- const session = {
258
- config, configFingerprint,
259
- state: { transport, tools: null },
260
- closed: false,
261
- }
262
- session.state.tools = buildTools(mcpTools, session, config)
263
- attachSession(session, transport)
264
- _sessions.set(name, session)
265
- return session.state.tools
266
- }
267
-
268
- /** Close all MCP transport connections on an agent's tools */
269
- export function closeAllMcp(agent) {
270
- for (const t of agent.tools) {
271
- if (t._mcpTransport) closeSession(t._mcpName)
272
- }
273
- }
274
-
275
- /** 主动关闭 session(不触发 onDead 重连):transport close + 登记关闭标记 + 清注册表。 */
276
- function closeSession(name) {
277
- const session = _sessions.get(name)
278
- if (!session) return
279
- session.closed = true
280
- try { session.state.transport?.close() } catch { /* ignore */ }
281
- _sessions.delete(name)
282
- }
283
-
284
- /** Remove MCP tools belonging to a specific server from the agent's tool list */
285
- export function removeMcpTools(agent, serverName) {
286
- const keep = []
287
- for (const t of agent.tools) {
288
- if (t._mcpName === serverName) {
289
- closeSession(serverName)
290
- } else {
291
- keep.push(t)
292
- }
293
- }
294
- agent.tools = keep
295
- }