thincoder 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/thincoder.mjs +25 -1
- package/package.json +1 -1
- package/src/SYSTEM_PROMPT.md +1 -0
- package/src/agent.mjs +97 -62
- package/src/checkpoint.mjs +6 -3
- package/src/config.mjs +10 -4
- package/src/context.mjs +37 -8
- package/src/distill.mjs +6 -2
- package/src/embedding.mjs +11 -2
- package/src/gitmem.mjs +6 -2
- package/src/markdown.mjs +11 -4
- package/src/mcp.mjs +118 -36
- package/src/memory.mjs +214 -74
- package/src/provider.mjs +15 -11
- package/src/repomap.mjs +17 -6
- package/src/session.mjs +8 -5
- package/src/skills.mjs +6 -2
- package/src/tools/apply_patch.md +11 -0
- package/src/tools/checkpoint.md +11 -0
- package/src/tools.mjs +311 -25
- package/src/tui.mjs +44 -15
package/src/gitmem.mjs
CHANGED
|
@@ -67,7 +67,9 @@ export async function pullTeam(dir) {
|
|
|
67
67
|
*/
|
|
68
68
|
export async function commitAndPush(dir, filename, message) {
|
|
69
69
|
await git(dir, ["add", filename])
|
|
70
|
-
|
|
70
|
+
// 内容没变化时 commit 会以 exit 1 报 "nothing to commit"——这是正常的幂等结果,不是错误
|
|
71
|
+
const dirty = await git(dir, ["status", "--porcelain", "--", filename])
|
|
72
|
+
if (dirty) await git(dir, ["commit", "-m", message])
|
|
71
73
|
try {
|
|
72
74
|
await git(dir, ["push"])
|
|
73
75
|
} catch {
|
|
@@ -80,7 +82,9 @@ export async function commitAndPush(dir, filename, message) {
|
|
|
80
82
|
async function hasConflict(dir) {
|
|
81
83
|
try {
|
|
82
84
|
const out = await git(dir, ["status", "--porcelain"])
|
|
83
|
-
|
|
85
|
+
// 未合并状态共 7 种:DD AU UD UA DU AA UU——只看 UU/AA/DD 会漏掉带 U 的四种,
|
|
86
|
+
// 漏判就不 abort,仓库留在冲突中间态(与"保持仓库干净"的承诺相悖)
|
|
87
|
+
return out.split("\n").some((l) => l[0] === "U" || l[1] === "U" || l.startsWith("AA") || l.startsWith("DD"))
|
|
84
88
|
} catch {
|
|
85
89
|
return false
|
|
86
90
|
}
|
package/src/markdown.mjs
CHANGED
|
@@ -41,14 +41,16 @@ export function parseEntry(text) {
|
|
|
41
41
|
export function serializeEntry(meta, content) {
|
|
42
42
|
if (!VALID_TYPES.has(meta.type)) throw new Error(`invalid type "${meta.type}"`)
|
|
43
43
|
if (!meta.title) throw new Error("meta.title is required")
|
|
44
|
-
|
|
44
|
+
// frontmatter 标量必须单行:title/author 含换行会注入伪 frontmatter 行
|
|
45
|
+
// (如 title "x\ntype: rule" 解析时覆盖真实 type),tags 含换行/逗号同理
|
|
46
|
+
const tags = (meta.tags ?? []).map((t) => oneLine(t).replaceAll(",", " ")).join(", ")
|
|
45
47
|
const lines = [
|
|
46
48
|
"---",
|
|
47
49
|
`type: ${meta.type}`,
|
|
48
|
-
`title: ${meta.title}`,
|
|
50
|
+
`title: ${oneLine(meta.title)}`,
|
|
49
51
|
`tags: [${tags}]`,
|
|
50
|
-
`author: ${meta.author ?? "unknown"}`,
|
|
51
|
-
`created: ${meta.created ?? new Date().toISOString().slice(0, 10)}`,
|
|
52
|
+
`author: ${oneLine(meta.author ?? "unknown")}`,
|
|
53
|
+
`created: ${oneLine(meta.created ?? new Date().toISOString().slice(0, 10))}`,
|
|
52
54
|
]
|
|
53
55
|
if (meta.embedding) lines.push(`embedding: ${meta.embedding}`)
|
|
54
56
|
lines.push("---", "", content.trim(), "")
|
|
@@ -74,6 +76,11 @@ export function entryFilename(title, date = new Date()) {
|
|
|
74
76
|
|
|
75
77
|
// ---------------------------------------------------------------- 内部
|
|
76
78
|
|
|
79
|
+
/** 压成单行(frontmatter 标量用):换行折叠为空格,防注入伪字段行 */
|
|
80
|
+
function oneLine(v) {
|
|
81
|
+
return String(v).replace(/\s*\r?\n\s*/g, " ").trim()
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
/**
|
|
78
85
|
* 极简 YAML 子集解析:只支持 `key: value` 和 `key: [a, b, c]`。
|
|
79
86
|
* 我们的 frontmatter 是自己生成的,不需要完整 YAML。
|
package/src/mcp.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { spawn } from "node:child_process"
|
|
|
8
8
|
|
|
9
9
|
const INIT_TIMEOUT_MS = 30_000
|
|
10
10
|
const CALL_TIMEOUT_MS = 120_000
|
|
11
|
+
// 等 legacy SSE 首个 endpoint 事件的上限:legacy server 连接后立即发,实际几毫秒内到达
|
|
12
|
+
const ENDPOINT_WAIT_MS = 5_000
|
|
11
13
|
|
|
12
14
|
// ---- JSON-RPC helpers ----
|
|
13
15
|
|
|
@@ -32,6 +34,7 @@ function stdioTransport(command, args) {
|
|
|
32
34
|
: spawn(command, args ?? [], spawnOptions)
|
|
33
35
|
|
|
34
36
|
const pending = new Map()
|
|
37
|
+
const decoder = new TextDecoder() // 单一实例:跨 chunk 保留多字节 UTF-8 的中间状态
|
|
35
38
|
let buffer = ""
|
|
36
39
|
let stderrTail = "" // 诊断用:server 起不来时给用户一点线索
|
|
37
40
|
let spawnError = null
|
|
@@ -43,7 +46,8 @@ function stdioTransport(command, args) {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
child.stdout.on("data", (chunk) => {
|
|
46
|
-
|
|
49
|
+
// stream:true:多字节字符跨 chunk 拆分时暂存残片,等下一个 chunk 拼完整
|
|
50
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true })
|
|
47
51
|
const lines = buffer.split("\n")
|
|
48
52
|
buffer = lines.pop() ?? ""
|
|
49
53
|
for (const line of lines) {
|
|
@@ -65,6 +69,9 @@ function stdioTransport(command, args) {
|
|
|
65
69
|
stderrTail = (stderrTail + chunk.toString()).slice(-2000)
|
|
66
70
|
})
|
|
67
71
|
|
|
72
|
+
// stdin 写错误(EPIPE 等):没有这个监听,error 事件会崩掉整个进程;close 事件统一兜底
|
|
73
|
+
child.stdin.on("error", () => {})
|
|
74
|
+
|
|
68
75
|
// spawn 失败(命令不存在/EINVAL):没有这个监听,error 事件会崩掉整个进程
|
|
69
76
|
child.on("error", (error) => {
|
|
70
77
|
spawnError = error
|
|
@@ -83,13 +90,21 @@ function stdioTransport(command, args) {
|
|
|
83
90
|
if (closed) return Promise.reject(new Error("MCP connection closed"))
|
|
84
91
|
const id = rpcId()
|
|
85
92
|
const promise = new Promise((resolve) => pending.set(id, resolve))
|
|
86
|
-
|
|
93
|
+
try {
|
|
94
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n")
|
|
95
|
+
} catch (error) {
|
|
96
|
+
pending.delete(id)
|
|
97
|
+
return Promise.resolve({ id: null, error: { code: -32000, message: `stdin write failed: ${error.message}` } })
|
|
98
|
+
}
|
|
87
99
|
return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
// notification:无 id,不期待响应(协议要求)
|
|
91
103
|
const notify = (method, params) => {
|
|
92
|
-
if (
|
|
104
|
+
if (closed) return
|
|
105
|
+
try {
|
|
106
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n")
|
|
107
|
+
} catch { /* 忽略:close 事件会兜底 */ }
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
return { send, notify, close: () => { if (!closed) child.kill() } }
|
|
@@ -103,6 +118,12 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
103
118
|
let closed = false
|
|
104
119
|
let eventSource = null
|
|
105
120
|
let abortController = null
|
|
121
|
+
// legacy SSE (2024-11-05):POST 地址由 server 的 endpoint 事件告知;
|
|
122
|
+
// Streamable HTTP (2025-03-26):POST 到配置的 URL 本身
|
|
123
|
+
let postUrl = url
|
|
124
|
+
// 收到 endpoint 事件才置真:legacy SSE (2024-11-05) 模式,POST 只回 202,响应经 SSE 流推回;
|
|
125
|
+
// 否则按 Streamable HTTP (2025-03-26):响应就在 POST 自身(即使 server 同时支持 GET 推送)
|
|
126
|
+
let legacySSE = false
|
|
106
127
|
|
|
107
128
|
const headers = () => {
|
|
108
129
|
const h = { "Content-Type": "application/json", Accept: "text/event-stream, application/json", ...extraHeaders }
|
|
@@ -112,7 +133,7 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
112
133
|
|
|
113
134
|
const pending = new Map()
|
|
114
135
|
|
|
115
|
-
// SSE 解析器:从 response body 逐行读,处理 data: / event: /
|
|
136
|
+
// SSE 解析器:从 response body 逐行读,处理 data: / event: / 空行(dispatch)
|
|
116
137
|
async function* parseSSE(response) {
|
|
117
138
|
const reader = response.body.getReader()
|
|
118
139
|
const decoder = new TextDecoder()
|
|
@@ -144,24 +165,34 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
144
165
|
}
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
// 打开 SSE
|
|
168
|
+
// 打开 SSE 长连接(legacy SSE transport,用于接收服务端推送)
|
|
169
|
+
// 按 2024-11-05 规范:GET 配置的 URL 本身,server 的第一个 endpoint 事件告知 POST 地址
|
|
148
170
|
async function openSSE() {
|
|
149
171
|
if (closed) return
|
|
150
172
|
abortController?.abort()
|
|
151
173
|
abortController = new AbortController()
|
|
152
|
-
const resp = await fetch(url
|
|
174
|
+
const resp = await fetch(url, {
|
|
153
175
|
method: "GET",
|
|
154
|
-
headers: { Accept: "text/event-stream" },
|
|
176
|
+
headers: { Accept: "text/event-stream", ...extraHeaders },
|
|
155
177
|
signal: abortController.signal,
|
|
156
178
|
})
|
|
157
179
|
if (!resp.ok) throw new Error(`SSE connect failed: HTTP ${resp.status}`)
|
|
158
180
|
eventSource = parseSSE(resp)
|
|
181
|
+
let endpointReady
|
|
182
|
+
const gotEndpoint = new Promise((resolve) => { endpointReady = resolve })
|
|
159
183
|
|
|
160
184
|
// 后台消费 SSE 事件并分发到 pending
|
|
161
185
|
;(async () => {
|
|
162
186
|
try {
|
|
163
|
-
for await (const { data } of eventSource) {
|
|
187
|
+
for await (const { event, data } of eventSource) {
|
|
164
188
|
if (closed) break
|
|
189
|
+
if (event === "endpoint") {
|
|
190
|
+
// data 是相对/绝对 URI,拼到配置的 URL 上作为 POST 地址
|
|
191
|
+
postUrl = new URL(data.trim(), url).href
|
|
192
|
+
legacySSE = true
|
|
193
|
+
endpointReady()
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
165
196
|
try {
|
|
166
197
|
const msg = JSON.parse(data)
|
|
167
198
|
const resolver = pending.get(msg.id)
|
|
@@ -175,9 +206,18 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
175
206
|
} catch (error) {
|
|
176
207
|
if (!closed) {
|
|
177
208
|
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: `SSE error: ${error.message}` } })
|
|
209
|
+
pending.clear()
|
|
178
210
|
}
|
|
179
211
|
}
|
|
180
212
|
})()
|
|
213
|
+
|
|
214
|
+
// 等 endpoint 事件再发请求(2024-11-05 要求拿到 POST 地址后才能 POST);
|
|
215
|
+
// 超时说明不是 legacy server——Streamable HTTP 的 GET 流只推服务端消息,响应走 POST 自身
|
|
216
|
+
const wait = new Promise((resolve) => {
|
|
217
|
+
const t = setTimeout(resolve, ENDPOINT_WAIT_MS)
|
|
218
|
+
t.unref?.()
|
|
219
|
+
})
|
|
220
|
+
await Promise.race([gotEndpoint, wait])
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
// POST JSON-RPC 请求,同时监听响应
|
|
@@ -185,20 +225,28 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
185
225
|
const id = rpcId()
|
|
186
226
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
187
227
|
|
|
188
|
-
//
|
|
189
|
-
if (
|
|
228
|
+
// legacy SSE:POST 只回 202,服务器经 SSE 流推回响应
|
|
229
|
+
if (legacySSE) {
|
|
190
230
|
return new Promise((resolve) => {
|
|
191
231
|
pending.set(id, resolve)
|
|
192
|
-
fetch(
|
|
232
|
+
fetch(postUrl, { method: "POST", headers: headers(), body, signal: AbortSignal.timeout(CALL_TIMEOUT_MS) })
|
|
233
|
+
.then((resp) => {
|
|
234
|
+
// 2024-11-05:POST 期望 202 Accepted;其他错误码说明请求没送达
|
|
235
|
+
if (!resp.ok) {
|
|
236
|
+
pending.delete(id)
|
|
237
|
+
resolve({ id, error: { code: -32000, message: `POST failed: HTTP ${resp.status}` } })
|
|
238
|
+
}
|
|
239
|
+
})
|
|
193
240
|
.catch((e) => {
|
|
194
241
|
pending.delete(id)
|
|
195
242
|
resolve({ id, error: { code: -32000, message: `POST failed: ${e.message}` } })
|
|
196
243
|
})
|
|
197
|
-
|
|
244
|
+
// 兜底清理:响应超时(send 外层 withTimeout 先赢)时 pending 不留尸
|
|
245
|
+
}).finally(() => pending.delete(id))
|
|
198
246
|
}
|
|
199
247
|
|
|
200
|
-
//
|
|
201
|
-
const resp = await fetch(
|
|
248
|
+
// Streamable HTTP(无 SSE,或 GET 流只推服务端消息):响应就在 POST 自身
|
|
249
|
+
const resp = await fetch(postUrl, {
|
|
202
250
|
method: "POST",
|
|
203
251
|
headers: headers(),
|
|
204
252
|
body,
|
|
@@ -231,7 +279,7 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
231
279
|
|
|
232
280
|
// notification:无 id,不期待响应(协议要求)
|
|
233
281
|
const notify = (method, params) => {
|
|
234
|
-
fetch(
|
|
282
|
+
fetch(postUrl, {
|
|
235
283
|
method: "POST",
|
|
236
284
|
headers: headers(),
|
|
237
285
|
body: JSON.stringify({ jsonrpc: "2.0", method, params }),
|
|
@@ -242,6 +290,15 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
242
290
|
const close = () => {
|
|
243
291
|
closed = true
|
|
244
292
|
abortController?.abort()
|
|
293
|
+
// Streamable HTTP 规范:有 session 时发 DELETE 让 server 释放会话(尽力而为)
|
|
294
|
+
if (sessionId) {
|
|
295
|
+
fetch(postUrl, {
|
|
296
|
+
method: "DELETE",
|
|
297
|
+
headers: { "Mcp-Session-Id": sessionId, ...extraHeaders },
|
|
298
|
+
signal: AbortSignal.timeout(5_000),
|
|
299
|
+
}).catch(() => {})
|
|
300
|
+
sessionId = null
|
|
301
|
+
}
|
|
245
302
|
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message: "Connection closed" } })
|
|
246
303
|
pending.clear()
|
|
247
304
|
}
|
|
@@ -263,10 +320,9 @@ function wsTransport(wsUrl, extraHeaders = {}) {
|
|
|
263
320
|
|
|
264
321
|
const connect = () => {
|
|
265
322
|
if (closed) throw new Error("MCP WebSocket connection closed")
|
|
266
|
-
// WebSocket API 不支持自定义 header
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
: new WebSocket(wsUrl)
|
|
323
|
+
// WebSocket API 不支持自定义 header,token 走 query param
|
|
324
|
+
// (不能把 "Bearer ..." 当子协议传——含空格,违反 Sec-WebSocket-Protocol token 规则会抛 SyntaxError)
|
|
325
|
+
ws = new WebSocket(withAuthToken(wsUrl, extraHeaders.Authorization))
|
|
270
326
|
|
|
271
327
|
return new Promise((resolve, reject) => {
|
|
272
328
|
const timeout = setTimeout(() => {
|
|
@@ -274,27 +330,27 @@ function wsTransport(wsUrl, extraHeaders = {}) {
|
|
|
274
330
|
reject(new Error(`WebSocket connect timeout: ${wsUrl}`))
|
|
275
331
|
}, INIT_TIMEOUT_MS)
|
|
276
332
|
|
|
277
|
-
ws.
|
|
333
|
+
ws.addEventListener("open", () => {
|
|
278
334
|
clearTimeout(timeout)
|
|
279
335
|
resolve()
|
|
280
336
|
})
|
|
281
337
|
|
|
282
|
-
ws.
|
|
338
|
+
ws.addEventListener("message", (event) => {
|
|
283
339
|
try {
|
|
284
|
-
const msg = JSON.parse(data.toString())
|
|
340
|
+
const msg = JSON.parse(event.data.toString())
|
|
285
341
|
const resolver = pending.get(msg.id)
|
|
286
342
|
if (resolver) {
|
|
287
343
|
pending.delete(msg.id)
|
|
288
344
|
resolver(msg)
|
|
289
345
|
}
|
|
290
|
-
// 没有
|
|
346
|
+
// 没有 resolver 的是通知,忽略
|
|
291
347
|
} catch { /* 非 JSON,忽略 */ }
|
|
292
348
|
})
|
|
293
349
|
|
|
294
|
-
ws.
|
|
350
|
+
ws.addEventListener("error", (event) => {
|
|
295
351
|
clearTimeout(timeout)
|
|
296
352
|
closed = true
|
|
297
|
-
const errMsg =
|
|
353
|
+
const errMsg = event.message || "WebSocket error"
|
|
298
354
|
if (pending.size > 0) {
|
|
299
355
|
failAll(errMsg)
|
|
300
356
|
} else {
|
|
@@ -302,7 +358,7 @@ function wsTransport(wsUrl, extraHeaders = {}) {
|
|
|
302
358
|
}
|
|
303
359
|
})
|
|
304
360
|
|
|
305
|
-
ws.
|
|
361
|
+
ws.addEventListener("close", () => {
|
|
306
362
|
clearTimeout(timeout)
|
|
307
363
|
closed = true
|
|
308
364
|
failAll("WebSocket closed")
|
|
@@ -314,7 +370,13 @@ function wsTransport(wsUrl, extraHeaders = {}) {
|
|
|
314
370
|
if (closed) return Promise.reject(new Error("MCP WebSocket connection closed"))
|
|
315
371
|
const id = rpcId()
|
|
316
372
|
const promise = new Promise((resolve) => pending.set(id, resolve))
|
|
317
|
-
|
|
373
|
+
try {
|
|
374
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }))
|
|
375
|
+
} catch (error) {
|
|
376
|
+
// 非 OPEN 状态 send 会同步抛;close 事件随后统一兜底
|
|
377
|
+
pending.delete(id)
|
|
378
|
+
return Promise.resolve({ id: null, error: { code: -32000, message: `ws send failed: ${error.message}` } })
|
|
379
|
+
}
|
|
318
380
|
return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
|
|
319
381
|
}
|
|
320
382
|
|
|
@@ -338,7 +400,8 @@ function wsTransport(wsUrl, extraHeaders = {}) {
|
|
|
338
400
|
function buildTools(mcpTools, transport, config) {
|
|
339
401
|
const prefix = config.name ? `${config.name}_` : "mcp_"
|
|
340
402
|
return mcpTools.map((t) => ({
|
|
341
|
-
|
|
403
|
+
// 组合名整体 sanitize + 截断:prefix 也要计入 64 字符上限
|
|
404
|
+
name: sanitizeToolName(prefix + t.name),
|
|
342
405
|
description: t.description ?? `MCP tool: ${t.name}`,
|
|
343
406
|
parameters: t.inputSchema ?? { type: "object", properties: {} },
|
|
344
407
|
readonly: false,
|
|
@@ -380,9 +443,14 @@ async function doInitialize(transport, name) {
|
|
|
380
443
|
export async function connectMcpServer(config) {
|
|
381
444
|
if (config.wsUrl) {
|
|
382
445
|
const transport = wsTransport(config.wsUrl, config.headers ?? {})
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
446
|
+
try {
|
|
447
|
+
await transport.connect()
|
|
448
|
+
const mcpTools = await doInitialize(transport, config.name ?? config.wsUrl)
|
|
449
|
+
return buildTools(mcpTools, transport, config)
|
|
450
|
+
} catch (error) {
|
|
451
|
+
transport.close()
|
|
452
|
+
throw error
|
|
453
|
+
}
|
|
386
454
|
}
|
|
387
455
|
|
|
388
456
|
if (config.url) {
|
|
@@ -390,10 +458,15 @@ export async function connectMcpServer(config) {
|
|
|
390
458
|
try {
|
|
391
459
|
await transport.openSSE()
|
|
392
460
|
} catch {
|
|
393
|
-
// 不支持 GET
|
|
461
|
+
// 不支持 GET 的 server(纯 Streamable HTTP POST):降级为无 SSE 模式
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const mcpTools = await doInitialize(transport, config.name ?? config.url)
|
|
465
|
+
return buildTools(mcpTools, transport, config)
|
|
466
|
+
} catch (error) {
|
|
467
|
+
transport.close()
|
|
468
|
+
throw error
|
|
394
469
|
}
|
|
395
|
-
const mcpTools = await doInitialize(transport, config.name ?? config.url)
|
|
396
|
-
return buildTools(mcpTools, transport, config)
|
|
397
470
|
}
|
|
398
471
|
|
|
399
472
|
if (config.command) {
|
|
@@ -430,13 +503,22 @@ export function removeMcpTools(agent, serverName) {
|
|
|
430
503
|
|
|
431
504
|
// ---- helpers ----
|
|
432
505
|
|
|
506
|
+
/** 把 Authorization header 转成 ?token= query param(WebSocket 无法自定义 header) */
|
|
507
|
+
function withAuthToken(wsUrl, authorization) {
|
|
508
|
+
if (!authorization) return wsUrl
|
|
509
|
+
const token = authorization.replace(/^Bearer\s+/i, "")
|
|
510
|
+
const u = new URL(wsUrl)
|
|
511
|
+
u.searchParams.set("token", token)
|
|
512
|
+
return u.href
|
|
513
|
+
}
|
|
514
|
+
|
|
433
515
|
function sanitizeToolName(name) {
|
|
434
516
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64)
|
|
435
517
|
}
|
|
436
518
|
|
|
437
|
-
/** cmd.exe
|
|
519
|
+
/** cmd.exe 参数加引号(含空格/引号时)。cmd 不认 \" 转义——内层引号必须翻倍 */
|
|
438
520
|
function quoteArg(s) {
|
|
439
|
-
return /[\s"]/.test(s) ? `"${s.replace(/"/g, '
|
|
521
|
+
return /[\s"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
440
522
|
}
|
|
441
523
|
|
|
442
524
|
function withTimeout(promise, ms) {
|