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
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
- }
@@ -1,219 +0,0 @@
1
- /**
2
- * memory/code-index.mjs — code and document chunking, language detection, symbol extraction
3
- */
4
-
5
- import { segmentCJK, CODE_EXTS, DOC_EXTS, SKIP_DIRS, BIG_FILE_LINES } from "./schema.mjs"
6
-
7
- /** Infer file language by extension */
8
- export function detectLanguage(filename) {
9
- const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase()
10
- const map = {
11
- ".mjs": "javascript", ".js": "javascript", ".cjs": "javascript", ".jsx": "jsx",
12
- ".ts": "typescript", ".tsx": "tsx", ".mts": "typescript", ".cts": "typescript",
13
- ".py": "python", ".rs": "rust", ".go": "go", ".java": "java",
14
- ".c": "c", ".h": "c", ".cpp": "cpp", ".hpp": "cpp",
15
- ".rb": "ruby", ".swift": "swift", ".kt": "kotlin", ".dart": "dart", ".lua": "lua",
16
- ".cs": "csharp", ".fs": "fsharp", ".fsx": "fsharp",
17
- ".clj": "clojure", ".cljs": "clojure", ".ex": "elixir", ".exs": "elixir",
18
- ".erl": "erlang", ".hrl": "erlang", ".scala": "scala", ".groovy": "groovy",
19
- ".pl": "perl", ".pm": "perl", ".r": "r", ".jl": "julia", ".zig": "zig",
20
- ".ps1": "powershell", ".proto": "protobuf", ".graphql": "graphql", ".tf": "terraform", ".hcl": "hcl",
21
- ".sh": "bash", ".bash": "bash", ".sql": "sql",
22
- ".yaml": "yaml", ".yml": "yaml", ".toml": "toml", ".json": "json",
23
- ".css": "css", ".html": "html", ".vue": "vue", ".svelte": "svelte",
24
- ".md": "markdown", ".mdc": "markdown", ".mdx": "markdown", ".org": "org", ".wiki": "wiki", ".tex": "tex",
25
- }
26
- return map[ext] ?? ext.slice(1)
27
- }
28
-
29
- /**
30
- * Extract top-level symbol declarations (functions, classes, const exports, etc.)
31
- * from JS/TS files using regex. Returns [{ name, line, kind }].
32
- */
33
- export function extractSymbols(lines, ext) {
34
- const jsish = new Set([".mjs", ".js", ".ts", ".jsx", ".tsx"])
35
- if (!jsish.has(ext)) return []
36
-
37
- const symbols = []
38
- const text = lines.join("\n")
39
- const re = /(?:export\s+)?(?:(?:async\s+)?function\s+(\w+)|class\s+(\w+)|(?:export\s+)?(?:const|let|var)\s+(\w+))/gm
40
- let m
41
- while ((m = re.exec(text))) {
42
- const name = m[1] || m[2] || m[3]
43
- if (!name || name[0] !== name[0].toLowerCase() && name.length < 2) continue
44
- const line = text.slice(0, m.index).split("\n").length
45
- symbols.push({ name, line, kind: m[1] ? "function" : m[2] ? "class" : "variable" })
46
- }
47
- return symbols
48
- }
49
-
50
- /** Extract top-level def/class from Python files. */
51
- export function extractPySymbols(lines) {
52
- const symbols = []
53
- const re = /^(?:async\s+)?(?:def|class)\s+(\w+)/gm
54
- const text = lines.join("\n")
55
- let m
56
- while ((m = re.exec(text))) {
57
- symbols.push({ name: m[1], line: text.slice(0, m.index).split("\n").length, kind: text[m.index] === "c" ? "class" : "function" })
58
- }
59
- return symbols
60
- }
61
-
62
- /**
63
- * Split a file into code chunks. Small files become a single chunk;
64
- * large files are split by symbol, with inter-symbol content merged into the preceding symbol chunk.
65
- * Each chunk includes the JSDoc/docstring before its symbol to improve search quality.
66
- */
67
- export function chunkCode(lines, filepath) {
68
- const ext = filepath.slice(filepath.lastIndexOf(".")).toLowerCase()
69
- const chunks = []
70
-
71
- if (lines.length <= BIG_FILE_LINES) {
72
- const doc = extractLeadingDoc(lines, 1, ext)
73
- const content = (doc ? doc + "\n" : "") + lines.join("\n").trimEnd()
74
- chunks.push({ name: filepath, line_start: 1, line_end: lines.length, content })
75
- return chunks
76
- }
77
-
78
- const symbols = ext === ".py" ? extractPySymbols(lines) : extractSymbols(lines, ext)
79
- if (symbols.length <= 1) {
80
- const doc = extractLeadingDoc(lines, 1, ext)
81
- const content = (doc ? doc + "\n" : "") + lines.join("\n").trimEnd()
82
- chunks.push({ name: filepath, line_start: 1, line_end: lines.length, content })
83
- return chunks
84
- }
85
-
86
- for (let i = 0; i < symbols.length; i++) {
87
- const sym = symbols[i]
88
- const start = sym.line
89
- const end = i + 1 < symbols.length ? symbols[i + 1].line - 1 : lines.length
90
- if (start > end) continue
91
- const doc = extractLeadingDoc(lines, start, ext)
92
- const body = lines.slice(start - 1, end).join("\n").trimEnd()
93
- const content = (doc ? doc + "\n" : "") + body
94
- if (!content) continue
95
- chunks.push({ name: `${filepath}:${sym.name}`, line_start: start, line_end: end, content })
96
- }
97
- return chunks
98
- }
99
-
100
- /**
101
- * Extract the JSDoc/docstring comment preceding a given line.
102
- * JS/TS: scan backwards for JSDoc block comments or consecutive // comment lines
103
- * Python: look for a """...""" docstring on the line after the symbol definition
104
- */
105
- export function extractLeadingDoc(lines, lineNum, ext) {
106
- if (ext === ".py") {
107
- if (lineNum >= lines.length) return ""
108
- const next = lines[lineNum]
109
- const m = next?.match(/^\s*"""(.+?)"""\s*$/)
110
- if (m) return m[1].trim()
111
- if (/^\s*"""\s*$/.test(next)) {
112
- const parts = []
113
- for (let i = lineNum + 1; i < lines.length && i < lineNum + 8; i++) {
114
- if (/^\s*"""\s*$/.test(lines[i])) break
115
- parts.push(lines[i].trim())
116
- }
117
- const text = parts.join(" ").trim()
118
- return text.length > 0 && text.length < 300 ? text : ""
119
- }
120
- return ""
121
- }
122
-
123
- const jsish = new Set([".mjs", ".js", ".ts", ".jsx", ".tsx"])
124
- if (!jsish.has(ext)) return ""
125
-
126
- const parts = []
127
- let i = lineNum - 2
128
- if (i >= 0 && /^\s*\*\/\s*$/.test(lines[i])) {
129
- while (i >= 0) {
130
- const line = lines[i].trim()
131
- if (/^\s*\/\*\*/.test(line)) {
132
- parts.unshift(line.replace(/^\s*\/\*\*\s*/, "").replace(/\s*\*\/\s*$/, "").trim())
133
- break
134
- }
135
- parts.unshift(line.replace(/^\s*\*\s?/, "").trim())
136
- i--
137
- }
138
- } else {
139
- while (i >= 0 && /^\s*\/\//.test(lines[i])) {
140
- parts.unshift(lines[i].replace(/^\s*\/\/\s*/, "").trim())
141
- i--
142
- }
143
- }
144
-
145
- const text = parts.join(" ").trim()
146
- return text.length > 0 && text.length < 300 ? text : ""
147
- }
148
-
149
- /** Yield control to the event loop for one tick (allows keyboard input to be processed). Uses setImmediate for lower latency than setTimeout(0). */
150
- export function yieldTick() {
151
- return new Promise((r) => setImmediate(r))
152
- }
153
-
154
- /** Index a single file: delete old chunks → chunk → insert new chunks */
155
- export function _upsertCodeFile(memory, origin, rel, lines, lang, mtimeMs) {
156
- const chunks = chunkCode(lines, rel)
157
- memory.db.exec("BEGIN")
158
- try {
159
- memory.db.prepare(`DELETE FROM code_chunks WHERE origin = ? AND path = ?`).run(origin, rel)
160
- const insert = memory.db.prepare(`
161
- INSERT INTO code_chunks (origin, path, language, chunk_type, symbol_name, content, line_start, line_end, mtime_ms, seg_content)
162
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
163
- `)
164
- for (const c of chunks) {
165
- const isFile = c.name === rel
166
- insert.run(origin, rel, lang, isFile ? "file" : "symbol", isFile ? "" : c.name.slice(rel.length + 1), c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
167
- }
168
- memory.db.exec("COMMIT")
169
- } catch (e) {
170
- memory.db.exec("ROLLBACK")
171
- throw e
172
- }
173
- }
174
-
175
- /**
176
- * Split a markdown file by ## headings. Each ## section is indexed independently,
177
- * with the heading path as the heading label (e.g. "README.md > Deployment > Docker") for easy retrieval.
178
- */
179
- export function chunkMarkdown(lines, filepath) {
180
- const chunks = []
181
- let start = 1
182
- let heading = filepath
183
-
184
- for (let i = 0; i < lines.length; i++) {
185
- const m = lines[i].match(/^(#{1,4})\s+(.+)/)
186
- if (m) {
187
- if (i > start) {
188
- chunks.push({ heading, line_start: start, line_end: i, content: lines.slice(start - 1, i).join("\n").trimEnd() })
189
- }
190
- heading = `${filepath} > ${m[2].trim()}`
191
- start = i + 1
192
- }
193
- }
194
- if (start <= lines.length) {
195
- chunks.push({ heading, line_start: start, line_end: lines.length, content: lines.slice(start - 1).join("\n").trimEnd() })
196
- }
197
- return chunks.filter((c) => c.content)
198
- }
199
-
200
- /** Upsert a documentation file's chunks into the doc_chunks table within a transaction */
201
- export function _upsertDocFile(memory, origin, rel, lines, mtimeMs) {
202
- const chunks = chunkMarkdown(lines, rel)
203
- const lang = rel.endsWith(".rst") ? "rst" : rel.endsWith(".adoc") ? "asciidoc" : rel.endsWith(".txt") ? "text" : "markdown"
204
- memory.db.exec("BEGIN")
205
- try {
206
- memory.db.prepare(`DELETE FROM doc_chunks WHERE origin = ? AND path = ?`).run(origin, rel)
207
- const insert = memory.db.prepare(`
208
- INSERT INTO doc_chunks (origin, path, language, heading, content, line_start, line_end, mtime_ms, seg_content)
209
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
210
- `)
211
- for (const c of chunks) {
212
- insert.run(origin, rel, lang, c.heading, c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
213
- }
214
- memory.db.exec("COMMIT")
215
- } catch (e) {
216
- memory.db.exec("ROLLBACK")
217
- throw e
218
- }
219
- }