@kkelly-offical/kkcode 0.1.3 → 0.1.7
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 +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +220 -170
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +59 -7
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/compaction.mjs +298 -276
- package/src/session/engine.mjs +232 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1097 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +900 -1462
- package/src/session/loop.mjs +65 -40
- package/src/session/project-context.mjs +30 -0
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/plan.txt +31 -9
- package/src/session/rollback.mjs +196 -0
- package/src/session/store.mjs +519 -503
- package/src/session/system-prompt.mjs +273 -260
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/question-prompt.mjs +93 -86
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
|
@@ -6,6 +6,9 @@ const GHOST_COMMIT_DIR = "ghost-commits"
|
|
|
6
6
|
const MAX_GHOST_COMMITS_PER_REPO = 50 // 每个仓库最多保留的幽灵提交数
|
|
7
7
|
const GHOST_COMMIT_TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7天过期
|
|
8
8
|
|
|
9
|
+
// 防止并发 cleanup 竞态:per-repo 锁
|
|
10
|
+
const cleanupLocks = new Map()
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* Ghost Commit 存储管理
|
|
11
14
|
*
|
|
@@ -152,16 +155,23 @@ export async function deleteGhostCommit(repoPath, ghostCommitId) {
|
|
|
152
155
|
* @param {string} repoPath - 仓库路径
|
|
153
156
|
*/
|
|
154
157
|
export async function cleanupOldGhostCommits(repoPath) {
|
|
155
|
-
|
|
158
|
+
// 同一 repo 的 cleanup 串行化,防止并发竞态
|
|
159
|
+
if (cleanupLocks.get(repoPath)) return
|
|
160
|
+
cleanupLocks.set(repoPath, true)
|
|
161
|
+
try {
|
|
162
|
+
const commits = await listGhostCommits(repoPath, { includeExpired: true })
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
if (commits.length <= MAX_GHOST_COMMITS_PER_REPO) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
// 删除多余的旧提交
|
|
169
|
+
const toDelete = commits.slice(MAX_GHOST_COMMITS_PER_REPO)
|
|
170
|
+
for (const commit of toDelete) {
|
|
171
|
+
await deleteGhostCommit(repoPath, commit.id)
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
cleanupLocks.delete(repoPath)
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
|
package/src/tool/executor.mjs
CHANGED
|
@@ -2,6 +2,10 @@ import { makeToolResult } from "../core/types.mjs"
|
|
|
2
2
|
import { EventBus } from "../core/events.mjs"
|
|
3
3
|
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
4
4
|
import { withAudit } from "./audit-wrapper.mjs"
|
|
5
|
+
import { autoSnapshotBeforeEdit } from "../session/checkpoint.mjs"
|
|
6
|
+
|
|
7
|
+
const FILE_EDIT_TOOLS = new Set(["write", "edit", "multiedit", "patch", "notebookedit"])
|
|
8
|
+
const snapshotted = new Set()
|
|
5
9
|
|
|
6
10
|
export async function executeTool({ tool, args, sessionId, turnId, context, signal = null }) {
|
|
7
11
|
return withAudit({
|
|
@@ -44,6 +48,13 @@ export async function executeTool({ tool, args, sessionId, turnId, context, sign
|
|
|
44
48
|
return cancelled
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
// Auto snapshot before first file edit per turn
|
|
52
|
+
if (FILE_EDIT_TOOLS.has(tool.name) && !snapshotted.has(turnId)) {
|
|
53
|
+
snapshotted.add(turnId)
|
|
54
|
+
if (snapshotted.size > 200) snapshotted.clear()
|
|
55
|
+
autoSnapshotBeforeEdit(sessionId, context.cwd, context.config).catch(() => {})
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
const raw = await tool.execute(args || {}, context)
|
|
48
59
|
let output = ""
|
|
49
60
|
let metadata = {}
|
package/src/tool/git-auto.mjs
CHANGED
|
@@ -524,22 +524,3 @@ export async function getLastSnapshotId(sessionId) {
|
|
|
524
524
|
return state.lastSnapshotId
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
/**
|
|
528
|
-
* 在修改前自动创建快照(如果配置启用)
|
|
529
|
-
*/
|
|
530
|
-
export async function autoSnapshotBeforeEdit(sessionId, cwd, config = {}) {
|
|
531
|
-
if (!config.git_auto?.enabled || !config.git_auto?.auto_snapshot) {
|
|
532
|
-
return { skipped: true, reason: "auto_snapshot_disabled" }
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (!(await isGitRepo(cwd))) {
|
|
536
|
-
return { skipped: true, reason: "not_a_git_repo" }
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const result = await gitSnapshotTool.execute(
|
|
540
|
-
{ auto: true, message: "Auto snapshot before AI edit" },
|
|
541
|
-
{ cwd, sessionId }
|
|
542
|
-
)
|
|
543
|
-
|
|
544
|
-
return result
|
|
545
|
-
}
|
|
@@ -1,86 +1,93 @@
|
|
|
1
|
-
import { stdin as input, stdout as output } from "node:process"
|
|
2
|
-
import { createInterface } from "node:readline/promises"
|
|
3
|
-
|
|
4
|
-
let customPromptHandler = null
|
|
5
|
-
|
|
6
|
-
export function setQuestionPromptHandler(handler) {
|
|
7
|
-
customPromptHandler = typeof handler === "function" ? handler : null
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function askQuestionInteractive({ questions }) {
|
|
11
|
-
if (!Array.isArray(questions) || questions.length === 0) {
|
|
12
|
-
return {}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// 1. TUI handler (registered by repl.mjs)
|
|
16
|
-
if (customPromptHandler) {
|
|
17
|
-
const answers = await customPromptHandler({ questions })
|
|
18
|
-
if (answers && typeof answers === "object") return answers
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// 2. Non-TTY: return empty answers
|
|
22
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
23
|
-
return Object.fromEntries(questions.map((q) => [q.id, ""]))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 3. TTY fallback: readline sequential Q&A
|
|
27
|
-
const rl = createInterface({ input, output })
|
|
28
|
-
const answers = {}
|
|
29
|
-
try {
|
|
30
|
-
for (const q of questions) {
|
|
31
|
-
console.log("")
|
|
32
|
-
console.log(` ${q.text}`)
|
|
33
|
-
if (q.description) console.log(` ${q.description}`)
|
|
34
|
-
const options = Array.isArray(q.options) ? q.options : []
|
|
35
|
-
if (options.length) {
|
|
36
|
-
for (let i = 0; i < options.length; i++) {
|
|
37
|
-
const opt = options[i]
|
|
38
|
-
console.log(` ${i + 1}. ${opt.label}`)
|
|
39
|
-
if (opt.description) console.log(` ${opt.description}`)
|
|
40
|
-
}
|
|
41
|
-
if (q.allowCustom !== false) {
|
|
42
|
-
console.log(` ${options.length + 1}. Custom...`)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const raw = (await rl.question(" > ")).trim()
|
|
46
|
-
if (options.length) {
|
|
47
|
-
const idx = parseInt(raw, 10)
|
|
48
|
-
if (idx >= 1 && idx <= options.length) {
|
|
49
|
-
const chosen = options[idx - 1]
|
|
50
|
-
answers[q.id] = chosen.value || chosen.label
|
|
51
|
-
} else {
|
|
52
|
-
answers[q.id] = raw
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
answers[q.id] = raw
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} finally {
|
|
59
|
-
rl.close()
|
|
60
|
-
}
|
|
61
|
-
return answers
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function askPlanApproval({ plan, files = [] }) {
|
|
65
|
-
const fileList = files.length ? `\nFiles to modify:\n${files.map(f => ` - ${f}`).join("\n")}` : ""
|
|
66
|
-
const questions = [
|
|
67
|
-
{
|
|
68
|
-
id: "plan_approval",
|
|
69
|
-
text: `Plan Review`,
|
|
70
|
-
description: `${plan}${fileList}`,
|
|
71
|
-
options: [
|
|
72
|
-
{ label: "Approve", value: "approve", description: "Proceed with this plan" },
|
|
73
|
-
{ label: "
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
2
|
+
import { createInterface } from "node:readline/promises"
|
|
3
|
+
|
|
4
|
+
let customPromptHandler = null
|
|
5
|
+
|
|
6
|
+
export function setQuestionPromptHandler(handler) {
|
|
7
|
+
customPromptHandler = typeof handler === "function" ? handler : null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function askQuestionInteractive({ questions }) {
|
|
11
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
12
|
+
return {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 1. TUI handler (registered by repl.mjs)
|
|
16
|
+
if (customPromptHandler) {
|
|
17
|
+
const answers = await customPromptHandler({ questions })
|
|
18
|
+
if (answers && typeof answers === "object") return answers
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Non-TTY: return empty answers
|
|
22
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
23
|
+
return Object.fromEntries(questions.map((q) => [q.id, ""]))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 3. TTY fallback: readline sequential Q&A
|
|
27
|
+
const rl = createInterface({ input, output })
|
|
28
|
+
const answers = {}
|
|
29
|
+
try {
|
|
30
|
+
for (const q of questions) {
|
|
31
|
+
console.log("")
|
|
32
|
+
console.log(` ${q.text}`)
|
|
33
|
+
if (q.description) console.log(` ${q.description}`)
|
|
34
|
+
const options = Array.isArray(q.options) ? q.options : []
|
|
35
|
+
if (options.length) {
|
|
36
|
+
for (let i = 0; i < options.length; i++) {
|
|
37
|
+
const opt = options[i]
|
|
38
|
+
console.log(` ${i + 1}. ${opt.label}`)
|
|
39
|
+
if (opt.description) console.log(` ${opt.description}`)
|
|
40
|
+
}
|
|
41
|
+
if (q.allowCustom !== false) {
|
|
42
|
+
console.log(` ${options.length + 1}. Custom...`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const raw = (await rl.question(" > ")).trim()
|
|
46
|
+
if (options.length) {
|
|
47
|
+
const idx = parseInt(raw, 10)
|
|
48
|
+
if (idx >= 1 && idx <= options.length) {
|
|
49
|
+
const chosen = options[idx - 1]
|
|
50
|
+
answers[q.id] = chosen.value || chosen.label
|
|
51
|
+
} else {
|
|
52
|
+
answers[q.id] = raw
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
answers[q.id] = raw
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
rl.close()
|
|
60
|
+
}
|
|
61
|
+
return answers
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function askPlanApproval({ plan, files = [] }) {
|
|
65
|
+
const fileList = files.length ? `\nFiles to modify:\n${files.map(f => ` - ${f}`).join("\n")}` : ""
|
|
66
|
+
const questions = [
|
|
67
|
+
{
|
|
68
|
+
id: "plan_approval",
|
|
69
|
+
text: `Plan Review`,
|
|
70
|
+
description: `${plan}${fileList}`,
|
|
71
|
+
options: [
|
|
72
|
+
{ label: "Approve", value: "approve", description: "Proceed with this plan" },
|
|
73
|
+
{ label: "Request Changes", value: "changes", description: "Revise and resubmit with feedback" },
|
|
74
|
+
{ label: "Reject", value: "reject", description: "Cancel this plan entirely" }
|
|
75
|
+
],
|
|
76
|
+
multi: false,
|
|
77
|
+
allowCustom: true
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
const answers = await askQuestionInteractive({ questions })
|
|
81
|
+
const answer = String(answers.plan_approval || "").trim().toLowerCase()
|
|
82
|
+
if (answer === "approve" || answer === "1") {
|
|
83
|
+
return { approved: true, requestChanges: false, feedback: "" }
|
|
84
|
+
}
|
|
85
|
+
if (answer === "changes" || answer === "2") {
|
|
86
|
+
return { approved: false, requestChanges: true, feedback: "" }
|
|
87
|
+
}
|
|
88
|
+
if (answer === "reject" || answer === "3") {
|
|
89
|
+
return { approved: false, requestChanges: false, feedback: "" }
|
|
90
|
+
}
|
|
91
|
+
// Custom text input: treat as "request changes" with the text as feedback
|
|
92
|
+
return { approved: false, requestChanges: true, feedback: answer }
|
|
93
|
+
}
|
package/src/tool/registry.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path"
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises"
|
|
3
3
|
import { access, stat, unlink } from "node:fs/promises"
|
|
4
|
-
import { exec as execCb } from "node:child_process"
|
|
4
|
+
import { exec as execCb, spawn } from "node:child_process"
|
|
5
5
|
import { promisify } from "node:util"
|
|
6
6
|
import { pathToFileURL } from "node:url"
|
|
7
7
|
import { atomicWriteFile, replaceInFileTransactional, replaceAllInFileTransactional, diffLineCount } from "./edit-transaction.mjs"
|
|
@@ -23,7 +23,8 @@ const state = {
|
|
|
23
23
|
loadedAt: 0,
|
|
24
24
|
lastSignature: "",
|
|
25
25
|
lastCwd: "",
|
|
26
|
-
lastConfig: null
|
|
26
|
+
lastConfig: null,
|
|
27
|
+
refreshing: false
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function schema(type, description) {
|
|
@@ -70,16 +71,37 @@ function wasFileRead(filePath) {
|
|
|
70
71
|
return fileReadTracker.has(filePath)
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function runRg(args, cwd, timeoutMs = 30000) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let stdout = "", stderr = "", done = false
|
|
77
|
+
const child = spawn("rg", ["--no-config", ...args], {
|
|
78
|
+
cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"]
|
|
79
|
+
})
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
if (done) return
|
|
82
|
+
done = true
|
|
83
|
+
child.kill("SIGTERM")
|
|
84
|
+
setTimeout(() => { try { child.kill("SIGKILL") } catch {} }, 2000).unref()
|
|
85
|
+
resolve({ ok: false, stdout, stderr: "search timed out" })
|
|
86
|
+
}, timeoutMs)
|
|
87
|
+
child.stdout.on("data", (b) => { stdout += b })
|
|
88
|
+
child.stderr.on("data", (b) => { stderr += b })
|
|
89
|
+
child.on("error", (e) => {
|
|
90
|
+
if (done) return; done = true; clearTimeout(timer)
|
|
91
|
+
resolve({ ok: false, stdout, stderr: e.message })
|
|
92
|
+
})
|
|
93
|
+
child.on("close", (code) => {
|
|
94
|
+
if (done) return; done = true; clearTimeout(timer)
|
|
95
|
+
resolve({ ok: code === 0 || code === 1, stdout: stdout.trim(), stderr: stderr.trim() })
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
async function runGlob(pattern, cwd, searchPath) {
|
|
74
101
|
if (!pattern) return "pattern is required"
|
|
75
|
-
const escaped = pattern.replace(/"/g, '\\"')
|
|
76
102
|
const target = searchPath ? path.resolve(cwd, searchPath) : "."
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
stdout: error.stdout ?? "",
|
|
80
|
-
stderr: error.stderr ?? error.message
|
|
81
|
-
}))
|
|
82
|
-
const text = `${out.stdout || ""}`.trim()
|
|
103
|
+
const { stdout } = await runRg(["--files", "--glob", pattern, target], cwd, 15000)
|
|
104
|
+
const text = stdout.trim()
|
|
83
105
|
if (!text) return "no files matched"
|
|
84
106
|
const lines = text.split("\n").filter(Boolean)
|
|
85
107
|
if (lines.length > 200) {
|
|
@@ -90,31 +112,23 @@ async function runGlob(pattern, cwd, searchPath) {
|
|
|
90
112
|
|
|
91
113
|
async function runGrep(pattern, cwd, options = {}) {
|
|
92
114
|
if (!pattern) return "pattern is required"
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
if (options.
|
|
96
|
-
if (options.outputMode === "
|
|
97
|
-
else
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (options.
|
|
101
|
-
if (options.
|
|
102
|
-
if (options.
|
|
103
|
-
|
|
104
|
-
if (options.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
parts.push(escaped, target)
|
|
111
|
-
const command = parts.join(" ")
|
|
112
|
-
const out = await exec(command, { cwd, timeout: 30000, encoding: "utf8" }).catch((error) => ({
|
|
113
|
-
stdout: error.stdout ?? "",
|
|
114
|
-
stderr: error.stderr ?? error.message
|
|
115
|
-
}))
|
|
116
|
-
let text = `${out.stdout || ""}${out.stderr || ""}`.trim()
|
|
117
|
-
// Post-process: offset + head_limit for pagination
|
|
115
|
+
const args = []
|
|
116
|
+
if (options.multiline) args.push("-U", "--multiline-dotall")
|
|
117
|
+
if (options.outputMode === "count") args.push("-c")
|
|
118
|
+
else if (options.outputMode === "files") args.push("-l")
|
|
119
|
+
else args.push("-n")
|
|
120
|
+
if (options.beforeContext) args.push("-B", String(options.beforeContext))
|
|
121
|
+
if (options.afterContext) args.push("-A", String(options.afterContext))
|
|
122
|
+
if (options.context) args.push("-C", String(options.context))
|
|
123
|
+
if (options.type) args.push("--type", options.type)
|
|
124
|
+
if (options.glob) args.push("--glob", options.glob)
|
|
125
|
+
if (options.maxCount) args.push("-m", String(options.maxCount))
|
|
126
|
+
if (options.ignoreCase) args.push("-i")
|
|
127
|
+
args.push(pattern)
|
|
128
|
+
args.push(options.path ? path.resolve(cwd, options.path) : ".")
|
|
129
|
+
const { stdout, stderr } = await runRg(args, cwd)
|
|
130
|
+
let text = stdout.trim()
|
|
131
|
+
if (!text && stderr) text = `[search error] ${stderr}`
|
|
118
132
|
if (text && (options.offset || options.headLimit)) {
|
|
119
133
|
const lines = text.split("\n")
|
|
120
134
|
const start = options.offset || 0
|
|
@@ -566,7 +580,8 @@ function builtinTools(config) {
|
|
|
566
580
|
},
|
|
567
581
|
async execute(args, ctx) {
|
|
568
582
|
const command = String(args.command || "")
|
|
569
|
-
const
|
|
583
|
+
const configBashTimeout = Number(ctx.config?.tool?.bash_timeout_ms || BASH_TIMEOUT_MS)
|
|
584
|
+
const timeoutMs = Math.min(Math.max(Number(args.timeout) || configBashTimeout, 1000), 600_000)
|
|
570
585
|
|
|
571
586
|
// 执行策略检查
|
|
572
587
|
const policyCheck = checkBashAllowed(command, ctx.config)
|
|
@@ -1206,8 +1221,14 @@ function mcpTools() {
|
|
|
1206
1221
|
description: `[mcp:${tool.server}] ${tool.description}`,
|
|
1207
1222
|
inputSchema: tool.inputSchema,
|
|
1208
1223
|
async execute(args, ctx) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1224
|
+
try {
|
|
1225
|
+
const result = await McpRegistry.callTool(tool.id, args || {}, ctx.signal || null)
|
|
1226
|
+
return result.output
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
const reason = error.reason || "unknown"
|
|
1229
|
+
const server = error.server || tool.server
|
|
1230
|
+
return `[MCP Error: ${server} ${reason}] ${error.message}`
|
|
1231
|
+
}
|
|
1211
1232
|
}
|
|
1212
1233
|
}))
|
|
1213
1234
|
}
|
|
@@ -1305,5 +1326,18 @@ export const ToolRegistry = {
|
|
|
1305
1326
|
error: error.message
|
|
1306
1327
|
}
|
|
1307
1328
|
}
|
|
1329
|
+
},
|
|
1330
|
+
|
|
1331
|
+
refreshMcpTools() {
|
|
1332
|
+
if (!state.initialized || state.refreshing) return
|
|
1333
|
+
state.refreshing = true
|
|
1334
|
+
try {
|
|
1335
|
+
// Atomic replacement: build new list, then assign once
|
|
1336
|
+
const nonMcp = state.tools.filter((t) => !t.name.startsWith("mcp_"))
|
|
1337
|
+
const newMcpTools = mcpTools()
|
|
1338
|
+
state.tools = [...nonMcp, ...newMcpTools]
|
|
1339
|
+
} finally {
|
|
1340
|
+
state.refreshing = false
|
|
1341
|
+
}
|
|
1308
1342
|
}
|
|
1309
1343
|
}
|