dgxay-code-cli 0.0.0-stage → 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/LICENSE +674 -0
- package/README-zh.md +200 -0
- package/README.md +182 -2
- package/bin/dgxay.js +42 -0
- package/package.json +47 -4
- package/src/cli/ansi.js +509 -0
- package/src/cli/approval.js +186 -0
- package/src/cli/args.js +162 -0
- package/src/cli/command-table.js +52 -0
- package/src/cli/editor.js +574 -0
- package/src/cli/index.js +228 -0
- package/src/cli/print.js +148 -0
- package/src/cli/render.js +324 -0
- package/src/cli/repl-state.js +85 -0
- package/src/cli/repl.js +1167 -0
- package/src/cli/slash.js +74 -0
- package/src/cli/ui.js +52 -0
- package/src/commands/_shared.js +20 -0
- package/src/commands/chat.js +212 -0
- package/src/commands/config.js +363 -0
- package/src/commands/context.js +43 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/export.js +102 -0
- package/src/commands/mcp.js +257 -0
- package/src/commands/model.js +223 -0
- package/src/commands/oauth.js +246 -0
- package/src/commands/permission.js +108 -0
- package/src/commands/provider.js +332 -0
- package/src/commands/run.js +172 -0
- package/src/commands/skill.js +94 -0
- package/src/commands/workspace.js +100 -0
- package/src/core/agent.js +419 -0
- package/src/core/llm.js +709 -0
- package/src/core/mcp.js +271 -0
- package/src/core/oauth.js +737 -0
- package/src/core/prompt.js +269 -0
- package/src/core/sessions.js +208 -0
- package/src/core/tools.js +391 -0
- package/src/services/config.js +182 -0
- package/src/services/input.js +91 -0
- package/src/services/session-bus.js +104 -0
- package/src/services/settings.js +340 -0
- package/src/services/skills.js +125 -0
- package/src/services/terminal.js +124 -0
- package/src/services/version.js +25 -0
package/src/core/mcp.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP 进程管理(与桌面版一致的字段与行为,去掉 Electron 依赖)。
|
|
5
|
+
*
|
|
6
|
+
* 配置保存在 ~/.dcb/mcp.json,字段:name、command、args、env、cwd、enabled。
|
|
7
|
+
* 当前版本只管理进程,不把 MCP 工具暴露给模型(与桌面版保持一致的限制)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { spawn, execFile } = require('child_process');
|
|
13
|
+
const { DCB_DIR, MCP_PATH } = require('../services/config');
|
|
14
|
+
|
|
15
|
+
const MAX_LOGS = 400;
|
|
16
|
+
|
|
17
|
+
/** id -> { child, status, logs, exitCode, startedAt, error } */
|
|
18
|
+
const processes = new Map();
|
|
19
|
+
let notifier = null;
|
|
20
|
+
|
|
21
|
+
function setNotifier(fn) {
|
|
22
|
+
notifier = typeof fn === 'function' ? fn : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function notify(payload) {
|
|
26
|
+
try {
|
|
27
|
+
notifier?.(payload);
|
|
28
|
+
} catch {
|
|
29
|
+
/* ignore */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readConfig() {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(MCP_PATH)) return { servers: [] };
|
|
36
|
+
const parsed = JSON.parse(fs.readFileSync(MCP_PATH, 'utf8'));
|
|
37
|
+
return { servers: Array.isArray(parsed.servers) ? parsed.servers : [] };
|
|
38
|
+
} catch {
|
|
39
|
+
return { servers: [] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeConfig(data) {
|
|
44
|
+
fs.mkdirSync(DCB_DIR, { recursive: true, mode: 0o700 });
|
|
45
|
+
const tmp = `${MCP_PATH}.${process.pid}.tmp`;
|
|
46
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ servers: data.servers || [] }, null, 2)}\n`, {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
mode: 0o600
|
|
49
|
+
});
|
|
50
|
+
fs.renameSync(tmp, MCP_PATH);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeArgs(raw) {
|
|
54
|
+
if (Array.isArray(raw)) return raw.map((item) => String(item));
|
|
55
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
56
|
+
const trimmed = raw.trim();
|
|
57
|
+
if (trimmed.startsWith('[')) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(trimmed);
|
|
60
|
+
if (Array.isArray(parsed)) return parsed.map((item) => String(item));
|
|
61
|
+
} catch {
|
|
62
|
+
/* fall through */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 支持带空格与引号的参数:npx -y pkg "D:/my work"
|
|
66
|
+
const tokens = trimmed.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
67
|
+
return tokens.map((token) => token.replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeEnv(raw) {
|
|
73
|
+
if (!raw) return {};
|
|
74
|
+
if (typeof raw === 'string') {
|
|
75
|
+
const env = {};
|
|
76
|
+
raw
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((line) => line.trim())
|
|
79
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
80
|
+
.forEach((line) => {
|
|
81
|
+
const idx = line.indexOf('=');
|
|
82
|
+
if (idx > 0) env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
83
|
+
});
|
|
84
|
+
return env;
|
|
85
|
+
}
|
|
86
|
+
if (typeof raw === 'object') {
|
|
87
|
+
return Object.fromEntries(Object.entries(raw).map(([k, v]) => [String(k), String(v)]));
|
|
88
|
+
}
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function addServer(input) {
|
|
93
|
+
const name = String(input?.name || '').trim();
|
|
94
|
+
const command = String(input?.command || '').trim();
|
|
95
|
+
if (!name) throw new Error('MCP 名称不能为空');
|
|
96
|
+
if (!command) throw new Error('MCP 启动命令不能为空');
|
|
97
|
+
|
|
98
|
+
const data = readConfig();
|
|
99
|
+
const record = {
|
|
100
|
+
name,
|
|
101
|
+
command,
|
|
102
|
+
args: normalizeArgs(input.args),
|
|
103
|
+
env: normalizeEnv(input.env),
|
|
104
|
+
cwd: String(input.cwd || '').trim(),
|
|
105
|
+
enabled: input.enabled === false ? false : true,
|
|
106
|
+
updatedAt: new Date().toISOString()
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const existing = data.servers.find((server) => server.name.toLowerCase() === name.toLowerCase());
|
|
110
|
+
if (existing) {
|
|
111
|
+
Object.assign(existing, record);
|
|
112
|
+
writeConfig(data);
|
|
113
|
+
return existing;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const server = {
|
|
117
|
+
id: `mcp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
118
|
+
...record,
|
|
119
|
+
createdAt: record.updatedAt
|
|
120
|
+
};
|
|
121
|
+
data.servers.push(server);
|
|
122
|
+
writeConfig(data);
|
|
123
|
+
return server;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function removeServer(id) {
|
|
127
|
+
stopServer(id);
|
|
128
|
+
const data = readConfig();
|
|
129
|
+
data.servers = data.servers.filter((server) => server.id !== id && server.name !== id);
|
|
130
|
+
writeConfig(data);
|
|
131
|
+
processes.delete(id);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function statusOf(id) {
|
|
136
|
+
const entry = processes.get(id);
|
|
137
|
+
if (!entry) return { status: 'stopped', logs: [] };
|
|
138
|
+
return {
|
|
139
|
+
status: entry.status,
|
|
140
|
+
pid: entry.child?.pid || null,
|
|
141
|
+
startedAt: entry.startedAt || null,
|
|
142
|
+
exitCode: entry.exitCode ?? null,
|
|
143
|
+
error: entry.error || '',
|
|
144
|
+
logs: entry.logs.slice(-120)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listServers() {
|
|
149
|
+
const data = readConfig();
|
|
150
|
+
return data.servers.map((server) => ({ ...server, ...statusOf(server.id) }));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findServer(idOrName) {
|
|
154
|
+
const key = String(idOrName || '').trim().toLowerCase();
|
|
155
|
+
return listServers().find((server) => server.id === idOrName || String(server.name).toLowerCase() === key) || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function pushLog(entry, stream, text) {
|
|
159
|
+
const lines = String(text).split(/\r?\n/).filter((line, index, arr) => line || index < arr.length - 1);
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
entry.logs.push({ stream, text: line, at: Date.now() });
|
|
162
|
+
}
|
|
163
|
+
if (entry.logs.length > MAX_LOGS) entry.logs.splice(0, entry.logs.length - MAX_LOGS);
|
|
164
|
+
notify({ type: 'log', id: entry.id, stream, text: String(text) });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function startServer(id) {
|
|
168
|
+
const data = readConfig();
|
|
169
|
+
const server = data.servers.find((item) => item.id === id) || data.servers.find((item) => item.name === id);
|
|
170
|
+
if (!server) throw new Error('找不到该 MCP server');
|
|
171
|
+
|
|
172
|
+
const existing = processes.get(server.id);
|
|
173
|
+
if (existing?.status === 'running' && existing.child && !existing.child.killed) {
|
|
174
|
+
return { ok: true, status: 'running', pid: existing.child.pid, server };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const env = { ...process.env, ...(server.env || {}) };
|
|
178
|
+
let child;
|
|
179
|
+
try {
|
|
180
|
+
child = spawn(server.command, server.args || [], {
|
|
181
|
+
cwd: server.cwd || DCB_DIR,
|
|
182
|
+
env,
|
|
183
|
+
shell: true,
|
|
184
|
+
windowsHide: true,
|
|
185
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
throw new Error(`无法启动 MCP server:${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const entry = {
|
|
192
|
+
id: server.id,
|
|
193
|
+
child,
|
|
194
|
+
status: 'running',
|
|
195
|
+
logs: [],
|
|
196
|
+
startedAt: Date.now(),
|
|
197
|
+
exitCode: null,
|
|
198
|
+
error: ''
|
|
199
|
+
};
|
|
200
|
+
processes.set(server.id, entry);
|
|
201
|
+
child.stdout?.on('data', (chunk) => pushLog(entry, 'stdout', chunk.toString('utf8')));
|
|
202
|
+
child.stderr?.on('data', (chunk) => pushLog(entry, 'stderr', chunk.toString('utf8')));
|
|
203
|
+
child.on('error', (err) => {
|
|
204
|
+
entry.status = 'error';
|
|
205
|
+
entry.error = err.message;
|
|
206
|
+
notify({ type: 'status', id: server.id, status: 'error', error: err.message });
|
|
207
|
+
});
|
|
208
|
+
child.on('exit', (code, signal) => {
|
|
209
|
+
const wasStopping = entry.status === 'stopping';
|
|
210
|
+
entry.status = wasStopping || code === 0 ? 'stopped' : 'error';
|
|
211
|
+
entry.exitCode = code;
|
|
212
|
+
if (!wasStopping && code !== 0 && code !== null) {
|
|
213
|
+
entry.error = `进程退出,code=${code}${signal ? `, signal=${signal}` : ''}`;
|
|
214
|
+
}
|
|
215
|
+
notify({ type: 'status', id: server.id, status: entry.status, code, signal });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
notify({ type: 'status', id: server.id, status: 'running', pid: child.pid });
|
|
219
|
+
return { ok: true, status: 'running', pid: child.pid, server };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stopServer(id) {
|
|
223
|
+
const entry = processes.get(id);
|
|
224
|
+
if (!entry?.child) return false;
|
|
225
|
+
const pid = entry.child.pid;
|
|
226
|
+
entry.status = 'stopping';
|
|
227
|
+
notify({ type: 'status', id, status: 'stopping' });
|
|
228
|
+
|
|
229
|
+
if (process.platform === 'win32' && pid) {
|
|
230
|
+
execFile('taskkill', ['/pid', String(pid), '/T', '/F'], () => {});
|
|
231
|
+
} else {
|
|
232
|
+
try {
|
|
233
|
+
entry.child.kill('SIGTERM');
|
|
234
|
+
} catch {
|
|
235
|
+
/* ignore */
|
|
236
|
+
}
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
try {
|
|
239
|
+
if (entry.child && !entry.child.killed) entry.child.kill('SIGKILL');
|
|
240
|
+
} catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
243
|
+
}, 3000).unref?.();
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function killAll() {
|
|
249
|
+
for (const id of processes.keys()) stopServer(id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function runningServers() {
|
|
253
|
+
return [...processes.entries()]
|
|
254
|
+
.filter(([, entry]) => entry.status === 'running')
|
|
255
|
+
.map(([id]) => id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = {
|
|
259
|
+
MCP_PATH,
|
|
260
|
+
readConfig,
|
|
261
|
+
addServer,
|
|
262
|
+
removeServer,
|
|
263
|
+
listServers,
|
|
264
|
+
findServer,
|
|
265
|
+
startServer,
|
|
266
|
+
stopServer,
|
|
267
|
+
setNotifier,
|
|
268
|
+
killAll,
|
|
269
|
+
statusOf,
|
|
270
|
+
runningServers
|
|
271
|
+
};
|