@raolin2025/claude-code-node 1.0.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/README.md +294 -0
- package/package.json +24 -0
- package/src/core/cli.js +314 -0
- package/src/core/config.js +131 -0
- package/src/core/index.js +9 -0
- package/src/core/query-engine.js +344 -0
- package/src/core/session.js +120 -0
- package/src/core/streaming.js +119 -0
- package/src/core/token-budget.js +88 -0
- package/src/index.js +13 -0
- package/src/mcp/client.js +214 -0
- package/src/mcp/index.js +5 -0
- package/src/mcp/registry.js +176 -0
- package/src/permission/permission.js +37 -0
- package/src/security/bash-guard.js +279 -0
- package/src/security/enhanced-permission.js +310 -0
- package/src/security/index.js +7 -0
- package/src/security/path-guard.js +190 -0
- package/src/security/ssrf-guard.js +178 -0
- package/src/tools/ask-user.js +34 -0
- package/src/tools/bash.js +101 -0
- package/src/tools/file-edit.js +112 -0
- package/src/tools/file-read.js +105 -0
- package/src/tools/file-write.js +57 -0
- package/src/tools/glob.js +113 -0
- package/src/tools/grep.js +117 -0
- package/src/tools/index.js +110 -0
- package/src/tools/web-fetch.js +125 -0
- package/src/tools/web-search.js +75 -0
- package/src/types/index.js +126 -0
- package/src/utils/diff.js +181 -0
- package/src/utils/file-ops.js +124 -0
- package/src/utils/format.js +130 -0
- package/src/utils/index.js +7 -0
- package/src/utils/process.js +112 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ====== 消息类型 ======
|
|
2
|
+
|
|
3
|
+
/** 消息角色 */
|
|
4
|
+
export const Role = {
|
|
5
|
+
USER: 'user',
|
|
6
|
+
ASSISTANT: 'assistant',
|
|
7
|
+
SYSTEM: 'system',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** 基础消息 */
|
|
11
|
+
export class Message {
|
|
12
|
+
role
|
|
13
|
+
content
|
|
14
|
+
timestamp
|
|
15
|
+
id
|
|
16
|
+
|
|
17
|
+
constructor(role, content) {
|
|
18
|
+
this.role = role
|
|
19
|
+
this.content = content
|
|
20
|
+
this.timestamp = Date.now()
|
|
21
|
+
this.id = crypto.randomUUID()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class UserMessage extends Message {
|
|
26
|
+
constructor(content) {
|
|
27
|
+
super(Role.USER, content)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AssistantMessage extends Message {
|
|
32
|
+
toolCalls
|
|
33
|
+
constructor(content, toolCalls = []) {
|
|
34
|
+
super(Role.ASSISTANT, content)
|
|
35
|
+
this.toolCalls = toolCalls
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class SystemMessage extends Message {
|
|
40
|
+
constructor(content) {
|
|
41
|
+
super(Role.SYSTEM, content)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ====== 工具调用类型 ======
|
|
46
|
+
|
|
47
|
+
export class ToolCall {
|
|
48
|
+
id
|
|
49
|
+
name
|
|
50
|
+
input
|
|
51
|
+
status // 'pending' | 'running' | 'done' | 'error'
|
|
52
|
+
|
|
53
|
+
constructor(id, name, input) {
|
|
54
|
+
this.id = id
|
|
55
|
+
this.name = name
|
|
56
|
+
this.input = input
|
|
57
|
+
this.status = 'pending'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ToolResult {
|
|
62
|
+
toolCallId
|
|
63
|
+
content
|
|
64
|
+
isError
|
|
65
|
+
|
|
66
|
+
constructor(toolCallId, content, isError = false) {
|
|
67
|
+
this.toolCallId = toolCallId
|
|
68
|
+
this.content = content
|
|
69
|
+
this.isError = isError
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ====== 工具定义 ======
|
|
74
|
+
|
|
75
|
+
export class ToolDef {
|
|
76
|
+
name
|
|
77
|
+
description
|
|
78
|
+
parameters // JSON Schema
|
|
79
|
+
handler
|
|
80
|
+
permissionLevel // 'always-allow' | 'ask' | 'deny'
|
|
81
|
+
|
|
82
|
+
constructor(name, description, parameters, handler, permissionLevel = 'ask') {
|
|
83
|
+
this.name = name
|
|
84
|
+
this.description = description
|
|
85
|
+
this.parameters = parameters
|
|
86
|
+
this.handler = handler
|
|
87
|
+
this.permissionLevel = permissionLevel
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ====== Agent 类型 ======
|
|
92
|
+
|
|
93
|
+
export class AgentDef {
|
|
94
|
+
id
|
|
95
|
+
name
|
|
96
|
+
systemPrompt
|
|
97
|
+
tools
|
|
98
|
+
model
|
|
99
|
+
|
|
100
|
+
constructor(id, name, systemPrompt, tools = [], model = 'default') {
|
|
101
|
+
this.id = id
|
|
102
|
+
this.name = name
|
|
103
|
+
this.systemPrompt = systemPrompt
|
|
104
|
+
this.tools = tools
|
|
105
|
+
this.model = model
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ====== 会话状态 ======
|
|
110
|
+
|
|
111
|
+
export class SessionState {
|
|
112
|
+
messages
|
|
113
|
+
toolResults
|
|
114
|
+
turnCount
|
|
115
|
+
budgetUsed // token 预算
|
|
116
|
+
isRunning
|
|
117
|
+
|
|
118
|
+
constructor() {
|
|
119
|
+
this.messages = []
|
|
120
|
+
this.toolResults = new Map()
|
|
121
|
+
this.turnCount = 0
|
|
122
|
+
this.budgetUsed = 0
|
|
123
|
+
this.isRunning = false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本差异计算 — 简单 LCS 算法
|
|
3
|
+
* 对应原版: src/utils/diff.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 计算两个文本之间的 unified diff
|
|
8
|
+
*/
|
|
9
|
+
export function unifiedDiff(oldText, newText, filePath = 'file') {
|
|
10
|
+
const oldLines = oldText.split('\n')
|
|
11
|
+
const newLines = newText.split('\n')
|
|
12
|
+
const hunks = computeHunks(oldLines, newLines)
|
|
13
|
+
|
|
14
|
+
if (hunks.length === 0) return ''
|
|
15
|
+
|
|
16
|
+
const header = `--- a/${filePath}\n+++ b/${filePath}\n`
|
|
17
|
+
const body = hunks.map(h => formatHunk(h, oldLines, newLines)).join('\n')
|
|
18
|
+
|
|
19
|
+
return header + body
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 计算差异区域
|
|
24
|
+
*/
|
|
25
|
+
function computeHunks(oldLines, newLines) {
|
|
26
|
+
const lcs = computeLCS(oldLines, newLines)
|
|
27
|
+
const edits = extractEdits(oldLines, newLines, lcs)
|
|
28
|
+
|
|
29
|
+
// 合并相邻的编辑区域
|
|
30
|
+
const hunks = []
|
|
31
|
+
let currentHunk = null
|
|
32
|
+
|
|
33
|
+
for (const edit of edits) {
|
|
34
|
+
if (!currentHunk || edit.oldStart - currentHunk.oldEnd > 3) {
|
|
35
|
+
if (currentHunk) hunks.push(currentHunk)
|
|
36
|
+
currentHunk = {
|
|
37
|
+
oldStart: Math.max(0, edit.oldStart - 3),
|
|
38
|
+
oldEnd: edit.oldEnd + 3,
|
|
39
|
+
newStart: Math.max(0, edit.newStart - 3),
|
|
40
|
+
newEnd: edit.newEnd + 3,
|
|
41
|
+
edits: [edit],
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
currentHunk.oldEnd = edit.oldEnd + 3
|
|
45
|
+
currentHunk.newEnd = edit.newEnd + 3
|
|
46
|
+
currentHunk.edits.push(edit)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (currentHunk) hunks.push(currentHunk)
|
|
50
|
+
|
|
51
|
+
return hunks
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 简单 LCS 动态规划
|
|
56
|
+
*/
|
|
57
|
+
function computeLCS(a, b) {
|
|
58
|
+
const m = a.length
|
|
59
|
+
const n = b.length
|
|
60
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))
|
|
61
|
+
|
|
62
|
+
for (let i = 1; i <= m; i++) {
|
|
63
|
+
for (let j = 1; j <= n; j++) {
|
|
64
|
+
if (a[i - 1] === b[j - 1]) {
|
|
65
|
+
dp[i][j] = dp[i - 1][j - 1] + 1
|
|
66
|
+
} else {
|
|
67
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 回溯
|
|
73
|
+
const result = []
|
|
74
|
+
let i = m, j = n
|
|
75
|
+
while (i > 0 && j > 0) {
|
|
76
|
+
if (a[i - 1] === b[j - 1]) {
|
|
77
|
+
result.unshift({ type: 'equal', oldIdx: i - 1, newIdx: j - 1 })
|
|
78
|
+
i--; j--
|
|
79
|
+
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
|
|
80
|
+
i--
|
|
81
|
+
} else {
|
|
82
|
+
j--
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 从 LCS 提取编辑操作
|
|
91
|
+
*/
|
|
92
|
+
function extractEdits(oldLines, newLines, lcs) {
|
|
93
|
+
const edits = []
|
|
94
|
+
let oldIdx = 0, newIdx = 0
|
|
95
|
+
|
|
96
|
+
for (const match of lcs) {
|
|
97
|
+
if (match.oldIdx > oldIdx || match.newIdx > newIdx) {
|
|
98
|
+
edits.push({
|
|
99
|
+
oldStart: oldIdx,
|
|
100
|
+
oldEnd: match.oldIdx,
|
|
101
|
+
newStart: newIdx,
|
|
102
|
+
newEnd: match.newIdx,
|
|
103
|
+
type: 'change',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
oldIdx = match.oldIdx + 1
|
|
107
|
+
newIdx = match.newIdx + 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 末尾
|
|
111
|
+
if (oldIdx < oldLines.length || newIdx < newLines.length) {
|
|
112
|
+
edits.push({
|
|
113
|
+
oldStart: oldIdx,
|
|
114
|
+
oldEnd: oldLines.length,
|
|
115
|
+
newStart: newIdx,
|
|
116
|
+
newEnd: newLines.length,
|
|
117
|
+
type: 'change',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return edits
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 格式化一个 hunk
|
|
126
|
+
*/
|
|
127
|
+
function formatHunk(hunk, oldLines, newLines) {
|
|
128
|
+
const oldStart = hunk.oldStart + 1
|
|
129
|
+
const oldCount = Math.min(hunk.oldEnd, oldLines.length) - hunk.oldStart
|
|
130
|
+
const newStart = hunk.newStart + 1
|
|
131
|
+
const newCount = Math.min(hunk.newEnd, newLines.length) - hunk.newStart
|
|
132
|
+
|
|
133
|
+
let output = `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@\n`
|
|
134
|
+
|
|
135
|
+
for (let i = hunk.oldStart; i < Math.min(hunk.oldEnd, oldLines.length); i++) {
|
|
136
|
+
const isEdit = hunk.edits.some(e => i >= e.oldStart && i < e.oldEnd)
|
|
137
|
+
output += isEdit ? `-${oldLines[i]}\n` : ` ${oldLines[i]}\n`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (let i = hunk.newStart; i < Math.min(hunk.newEnd, newLines.length); i++) {
|
|
141
|
+
const isEdit = hunk.edits.some(e => i >= e.newStart && i < e.newEnd)
|
|
142
|
+
if (isEdit) output += `+${newLines[i]}\n`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return output
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 简单的字符级 diff(用于行内差异)
|
|
150
|
+
*/
|
|
151
|
+
export function inlineDiff(oldStr, newStr) {
|
|
152
|
+
if (oldStr === newStr) return oldStr
|
|
153
|
+
|
|
154
|
+
const commonPrefix = commonPrefixLength(oldStr, newStr)
|
|
155
|
+
const commonSuffix = commonSuffixLength(
|
|
156
|
+
oldStr.slice(commonPrefix),
|
|
157
|
+
newStr.slice(commonPrefix)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const removed = oldStr.slice(commonPrefix, oldStr.length - commonSuffix)
|
|
161
|
+
const added = newStr.slice(commonPrefix, newStr.length - commonSuffix)
|
|
162
|
+
|
|
163
|
+
let result = oldStr.slice(0, commonPrefix)
|
|
164
|
+
if (removed) result += `[-${removed}-]`
|
|
165
|
+
if (added) result += `{+${added}+}`
|
|
166
|
+
result += oldStr.slice(oldStr.length - commonSuffix)
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function commonPrefixLength(a, b) {
|
|
172
|
+
let i = 0
|
|
173
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++
|
|
174
|
+
return i
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function commonSuffixLength(a, b) {
|
|
178
|
+
let i = 0
|
|
179
|
+
while (i < a.length && i < b.length && a[a.length - 1 - i] === b[b.length - 1 - i]) i++
|
|
180
|
+
return i
|
|
181
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件操作工具
|
|
3
|
+
* 对应原版: src/utils/file.ts + src/utils/fsOperations.ts
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile, stat, mkdir, rm, rename, copyFile } from 'fs/promises'
|
|
6
|
+
import { resolve, dirname, basename, isAbsolute } from 'path'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 安全读取文件(带大小限制)
|
|
11
|
+
*/
|
|
12
|
+
export async function safeReadFile(filePath, options = {}) {
|
|
13
|
+
const maxBytes = options.maxBytes || 256 * 1024 // 256KB
|
|
14
|
+
const absPath = resolvePath(filePath, options.cwd)
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const fileStat = await stat(absPath)
|
|
18
|
+
if (fileStat.size > maxBytes) {
|
|
19
|
+
return { ok: false, error: `File too large: ${fileStat.size} bytes (limit: ${maxBytes})` }
|
|
20
|
+
}
|
|
21
|
+
const content = await readFile(absPath, 'utf-8')
|
|
22
|
+
return { ok: true, content, size: fileStat.size, mtime: fileStat.mtimeMs }
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return { ok: false, error: err.message, code: err.code }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 安全写入文件(自动创建目录)
|
|
30
|
+
*/
|
|
31
|
+
export async function safeWriteFile(filePath, content, options = {}) {
|
|
32
|
+
const absPath = resolvePath(filePath, options.cwd)
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(dirname(absPath), { recursive: true })
|
|
36
|
+
await writeFile(absPath, content, 'utf-8')
|
|
37
|
+
return { ok: true, path: absPath, size: Buffer.byteLength(content, 'utf-8') }
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { ok: false, error: err.message }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 精确文本替换编辑
|
|
45
|
+
*/
|
|
46
|
+
export async function editFile(filePath, edits, options = {}) {
|
|
47
|
+
const absPath = resolvePath(filePath, options.cwd)
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const content = await readFile(absPath, 'utf-8')
|
|
51
|
+
let newContent = content
|
|
52
|
+
|
|
53
|
+
for (const edit of Array.isArray(edits) ? edits : [edits]) {
|
|
54
|
+
const { oldText, newText, replaceAll = false } = edit
|
|
55
|
+
|
|
56
|
+
if (!newContent.includes(oldText)) {
|
|
57
|
+
return { ok: false, error: `oldText not found in ${absPath}` }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const count = newContent.split(oldText).length - 1
|
|
61
|
+
if (count > 1 && !replaceAll) {
|
|
62
|
+
return { ok: false, error: `oldText appears ${count} times. Use replaceAll=true or provide more context.` }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (replaceAll) {
|
|
66
|
+
newContent = newContent.split(oldText).join(newText)
|
|
67
|
+
} else {
|
|
68
|
+
const idx = newContent.indexOf(oldText)
|
|
69
|
+
newContent = newContent.slice(0, idx) + newText + newContent.slice(idx + oldText.length)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await writeFile(absPath, newContent, 'utf-8')
|
|
74
|
+
return { ok: true, path: absPath }
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { ok: false, error: err.message }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 检查文件/目录是否存在
|
|
82
|
+
*/
|
|
83
|
+
export async function pathExists(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
await stat(filePath)
|
|
86
|
+
return true
|
|
87
|
+
} catch {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 递归删除
|
|
94
|
+
*/
|
|
95
|
+
export async function removeRecursive(filePath) {
|
|
96
|
+
try {
|
|
97
|
+
await rm(filePath, { recursive: true, force: true })
|
|
98
|
+
return { ok: true }
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { ok: false, error: err.message }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 获取文件修改时间
|
|
106
|
+
*/
|
|
107
|
+
export async function getMtime(filePath) {
|
|
108
|
+
try {
|
|
109
|
+
const s = await stat(filePath)
|
|
110
|
+
return s.mtimeMs
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 解析路径(支持相对路径)
|
|
118
|
+
*/
|
|
119
|
+
function resolvePath(filePath, cwd) {
|
|
120
|
+
if (isAbsolute(filePath)) return filePath
|
|
121
|
+
return resolve(cwd || process.cwd(), filePath)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { resolvePath as resolvePath }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 输出格式化工具
|
|
3
|
+
* 对应原版: src/utils/format.ts + src/outputStyles/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ANSI 颜色码
|
|
8
|
+
*/
|
|
9
|
+
const ANSI = {
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
bold: '\x1b[1m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
red: '\x1b[31m',
|
|
14
|
+
green: '\x1b[32m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
blue: '\x1b[34m',
|
|
17
|
+
magenta: '\x1b[35m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
gray: '\x1b[90m',
|
|
20
|
+
bgRed: '\x1b[41m',
|
|
21
|
+
bgGreen: '\x1b[42m',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 是否支持颜色 */
|
|
25
|
+
const supportsColor = process.stdout?.isTTY && !process.env.NO_COLOR
|
|
26
|
+
|
|
27
|
+
function color(code, text) {
|
|
28
|
+
return supportsColor ? `${code}${text}${ANSI.reset}` : text
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const format = {
|
|
32
|
+
bold: (t) => color(ANSI.bold, t),
|
|
33
|
+
dim: (t) => color(ANSI.dim, t),
|
|
34
|
+
red: (t) => color(ANSI.red, t),
|
|
35
|
+
green: (t) => color(ANSI.green, t),
|
|
36
|
+
yellow: (t) => color(ANSI.yellow, t),
|
|
37
|
+
blue: (t) => color(ANSI.blue, t),
|
|
38
|
+
cyan: (t) => color(ANSI.cyan, t),
|
|
39
|
+
gray: (t) => color(ANSI.gray, t),
|
|
40
|
+
magenta: (t) => color(ANSI.magenta, t),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 格式化代码块
|
|
45
|
+
*/
|
|
46
|
+
export function codeBlock(code, lang = '') {
|
|
47
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 格式化文件路径
|
|
52
|
+
*/
|
|
53
|
+
export function formatPath(path) {
|
|
54
|
+
return format.cyan(path)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 格式化工具调用
|
|
59
|
+
*/
|
|
60
|
+
export function formatToolCall(name, input) {
|
|
61
|
+
const inputStr = typeof input === 'string' ? input : JSON.stringify(input, null, 2)
|
|
62
|
+
const shortInput = inputStr.length > 200 ? inputStr.slice(0, 200) + '...' : inputStr
|
|
63
|
+
return `${format.bold(format.magenta(name))}(${format.dim(shortInput)})`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 格式化工具结果
|
|
68
|
+
*/
|
|
69
|
+
export function formatToolResult(name, result, isError = false) {
|
|
70
|
+
const icon = isError ? format.red('✗') : format.green('✓')
|
|
71
|
+
const shortResult = result.length > 300 ? result.slice(0, 300) + '...' : result
|
|
72
|
+
return `${icon} ${format.bold(name)}: ${shortResult}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 格式化 token 用量
|
|
77
|
+
*/
|
|
78
|
+
export function formatTokenUsage(used, total) {
|
|
79
|
+
const percent = Math.round((used / total) * 100)
|
|
80
|
+
const bar = progressBar(percent, 20)
|
|
81
|
+
const colorFn = percent > 90 ? format.red : percent > 70 ? format.yellow : format.green
|
|
82
|
+
return `Tokens: ${colorFn(bar)} ${used.toLocaleString()}/${total.toLocaleString()} (${percent}%)`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 进度条
|
|
87
|
+
*/
|
|
88
|
+
export function progressBar(percent, width = 30) {
|
|
89
|
+
const filled = Math.round((percent / 100) * width)
|
|
90
|
+
const empty = width - filled
|
|
91
|
+
return '█'.repeat(filled) + '░'.repeat(empty)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 格式化时间
|
|
96
|
+
*/
|
|
97
|
+
export function formatDuration(ms) {
|
|
98
|
+
if (ms < 1000) return `${ms}ms`
|
|
99
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
100
|
+
const min = Math.floor(ms / 60000)
|
|
101
|
+
const sec = Math.round((ms % 60000) / 1000)
|
|
102
|
+
return `${min}m ${sec}s`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 格式化字节数
|
|
107
|
+
*/
|
|
108
|
+
export function formatBytes(bytes) {
|
|
109
|
+
if (bytes < 1024) return `${bytes}B`
|
|
110
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
|
111
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 表格格式化
|
|
116
|
+
*/
|
|
117
|
+
export function formatTable(headers, rows) {
|
|
118
|
+
const colWidths = headers.map((h, i) => {
|
|
119
|
+
const maxDataLen = Math.max(...rows.map(r => String(r[i]).length), 0)
|
|
120
|
+
return Math.max(h.length, maxDataLen)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' | ')
|
|
124
|
+
const separator = colWidths.map(w => '─'.repeat(w)).join('─┼─')
|
|
125
|
+
const dataLines = rows.map(row =>
|
|
126
|
+
row.map((cell, i) => String(cell).padEnd(colWidths[i])).join(' | ')
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return [headerLine, separator, ...dataLines].join('\n')
|
|
130
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函数统一导出
|
|
3
|
+
*/
|
|
4
|
+
export { unifiedDiff, inlineDiff } from './diff.js'
|
|
5
|
+
export { safeReadFile, safeWriteFile, editFile, pathExists, removeRecursive, getMtime, resolvePath } from './file-ops.js'
|
|
6
|
+
export { execCommand, spawnProcess, commandExists, sendInput } from './process.js'
|
|
7
|
+
export { format, codeBlock, formatPath, formatToolCall, formatToolResult, formatTokenUsage, formatDuration, formatBytes, formatTable, progressBar } from './format.js'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 进程管理工具
|
|
3
|
+
* 对应原版: src/utils/process.ts + src/utils/Shell.ts
|
|
4
|
+
*/
|
|
5
|
+
import { spawn, exec } from 'child_process'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 执行命令并获取输出
|
|
9
|
+
*/
|
|
10
|
+
export function execCommand(command, options = {}) {
|
|
11
|
+
const cwd = options.cwd || process.cwd()
|
|
12
|
+
const timeout = options.timeout || 120_000
|
|
13
|
+
const env = { ...process.env, ...options.env }
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const proc = exec(command, { cwd, timeout, env, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
17
|
+
resolve({
|
|
18
|
+
ok: !err,
|
|
19
|
+
exitCode: err ? (err.killed ? -1 : err.code || 1) : 0,
|
|
20
|
+
stdout: stdout || '',
|
|
21
|
+
stderr: stderr || '',
|
|
22
|
+
killed: err?.killed || false,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 启动一个长期运行的进程(带实时输出流)
|
|
30
|
+
*/
|
|
31
|
+
export function spawnProcess(command, args = [], options = {}) {
|
|
32
|
+
const cwd = options.cwd || process.cwd()
|
|
33
|
+
const env = { ...process.env, ...options.env }
|
|
34
|
+
const timeout = options.timeout
|
|
35
|
+
|
|
36
|
+
const proc = spawn(command, args, {
|
|
37
|
+
cwd,
|
|
38
|
+
env,
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
+
shell: options.shell || false,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
let stdout = ''
|
|
44
|
+
let stderr = ''
|
|
45
|
+
|
|
46
|
+
proc.stdout.on('data', (data) => {
|
|
47
|
+
stdout += data.toString()
|
|
48
|
+
options.onStdout?.(data.toString())
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
proc.stderr.on('data', (data) => {
|
|
52
|
+
stderr += data.toString()
|
|
53
|
+
options.onStderr?.(data.toString())
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// 超时处理
|
|
57
|
+
let timer = null
|
|
58
|
+
if (timeout) {
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
|
+
proc.kill('SIGTERM')
|
|
61
|
+
setTimeout(() => { try { proc.kill('SIGKILL') } catch {} }, 5000)
|
|
62
|
+
}, timeout)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const promise = new Promise((resolve) => {
|
|
66
|
+
proc.on('close', (code) => {
|
|
67
|
+
if (timer) clearTimeout(timer)
|
|
68
|
+
resolve({
|
|
69
|
+
ok: code === 0,
|
|
70
|
+
exitCode: code || 0,
|
|
71
|
+
stdout,
|
|
72
|
+
stderr,
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
proc.on('error', (err) => {
|
|
77
|
+
if (timer) clearTimeout(timer)
|
|
78
|
+
resolve({
|
|
79
|
+
ok: false,
|
|
80
|
+
exitCode: -1,
|
|
81
|
+
stdout,
|
|
82
|
+
stderr: err.message,
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return { process: proc, promise }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 检查命令是否可用
|
|
92
|
+
*/
|
|
93
|
+
export async function commandExists(cmd) {
|
|
94
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`
|
|
95
|
+
const result = await execCommand(checkCmd, { timeout: 5000 })
|
|
96
|
+
return result.ok
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 向进程发送输入
|
|
101
|
+
*/
|
|
102
|
+
export function sendInput(proc, data) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
if (!proc.stdin?.writable) {
|
|
105
|
+
return reject(new Error('Process stdin is not writable'))
|
|
106
|
+
}
|
|
107
|
+
proc.stdin.write(data, (err) => {
|
|
108
|
+
if (err) reject(err)
|
|
109
|
+
else resolve()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
}
|