@raolin2025/claude-code-node 2.0.0 → 2.1.0
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/package.json +1 -1
- package/src/channel/notify-daemon.js +16 -13
- package/src/core/cli.js +45 -5
- package/src/core/query-engine.js +57 -54
- package/src/core/session.js +3 -1
- package/src/mcp/client.js +13 -3
- package/src/security/path-guard.js +11 -1
- package/src/tools/web-fetch.js +3 -1
package/package.json
CHANGED
|
@@ -491,28 +491,31 @@ async function main() {
|
|
|
491
491
|
// 确保 socket 目录存在
|
|
492
492
|
mkdirSync(SOCK_DIR, { recursive: true });
|
|
493
493
|
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
494
|
+
// M9 fix: PID file lock — atomic create with retry, no unlink+write race
|
|
495
|
+
let pidAcquired = false;
|
|
496
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
497
|
+
try {
|
|
498
|
+
const fd = openSync(config.pidFile, "wx");
|
|
499
|
+
writeFileSync(fd, String(process.pid));
|
|
500
|
+
closeSync(fd);
|
|
501
|
+
pidAcquired = true;
|
|
502
|
+
break;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (err.code !== "EEXIST") throw err;
|
|
501
505
|
const oldPid = parseInt(readFileSync(config.pidFile, "utf8").trim(), 10);
|
|
502
506
|
try {
|
|
503
507
|
process.kill(oldPid, 0);
|
|
504
508
|
console.error("cc-notify already running (PID " + oldPid + "). Use --stop first.");
|
|
505
509
|
process.exit(1);
|
|
506
510
|
} catch {
|
|
507
|
-
try {
|
|
508
|
-
|
|
509
|
-
} catch {}
|
|
510
|
-
writeFileSync(config.pidFile, String(process.pid));
|
|
511
|
+
try { unlinkSync(config.pidFile); } catch {}
|
|
512
|
+
if (attempt < 2) await sleep(100);
|
|
511
513
|
}
|
|
512
|
-
} else {
|
|
513
|
-
throw err;
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
|
+
if (!pidAcquired) {
|
|
517
|
+
writeFileSync(config.pidFile, String(process.pid));
|
|
518
|
+
}
|
|
516
519
|
|
|
517
520
|
const cleanup = () => {
|
|
518
521
|
log("Shutting down...");
|
package/src/core/cli.js
CHANGED
|
@@ -138,6 +138,7 @@ Commands:
|
|
|
138
138
|
/channel CMD — Manage notification channels (list|send|test)
|
|
139
139
|
/cost — Show API cost report
|
|
140
140
|
/compact — Manually compact conversation context
|
|
141
|
+
/allow [tool] — Allow a tool for the current session (default: all)
|
|
141
142
|
/exit — Exit (also Ctrl+C)
|
|
142
143
|
/quit — Same as /exit
|
|
143
144
|
`
|
|
@@ -241,6 +242,8 @@ export async function main() {
|
|
|
241
242
|
session = await sessionManager.create()
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
// M1 fix: tokenBudget 必须在 engineConfig 之前定义,否则 TDZ ReferenceError
|
|
246
|
+
const tokenBudget = new TokenBudget({ maxTokens: config.get('maxBudgetTokens') || 200_000 })
|
|
244
247
|
const costTracker = new CostTracker({ model })
|
|
245
248
|
|
|
246
249
|
const engineConfig = new QueryEngineConfig({
|
|
@@ -252,11 +255,17 @@ export async function main() {
|
|
|
252
255
|
})
|
|
253
256
|
const engine = new QueryEngine(engineConfig)
|
|
254
257
|
|
|
255
|
-
// M5: 恢复会话历史和状态
|
|
258
|
+
// M5: 恢复会话历史和状态 — 完整恢复所有角色(含 tool_calls、tool 结果)
|
|
256
259
|
if (session?.messages?.length) {
|
|
257
260
|
for (const msg of session.messages) {
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
const entry = { role: msg.role, content: msg.content }
|
|
262
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
263
|
+
entry.toolCalls = msg.toolCalls
|
|
264
|
+
}
|
|
265
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
266
|
+
entry.tool_call_id = msg.tool_call_id
|
|
267
|
+
}
|
|
268
|
+
engine.state.messages.push(entry)
|
|
260
269
|
}
|
|
261
270
|
// 恢复 turn count
|
|
262
271
|
if (session.state?.turnCount) engine.state.turnCount = session.state.turnCount
|
|
@@ -268,7 +277,6 @@ export async function main() {
|
|
|
268
277
|
}
|
|
269
278
|
}
|
|
270
279
|
|
|
271
|
-
const tokenBudget = new TokenBudget({ maxTokens: config.get('maxBudgetTokens') || 200_000 })
|
|
272
280
|
|
|
273
281
|
const channelManager = new ChannelManager({
|
|
274
282
|
channels: config.get('channels') || {},
|
|
@@ -277,8 +285,16 @@ export async function main() {
|
|
|
277
285
|
|
|
278
286
|
// 一次性输入模式
|
|
279
287
|
if (cliArgs.oneShot) {
|
|
288
|
+
// 一次性模式下用户已明确表达了执行意图,自动批准所有工具调用
|
|
289
|
+
if (engine.permissionChecker.mode === 'ask') {
|
|
290
|
+
engine.config.onConfirmTool = async () => true
|
|
291
|
+
}
|
|
280
292
|
const result = await engine.processMessage(cliArgs.oneShot)
|
|
281
293
|
console.log(result.response)
|
|
294
|
+
// 保存会话
|
|
295
|
+
session = await sessionManager.create(`one-shot: ${cliArgs.oneShot.slice(0, 50)}`)
|
|
296
|
+
await sessionManager.appendMessage({ role: 'user', content: cliArgs.oneShot })
|
|
297
|
+
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
282
298
|
if (channelManager.list().length > 0) {
|
|
283
299
|
await channelManager.sendTemplate('task-done', {
|
|
284
300
|
task: cliArgs.oneShot.slice(0, 80),
|
|
@@ -293,6 +309,19 @@ export async function main() {
|
|
|
293
309
|
|
|
294
310
|
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
|
|
295
311
|
|
|
312
|
+
// 将 readline 注入引擎配置,用于 ask 模式确认和 AskUserQuestion 工具
|
|
313
|
+
if (permissionMode === 'ask') {
|
|
314
|
+
engine.config.onConfirmTool = async (toolName, input) => {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
const snippet = JSON.stringify(input).slice(0, 120) || '(no params)'
|
|
317
|
+
rl.question(`\n⚠️ Allow tool "${toolName}"?\n Input: ${snippet}\n (y/N) `, (answer) => {
|
|
318
|
+
resolve(answer.toLowerCase().startsWith('y'))
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
engine.config.readline = rl
|
|
324
|
+
|
|
296
325
|
console.log(BANNER)
|
|
297
326
|
console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
|
|
298
327
|
console.log(`Socket: ${SOCK_PATH} (cc-notify can connect)`)
|
|
@@ -304,7 +333,7 @@ export async function main() {
|
|
|
304
333
|
console.log()
|
|
305
334
|
rl.prompt()
|
|
306
335
|
|
|
307
|
-
//
|
|
336
|
+
// REPL 消息处理包装(留作扩展点)
|
|
308
337
|
async function processInput(input) {
|
|
309
338
|
return engine.processMessage(input)
|
|
310
339
|
}
|
|
@@ -380,6 +409,12 @@ export async function main() {
|
|
|
380
409
|
}
|
|
381
410
|
break
|
|
382
411
|
}
|
|
412
|
+
case 'allow': {
|
|
413
|
+
const allowTool = rest.join(' ') || '*'
|
|
414
|
+
engine.permissionChecker.allowForSession(allowTool, '*')
|
|
415
|
+
console.log(`✅ Tool "${allowTool}" allowed for this session`)
|
|
416
|
+
break
|
|
417
|
+
}
|
|
383
418
|
case 'cost':
|
|
384
419
|
console.log(engine.costTracker.formatReport())
|
|
385
420
|
break
|
|
@@ -415,6 +450,11 @@ export async function main() {
|
|
|
415
450
|
console.log()
|
|
416
451
|
await sessionManager.appendMessage({ role: 'user', content: input })
|
|
417
452
|
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
453
|
+
// 保存引擎状态到会话
|
|
454
|
+
session.state = session.state || {}
|
|
455
|
+
session.state.turnCount = engine.state.turnCount
|
|
456
|
+
session.state.costHistory = engine.costTracker.history.slice(-50)
|
|
457
|
+
await sessionManager.save(session)
|
|
418
458
|
if (verbose) console.log(`[Turns: ${result.turns} | Tools: ${result.toolResults.length}]`)
|
|
419
459
|
// 显示费用(即使非 verbose 也显示)
|
|
420
460
|
if (engine.costTracker && engine.costTracker.totalApiCalls > 0) {
|
package/src/core/query-engine.js
CHANGED
|
@@ -43,6 +43,8 @@ export class QueryEngineConfig {
|
|
|
43
43
|
this.costTracker = options.costTracker || null
|
|
44
44
|
this.tokenBudget = options.tokenBudget || null
|
|
45
45
|
this.initialMessages = options.initialMessages || []
|
|
46
|
+
this.onConfirmTool = options.onConfirmTool || null // ask 模式确认回调
|
|
47
|
+
this.readline = options.readline || null // 用于 AskUserQuestion 工具
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -196,39 +198,61 @@ export class QueryEngine {
|
|
|
196
198
|
|
|
197
199
|
|
|
198
200
|
/**
|
|
199
|
-
* 执行工具调用
|
|
201
|
+
* 执行工具调用 — 两阶段策略
|
|
202
|
+
* 阶段1(串行):安全检查 + ask 模式确认(需要用户交互,必须串行)
|
|
203
|
+
* 阶段2(并行):批准后的工具并行执行,互不依赖的工具同时跑
|
|
200
204
|
*/
|
|
201
205
|
async _executeToolCalls(toolCalls) {
|
|
202
|
-
|
|
206
|
+
// 阶段1:串行安全检查
|
|
207
|
+
const approved = []
|
|
203
208
|
for (const tc of toolCalls) {
|
|
204
|
-
// 安全检查
|
|
205
209
|
const permResult = await this.permissionChecker.check(tc.name, tc.input)
|
|
206
210
|
if (!permResult.allowed) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
if (permResult.requiresConfirmation && this.config.onConfirmTool) {
|
|
212
|
+
const confirmed = await this.config.onConfirmTool(tc.name, tc.input)
|
|
213
|
+
if (!confirmed) {
|
|
214
|
+
approved.push({ tc, error: '用户未确认' })
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
approved.push({ tc, error: `安全策略拒绝: ${permResult.reason || ""}` })
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
210
221
|
}
|
|
211
222
|
|
|
212
|
-
// 查找工具
|
|
213
223
|
const tool = this.config.tools.find(t => t.name === tc.name)
|
|
214
224
|
if (!tool) {
|
|
215
|
-
|
|
216
|
-
results[results.length - 1].toolName = tc.name
|
|
225
|
+
approved.push({ tc, error: `未找到工具: ${tc.name}` })
|
|
217
226
|
continue
|
|
218
227
|
}
|
|
219
228
|
|
|
229
|
+
approved.push({ tc, tool })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 阶段2:并行执行已批准的工具
|
|
233
|
+
const execPromises = approved.map(async (item) => {
|
|
234
|
+
if (item.error) {
|
|
235
|
+
const r = new ToolResult(item.tc.id, item.error, true)
|
|
236
|
+
r.toolName = item.tc.name
|
|
237
|
+
return r
|
|
238
|
+
}
|
|
239
|
+
const { tc, tool } = item
|
|
240
|
+
tc.status = 'running'
|
|
220
241
|
try {
|
|
221
|
-
tc.
|
|
222
|
-
const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this })
|
|
242
|
+
const content = await tool.handler(tc.input, { cwd: this.config.cwd, engine: this, readline: this.config.readline })
|
|
223
243
|
tc.status = 'done'
|
|
224
|
-
|
|
225
|
-
|
|
244
|
+
const r = new ToolResult(tc.id, typeof content === 'string' ? content : JSON.stringify(content), false)
|
|
245
|
+
r.toolName = tc.name
|
|
246
|
+
return r
|
|
226
247
|
} catch (err) {
|
|
227
248
|
tc.status = 'error'
|
|
228
|
-
|
|
229
|
-
|
|
249
|
+
const r = new ToolResult(tc.id, `工具执行错误: ${err.message}`, true)
|
|
250
|
+
r.toolName = tc.name
|
|
251
|
+
return r
|
|
230
252
|
}
|
|
231
|
-
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const results = await Promise.all(execPromises)
|
|
232
256
|
return results
|
|
233
257
|
}
|
|
234
258
|
|
|
@@ -289,6 +313,12 @@ export class QueryEngine {
|
|
|
289
313
|
|
|
290
314
|
// 带重试的 fetch
|
|
291
315
|
const maxRetries = 3
|
|
316
|
+
// Jitter 退避 — 指数退避 + 随机 ±50%,防止惊群效应
|
|
317
|
+
const retryDelay = (baseMs, attempt) => {
|
|
318
|
+
const ms = baseMs * Math.pow(2, attempt - 1)
|
|
319
|
+
const jitter = ms * (0.5 + Math.random() * 0.5) // 50%-100% of base
|
|
320
|
+
return Math.round(jitter)
|
|
321
|
+
}
|
|
292
322
|
let lastError = null
|
|
293
323
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
294
324
|
try {
|
|
@@ -310,7 +340,7 @@ export class QueryEngine {
|
|
|
310
340
|
const errText = await response.text()
|
|
311
341
|
// 429/503 可重试
|
|
312
342
|
if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
|
|
313
|
-
const waitMs = response.status === 429 ? 2000
|
|
343
|
+
const waitMs = retryDelay(response.status === 429 ? 2000 : 1000, attempt)
|
|
314
344
|
if (this.config.verbose) {
|
|
315
345
|
console.error(`[retry] API ${response.status}, waiting ${waitMs}ms (attempt ${attempt}/${maxRetries})`)
|
|
316
346
|
}
|
|
@@ -322,11 +352,17 @@ export class QueryEngine {
|
|
|
322
352
|
|
|
323
353
|
// 流式或非流式处理
|
|
324
354
|
if (useStream && response.body) {
|
|
325
|
-
|
|
355
|
+
const result = await this._handleStreamResponse(response)
|
|
356
|
+
if (result.usage && this.costTracker) {
|
|
357
|
+
this.costTracker.recordUsage(result.usage)
|
|
358
|
+
}
|
|
359
|
+
if (this.tokenBudget && result.usage) {
|
|
360
|
+
this.tokenBudget.recordUsage(result.usage)
|
|
361
|
+
}
|
|
362
|
+
return result
|
|
326
363
|
} else {
|
|
327
364
|
const data = await response.json()
|
|
328
365
|
const result = parseNonStreamResponse(data)
|
|
329
|
-
// M4: 记录非流式响应费用
|
|
330
366
|
if (result.usage && this.costTracker) {
|
|
331
367
|
this.costTracker.recordUsage(result.usage)
|
|
332
368
|
}
|
|
@@ -339,7 +375,7 @@ export class QueryEngine {
|
|
|
339
375
|
lastError = err
|
|
340
376
|
// 网络错误重试
|
|
341
377
|
if (err.name !== 'AbortError' && attempt < maxRetries && !err.message.startsWith('API 错误')) {
|
|
342
|
-
const waitMs = 1000
|
|
378
|
+
const waitMs = retryDelay(1000, attempt)
|
|
343
379
|
if (this.config.verbose) {
|
|
344
380
|
console.error(`[retry] Network error: ${err.message}, waiting ${waitMs}ms (attempt ${attempt}/${maxRetries})`)
|
|
345
381
|
}
|
|
@@ -394,42 +430,9 @@ export class QueryEngine {
|
|
|
394
430
|
return result
|
|
395
431
|
}
|
|
396
432
|
|
|
397
|
-
/**
|
|
398
|
-
* 解析 OpenAI 兼容响应
|
|
399
|
-
*/
|
|
400
|
-
_parseResponse(data) {
|
|
401
|
-
const result = { content: '', toolCalls: [] }
|
|
402
|
-
const choice = data.choices?.[0]
|
|
403
|
-
if (!choice) return result
|
|
404
|
-
|
|
405
|
-
const message = choice.message
|
|
406
|
-
if (message.content) {
|
|
407
|
-
result.content = message.content
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
for (const tc of (message.tool_calls || [])) {
|
|
411
|
-
let input = {}
|
|
412
|
-
try {
|
|
413
|
-
input = JSON.parse(tc.function.arguments || '{}')
|
|
414
|
-
} catch {
|
|
415
|
-
input = { _raw: tc.function.arguments }
|
|
416
|
-
}
|
|
417
|
-
result.toolCalls.push(new ToolCall(tc.id, tc.function.name, input))
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// M4: 记录 API 调用费用
|
|
421
|
-
if (result.usage && this.costTracker) {
|
|
422
|
-
this.costTracker.recordUsage(result.usage)
|
|
423
|
-
}
|
|
424
|
-
if (this.tokenBudget && result.usage) {
|
|
425
|
-
this.tokenBudget.recordUsage(result.usage)
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return result
|
|
429
|
-
}
|
|
430
|
-
|
|
431
433
|
/** 格式化内容 */
|
|
432
434
|
_formatContent(content) {
|
|
435
|
+
if (content == null) return ''
|
|
433
436
|
if (typeof content === 'string') return content
|
|
434
437
|
if (typeof content === 'object') return JSON.stringify(content)
|
|
435
438
|
return String(content)
|
package/src/core/session.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFile, writeFile, mkdir, readdir, rm, chmod } from 'fs/promises'
|
|
6
6
|
import { resolve, join } from 'path'
|
|
7
|
+
import { randomBytes } from 'crypto'
|
|
7
8
|
|
|
8
9
|
const DEFAULT_SESSIONS_DIR = '.claude-code/sessions'
|
|
9
10
|
|
|
@@ -21,7 +22,8 @@ export class SessionManager {
|
|
|
21
22
|
/** 创建新会话 */
|
|
22
23
|
async create(title = '') {
|
|
23
24
|
await this.ensureDir()
|
|
24
|
-
|
|
25
|
+
// M4 fix: 使用 crypto.randomBytes 生成不可预测的会话 ID
|
|
26
|
+
const id = `session-${Date.now()}-${randomBytes(8).toString('hex')}`
|
|
25
27
|
const session = {
|
|
26
28
|
id,
|
|
27
29
|
title: title || `Session ${new Date().toISOString().slice(0, 19)}`,
|
package/src/mcp/client.js
CHANGED
|
@@ -212,7 +212,7 @@ export class MCPClient {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/** 发送 JSON-RPC 请求 */
|
|
215
|
-
_sendRequest(method, params) {
|
|
215
|
+
_sendRequest(method, params, timeoutMs = 30000) {
|
|
216
216
|
return new Promise((resolve, reject) => {
|
|
217
217
|
const id = nextId()
|
|
218
218
|
const message = JSON.stringify({
|
|
@@ -222,11 +222,19 @@ export class MCPClient {
|
|
|
222
222
|
params,
|
|
223
223
|
})
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
// M8 fix: 请求超时保护,防止 MCP 服务器无响应时永远挂起
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
if (this.pending.has(id)) {
|
|
228
|
+
this.pending.delete(id)
|
|
229
|
+
reject(new Error(`MCP request timeout: ${method} (${timeoutMs}ms)`))
|
|
230
|
+
}
|
|
231
|
+
}, timeoutMs)
|
|
232
|
+
this.pending.set(id, { resolve, reject, timer })
|
|
226
233
|
|
|
227
234
|
// 每条消息以换行符分隔
|
|
228
235
|
this.process.stdin.write(message + '\n', (err) => {
|
|
229
236
|
if (err) {
|
|
237
|
+
clearTimeout(timer)
|
|
230
238
|
this.pending.delete(id)
|
|
231
239
|
reject(new Error(`Failed to send message: ${err.message}`))
|
|
232
240
|
}
|
|
@@ -255,8 +263,10 @@ export class MCPClient {
|
|
|
255
263
|
const message = JSON.parse(line)
|
|
256
264
|
|
|
257
265
|
if (message.id && this.pending.has(message.id)) {
|
|
258
|
-
const { resolve, reject } = this.pending.get(message.id)
|
|
266
|
+
const { resolve, reject, timer } = this.pending.get(message.id)
|
|
259
267
|
this.pending.delete(message.id)
|
|
268
|
+
// M8 fix: 清除超时定时器
|
|
269
|
+
if (timer) clearTimeout(timer)
|
|
260
270
|
|
|
261
271
|
if (message.error) {
|
|
262
272
|
reject(new Error(message.error.message || 'MCP error'))
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - 双重编码绕过检测(%252e 等)
|
|
9
9
|
*/
|
|
10
10
|
import { resolve, normalize, isAbsolute, relative, sep } from 'path'
|
|
11
|
+
import { realpathSync } from 'fs'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 敏感路径列表 — 禁止读写
|
|
@@ -53,7 +54,16 @@ export function sanitizePath(filePath, cwd = process.cwd()) {
|
|
|
53
54
|
// 如果是相对路径,基于 cwd 解析
|
|
54
55
|
const absPath = isAbsolute(decoded) ? decoded : resolve(cwd, decoded)
|
|
55
56
|
// 规范化:消除 .. 和 .
|
|
56
|
-
|
|
57
|
+
const normalized = normalize(absPath)
|
|
58
|
+
|
|
59
|
+
// M6 fix: 解析符号链接,防止通过 symlink 绕过路径安全检查
|
|
60
|
+
// 例如: /tmp/link → /etc/shadow
|
|
61
|
+
try {
|
|
62
|
+
return realpathSync(normalized)
|
|
63
|
+
} catch {
|
|
64
|
+
// 文件不存在时 realpathSync 会抛错,返回规范化路径即可
|
|
65
|
+
return normalized
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
package/src/tools/web-fetch.js
CHANGED
|
@@ -35,6 +35,8 @@ function htmlToText(html) {
|
|
|
35
35
|
return text
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const VERSION = '2.0.0'
|
|
39
|
+
|
|
38
40
|
export const webFetchTool = new ToolDef(
|
|
39
41
|
'WebFetch',
|
|
40
42
|
`Fetch and extract content from a URL.
|
|
@@ -69,7 +71,7 @@ Usage:
|
|
|
69
71
|
try {
|
|
70
72
|
const response = await fetch(url, {
|
|
71
73
|
headers: {
|
|
72
|
-
'User-Agent':
|
|
74
|
+
'User-Agent': `ClaudeCode-Node/${VERSION}`,
|
|
73
75
|
'Accept': 'text/html,application/json,text/plain,*/*',
|
|
74
76
|
},
|
|
75
77
|
signal: AbortSignal.timeout(30000),
|