@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,119 @@
1
+ /**
2
+ * 流式响应处理
3
+ * OpenAI 兼容 SSE 格式(全行业通用)
4
+ *
5
+ * 适用于: DeepSeek / Qwen / GLM / Kimi / Ollama / vLLM / OpenAI / 任何兼容接口
6
+ */
7
+
8
+ /**
9
+ * 解析 SSE 流式响应
10
+ */
11
+ export async function* parseStream(response) {
12
+ const reader = response.body.getReader()
13
+ const decoder = new TextDecoder()
14
+ let buffer = ''
15
+ const toolCallBuffers = new Map() // index → { id, name, arguments }
16
+
17
+ const result = {
18
+ content: '',
19
+ toolCalls: [],
20
+ usage: { input_tokens: 0, output_tokens: 0 },
21
+ }
22
+
23
+ try {
24
+ while (true) {
25
+ const { done, value } = await reader.read()
26
+ if (done) break
27
+
28
+ buffer += decoder.decode(value, { stream: true })
29
+ const lines = buffer.split('\n')
30
+ buffer = lines.pop()
31
+
32
+ for (const line of lines) {
33
+ if (!line.startsWith('data: ')) continue
34
+ const data = line.slice(6).trim()
35
+ if (data === '[DONE]') continue
36
+
37
+ try {
38
+ const chunk = JSON.parse(data)
39
+ const delta = chunk.choices?.[0]?.delta
40
+
41
+ if (!delta) continue
42
+
43
+ // 文本
44
+ if (delta.content) {
45
+ result.content += delta.content
46
+ yield { type: 'text', text: delta.content }
47
+ }
48
+
49
+ // 工具调用
50
+ if (delta.tool_calls) {
51
+ for (const tc of delta.tool_calls) {
52
+ const idx = tc.index
53
+ if (!toolCallBuffers.has(idx)) {
54
+ toolCallBuffers.set(idx, {
55
+ id: tc.id || '',
56
+ name: tc.function?.name || '',
57
+ arguments: '',
58
+ })
59
+ }
60
+ const buf = toolCallBuffers.get(idx)
61
+ if (tc.id) buf.id = tc.id
62
+ if (tc.function?.name) buf.name = tc.function.name
63
+ if (tc.function?.arguments) buf.arguments += tc.function.arguments
64
+ }
65
+ }
66
+
67
+ // 流结束 — 处理积攒的工具调用
68
+ if (chunk.choices?.[0]?.finish_reason) {
69
+ for (const [_, buf] of toolCallBuffers) {
70
+ let input = {}
71
+ try { input = JSON.parse(buf.arguments) } catch {}
72
+ result.toolCalls.push({ id: buf.id, name: buf.name, input })
73
+ yield { type: 'tool_use', toolCall: { id: buf.id, name: buf.name, input } }
74
+ }
75
+ }
76
+
77
+ // usage
78
+ if (chunk.usage) {
79
+ result.usage.input_tokens = chunk.usage.prompt_tokens || 0
80
+ result.usage.output_tokens = chunk.usage.completion_tokens || 0
81
+ }
82
+ } catch {
83
+ // 忽略解析错误
84
+ }
85
+ }
86
+ }
87
+ } finally {
88
+ reader.releaseLock()
89
+ }
90
+
91
+ yield { type: 'done', result }
92
+ }
93
+
94
+ /**
95
+ * 解析非流式响应
96
+ */
97
+ export function parseNonStreamResponse(data) {
98
+ const result = { content: '', toolCalls: [], usage: {} }
99
+ const choice = data.choices?.[0]
100
+
101
+ if (choice?.message?.content) {
102
+ result.content = choice.message.content
103
+ }
104
+
105
+ for (const tc of (choice?.message?.tool_calls || [])) {
106
+ let input = {}
107
+ try { input = JSON.parse(tc.function.arguments) } catch {}
108
+ result.toolCalls.push({ id: tc.id, name: tc.function.name, input })
109
+ }
110
+
111
+ if (data.usage) {
112
+ result.usage = {
113
+ input_tokens: data.usage.prompt_tokens,
114
+ output_tokens: data.usage.completion_tokens,
115
+ }
116
+ }
117
+
118
+ return result
119
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Token 预算管理
3
+ * 对应原版: src/query/tokenBudget.ts
4
+ */
5
+
6
+ /**
7
+ * 简单的 token 估算器
8
+ * 规则:英文 ~4 字符/token,中文 ~1.5 字符/token
9
+ */
10
+ export function estimateTokens(text) {
11
+ if (!text) return 0
12
+ // 统计中文字符
13
+ const cjkCount = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length
14
+ const nonCjk = text.length - cjkCount
15
+ return Math.ceil(cjkCount / 1.5 + nonCjk / 4)
16
+ }
17
+
18
+ export class TokenBudget {
19
+ constructor(options = {}) {
20
+ this.maxTokens = options.maxTokens || 200_000
21
+ this.maxOutputTokens = options.maxOutputTokens || 8192
22
+ this.reservedForSystem = options.reservedForSystem || 20_000
23
+ this.reservedForOutput = options.reservedForOutput || 8192
24
+ this.used = 0
25
+ this.inputTokens = 0
26
+ this.outputTokens = 0
27
+ }
28
+
29
+ /** 可用于上下文的最大 token 数 */
30
+ get availableForContext() {
31
+ return this.maxTokens - this.reservedForSystem - this.reservedForOutput - this.inputTokens
32
+ }
33
+
34
+ /** 是否还有预算 */
35
+ get hasBudget() {
36
+ return this.availableForContext > 1000
37
+ }
38
+
39
+ /** 使用率百分比 */
40
+ get usagePercent() {
41
+ return Math.round((this.inputTokens / this.maxTokens) * 100)
42
+ }
43
+
44
+ /** 记录一次 API 调用的 token 使用 */
45
+ recordUsage(usage) {
46
+ if (usage.input_tokens) this.inputTokens += usage.input_tokens
47
+ if (usage.output_tokens) this.outputTokens += usage.output_tokens
48
+ if (usage.cache_read_input_tokens) this.inputTokens += usage.cache_read_input_tokens
49
+ this.used = this.inputTokens + this.outputTokens
50
+ }
51
+
52
+ /** 估算消息列表的 token 数 */
53
+ estimateMessages(messages) {
54
+ let total = 0
55
+ for (const msg of messages) {
56
+ if (typeof msg.content === 'string') {
57
+ total += estimateTokens(msg.content)
58
+ } else if (Array.isArray(msg.content)) {
59
+ for (const block of msg.content) {
60
+ if (block.type === 'text') total += estimateTokens(block.text)
61
+ else if (block.type === 'tool_result') total += estimateTokens(block.content)
62
+ else if (block.type === 'tool_use') total += estimateTokens(JSON.stringify(block.input))
63
+ }
64
+ }
65
+ // 每条消息有固定开销
66
+ total += 10
67
+ }
68
+ return total
69
+ }
70
+
71
+ /** 检查是否可以在预算内发送这些消息 */
72
+ canAfford(messages) {
73
+ const estimated = this.estimateMessages(messages)
74
+ return (this.inputTokens + estimated) < (this.maxTokens - this.reservedForOutput)
75
+ }
76
+
77
+ /** 格式化 token 使用情况 */
78
+ format() {
79
+ return `Token Budget: ${this.inputTokens.toLocaleString()}/${this.maxTokens.toLocaleString()} (${this.usagePercent}% used) | Output: ${this.outputTokens.toLocaleString()}`
80
+ }
81
+
82
+ /** 重置 */
83
+ reset() {
84
+ this.used = 0
85
+ this.inputTokens = 0
86
+ this.outputTokens = 0
87
+ }
88
+ }
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Code — Node.js Edition
5
+ * 入口文件
6
+ */
7
+
8
+ import { main } from './core/cli.js'
9
+
10
+ main().catch((err) => {
11
+ console.error('Fatal error:', err.message)
12
+ process.exit(1)
13
+ })
@@ -0,0 +1,214 @@
1
+ /**
2
+ * MCP (Model Context Protocol) 客户端
3
+ * 对应原版: src/services/mcp/
4
+ * 简化版:支持 stdio 传输,JSON-RPC 2.0
5
+ */
6
+ import { spawn } from 'child_process'
7
+
8
+ /**
9
+ * JSON-RPC 2.0 请求 ID 计数器
10
+ */
11
+ let requestId = 0
12
+
13
+ function nextId() {
14
+ return ++requestId
15
+ }
16
+
17
+ /**
18
+ * MCP 客户端 — 通过 stdio 与 MCP 服务器通信
19
+ */
20
+ export class MCPClient {
21
+ constructor(serverConfig) {
22
+ this.config = serverConfig
23
+ this.process = null
24
+ this.pending = new Map() // id → { resolve, reject }
25
+ this.buffer = ''
26
+ this.tools = []
27
+ this.resources = []
28
+ }
29
+
30
+ /** 启动 MCP 服务器进程 */
31
+ async connect() {
32
+ const { command, args = [], env = {} } = this.config
33
+
34
+ this.process = spawn(command, args, {
35
+ stdio: ['pipe', 'pipe', 'pipe'],
36
+ env: { ...process.env, ...env },
37
+ })
38
+
39
+ this.process.stdout.on('data', (data) => {
40
+ this.buffer += data.toString()
41
+ this._processBuffer()
42
+ })
43
+
44
+ this.process.stderr.on('data', (data) => {
45
+ // MCP 服务器日志输出到 stderr
46
+ if (this.config.debug) {
47
+ console.error(`[MCP stderr] ${data.toString().trim()}`)
48
+ }
49
+ })
50
+
51
+ this.process.on('error', (err) => {
52
+ // 拒绝所有等待中的请求
53
+ for (const [id, { reject }] of this.pending) {
54
+ reject(new Error(`MCP server error: ${err.message}`))
55
+ }
56
+ this.pending.clear()
57
+ })
58
+
59
+ this.process.on('close', (code) => {
60
+ for (const [id, { reject }] of this.pending) {
61
+ reject(new Error(`MCP server exited with code ${code}`))
62
+ }
63
+ this.pending.clear()
64
+ })
65
+
66
+ // 初始化握手
67
+ await this._initialize()
68
+ }
69
+
70
+ /** 初始化协议 */
71
+ async _initialize() {
72
+ const result = await this._sendRequest('initialize', {
73
+ protocolVersion: '2024-11-05',
74
+ capabilities: {},
75
+ clientInfo: {
76
+ name: 'claude-code-node',
77
+ version: '1.0.0',
78
+ },
79
+ })
80
+
81
+ // 发送 initialized 通知
82
+ this._sendNotification('notifications/initialized', {})
83
+
84
+ // 获取工具列表
85
+ try {
86
+ const toolsResult = await this._sendRequest('tools/list', {})
87
+ this.tools = toolsResult?.tools || []
88
+ } catch {
89
+ this.tools = []
90
+ }
91
+
92
+ // 获取资源列表
93
+ try {
94
+ const resourcesResult = await this._sendRequest('resources/list', {})
95
+ this.resources = resourcesResult?.resources || []
96
+ } catch {
97
+ this.resources = []
98
+ }
99
+
100
+ return result
101
+ }
102
+
103
+ /** 调用 MCP 工具 */
104
+ async callTool(name, args = {}) {
105
+ return this._sendRequest('tools/call', {
106
+ name,
107
+ arguments: args,
108
+ })
109
+ }
110
+
111
+ /** 读取 MCP 资源 */
112
+ async readResource(uri) {
113
+ return this._sendRequest('resources/read', { uri })
114
+ }
115
+
116
+ /** 发送 JSON-RPC 请求 */
117
+ _sendRequest(method, params) {
118
+ return new Promise((resolve, reject) => {
119
+ const id = nextId()
120
+ const message = JSON.stringify({
121
+ jsonrpc: '2.0',
122
+ id,
123
+ method,
124
+ params,
125
+ })
126
+
127
+ this.pending.set(id, { resolve, reject })
128
+
129
+ // 每条消息以换行符分隔
130
+ this.process.stdin.write(message + '\n', (err) => {
131
+ if (err) {
132
+ this.pending.delete(id)
133
+ reject(new Error(`Failed to send message: ${err.message}`))
134
+ }
135
+ })
136
+ })
137
+ }
138
+
139
+ /** 发送 JSON-RPC 通知(无响应) */
140
+ _sendNotification(method, params) {
141
+ const message = JSON.stringify({
142
+ jsonrpc: '2.0',
143
+ method,
144
+ params,
145
+ })
146
+ this.process.stdin.write(message + '\n')
147
+ }
148
+
149
+ /** 处理接收缓冲区 */
150
+ _processBuffer() {
151
+ const lines = this.buffer.split('\n')
152
+ this.buffer = lines.pop() // 保留不完整的行
153
+
154
+ for (const line of lines) {
155
+ if (!line.trim()) continue
156
+ try {
157
+ const message = JSON.parse(line)
158
+
159
+ if (message.id && this.pending.has(message.id)) {
160
+ const { resolve, reject } = this.pending.get(message.id)
161
+ this.pending.delete(message.id)
162
+
163
+ if (message.error) {
164
+ reject(new Error(message.error.message || 'MCP error'))
165
+ } else {
166
+ resolve(message.result)
167
+ }
168
+ }
169
+ // 忽略通知和未知消息
170
+ } catch {
171
+ // 忽略解析错误
172
+ }
173
+ }
174
+ }
175
+
176
+ /** 关闭连接 */
177
+ async close() {
178
+ if (this.process) {
179
+ try {
180
+ await this._sendRequest('shutdown', {})
181
+ } catch {
182
+ // 忽略关闭错误
183
+ }
184
+ this.process.kill('SIGTERM')
185
+ this.process = null
186
+ }
187
+ }
188
+
189
+ /** 获取工具定义 — tool_use 格式(兼容多种 API) */
190
+ getToolDefinitions() {
191
+ return this.tools.map(t => ({
192
+ name: t.name,
193
+ description: t.description,
194
+ input_schema: t.inputSchema,
195
+ }))
196
+ }
197
+
198
+ /** @deprecated 使用 getToolDefinitions() 替代 */
199
+ getAnthropicTools() {
200
+ return this.getToolDefinitions()
201
+ }
202
+
203
+ /** 获取工具定义(适配 OpenAI function-calling 格式) */
204
+ getOpenAITools() {
205
+ return this.tools.map(t => ({
206
+ type: 'function',
207
+ function: {
208
+ name: t.name,
209
+ description: t.description,
210
+ parameters: t.inputSchema,
211
+ },
212
+ }))
213
+ }
214
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * MCP 模块统一导出
3
+ */
4
+ export { MCPClient } from './client.js'
5
+ export { MCPRegistry } from './registry.js'
@@ -0,0 +1,176 @@
1
+ /**
2
+ * MCP 服务器注册表
3
+ * 对应原版: src/services/mcp/mcpServerApproval.tsx + 配置加载
4
+ */
5
+ import { readFile, writeFile, mkdir } from 'fs/promises'
6
+ import { resolve, join } from 'path'
7
+ import { existsSync } from 'fs'
8
+ import { MCPClient } from './client.js'
9
+
10
+ /**
11
+ * MCP 服务器注册表 — 管理多个 MCP 服务器连接
12
+ */
13
+ export class MCPRegistry {
14
+ constructor(options = {}) {
15
+ this.configPath = options.configPath || null
16
+ this.servers = new Map() // name → MCPClient
17
+ this.serverConfigs = new Map() // name → config
18
+ }
19
+
20
+ /** 从配置文件加载 MCP 服务器列表 */
21
+ async loadFromConfig(configPath) {
22
+ this.configPath = configPath
23
+ try {
24
+ const raw = await readFile(configPath, 'utf-8')
25
+ const config = JSON.parse(raw)
26
+ const mcpServers = config.mcpServers || {}
27
+
28
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
29
+ this.serverConfigs.set(name, serverConfig)
30
+ }
31
+ } catch {
32
+ // 配置文件不存在 — 空注册表
33
+ }
34
+ }
35
+
36
+ /** 注册一个 MCP 服务器 */
37
+ register(name, config) {
38
+ this.serverConfigs.set(name, config)
39
+ }
40
+
41
+ /** 注销一个 MCP 服务器 */
42
+ unregister(name) {
43
+ this.serverConfigs.delete(name)
44
+ const client = this.servers.get(name)
45
+ if (client) {
46
+ client.close()
47
+ this.servers.delete(name)
48
+ }
49
+ }
50
+
51
+ /** 连接指定服务器 */
52
+ async connect(name) {
53
+ const config = this.serverConfigs.get(name)
54
+ if (!config) {
55
+ throw new Error(`MCP server not registered: ${name}`)
56
+ }
57
+
58
+ const client = new MCPClient(config)
59
+ await client.connect()
60
+ this.servers.set(name, client)
61
+ return client
62
+ }
63
+
64
+ /** 连接所有注册的服务器 */
65
+ async connectAll() {
66
+ const results = {}
67
+ for (const [name, _] of this.serverConfigs) {
68
+ try {
69
+ results[name] = await this.connect(name)
70
+ } catch (err) {
71
+ results[name] = { error: err.message }
72
+ }
73
+ }
74
+ return results
75
+ }
76
+
77
+ /** 关闭所有连接 */
78
+ async closeAll() {
79
+ const promises = []
80
+ for (const [name, client] of this.servers) {
81
+ promises.push(
82
+ client.close().catch(() => {})
83
+ )
84
+ }
85
+ await Promise.all(promises)
86
+ this.servers.clear()
87
+ }
88
+
89
+ /** 获取所有已连接服务器的工具列表 */
90
+ getAllTools() {
91
+ const tools = []
92
+ for (const [name, client] of this.servers) {
93
+ for (const tool of client.tools) {
94
+ tools.push({
95
+ ...tool,
96
+ _mcpServer: name, // 标记来源
97
+ })
98
+ }
99
+ }
100
+ return tools
101
+ }
102
+
103
+ /** 获取所有已连接服务器的资源列表 */
104
+ getAllResources() {
105
+ const resources = []
106
+ for (const [name, client] of this.servers) {
107
+ for (const resource of client.resources) {
108
+ resources.push({
109
+ ...resource,
110
+ _mcpServer: name,
111
+ })
112
+ }
113
+ }
114
+ return resources
115
+ }
116
+
117
+ /** 调用指定服务器上的工具 */
118
+ async callTool(serverName, toolName, args) {
119
+ const client = this.servers.get(serverName)
120
+ if (!client) {
121
+ throw new Error(`MCP server not connected: ${serverName}`)
122
+ }
123
+ return client.callTool(toolName, args)
124
+ }
125
+
126
+ /** 通过工具名查找并调用(自动路由到正确的服务器) */
127
+ async callToolByName(toolName, args) {
128
+ for (const [name, client] of this.servers) {
129
+ const tool = client.tools.find(t => t.name === toolName)
130
+ if (tool) {
131
+ return client.callTool(toolName, args)
132
+ }
133
+ }
134
+ throw new Error(`Tool not found on any connected MCP server: ${toolName}`)
135
+ }
136
+
137
+ /** 读取指定服务器上的资源 */
138
+ async readResource(serverName, uri) {
139
+ const client = this.servers.get(serverName)
140
+ if (!client) {
141
+ throw new Error(`MCP server not connected: ${serverName}`)
142
+ }
143
+ return client.readResource(uri)
144
+ }
145
+
146
+ /** 保存当前配置到文件 */
147
+ async saveConfig(configPath) {
148
+ const path = configPath || this.configPath
149
+ if (!path) throw new Error('No config path specified')
150
+
151
+ const mcpServers = {}
152
+ for (const [name, config] of this.serverConfigs) {
153
+ mcpServers[name] = config
154
+ }
155
+
156
+ await mkdir(resolve(path, '..'), { recursive: true })
157
+ await writeFile(path, JSON.stringify({ mcpServers }, null, 2), 'utf-8')
158
+ }
159
+
160
+ /** 获取服务器状态 */
161
+ getStatus() {
162
+ const status = []
163
+ for (const [name, config] of this.serverConfigs) {
164
+ const connected = this.servers.has(name)
165
+ const client = this.servers.get(name)
166
+ status.push({
167
+ name,
168
+ command: config.command,
169
+ connected,
170
+ toolCount: client?.tools?.length || 0,
171
+ resourceCount: client?.resources?.length || 0,
172
+ })
173
+ }
174
+ return status
175
+ }
176
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 权限检查器
3
+ * 对应原版: src/hooks/toolPermission/ + src/Tool.ts 中的 ToolPermissionContext
4
+ */
5
+
6
+ export class PermissionChecker {
7
+ constructor(mode = 'ask') {
8
+ this.mode = mode // 'always-allow' | 'ask' | 'deny'
9
+ this.alwaysAllow = new Set()
10
+ this.alwaysDeny = new Set()
11
+ this.askRules = new Set()
12
+ }
13
+
14
+ /**
15
+ * 检查工具调用是否被允许
16
+ */
17
+ async check(toolName, input = {}) {
18
+ if (this.mode === 'always-allow') return true
19
+ if (this.mode === 'deny') return false
20
+
21
+ if (this.alwaysAllow.has(toolName)) return true
22
+ if (this.alwaysDeny.has(toolName)) return false
23
+
24
+ // 'ask' 模式 — 需要用户确认(简化版直接允许)
25
+ // 完整版应该在终端显示确认对话框
26
+ return true
27
+ }
28
+
29
+ allow(toolName) {
30
+ this.alwaysAllow.add(toolName)
31
+ }
32
+
33
+ deny(toolName) {
34
+ this.alwaysDeny.add(toolName)
35
+ }
36
+ }
37
+