@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.
@@ -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
+ }