dgxay-code-cli 0.0.0-stage → 1.1.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 +242 -0
- package/README.md +223 -2
- package/bin/dgxay.js +42 -0
- package/package.json +47 -4
- package/src/cli/ansi.js +539 -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 +828 -0
- package/src/cli/i18n.js +276 -0
- package/src/cli/index.js +228 -0
- package/src/cli/print.js +148 -0
- package/src/cli/render.js +395 -0
- package/src/cli/repl-state.js +85 -0
- package/src/cli/repl.js +1231 -0
- package/src/cli/slash.js +75 -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 +420 -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
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent 循环:与桌面版 chat:send 的流程一致,把事件流交给上层(REPL / 单次运行)。
|
|
5
|
+
*
|
|
6
|
+
* 事件:
|
|
7
|
+
* status { text }
|
|
8
|
+
* delta { text } 正文增量
|
|
9
|
+
* reasoning { text } 思考增量
|
|
10
|
+
* tool { id, name, status: 'running'|'ok'|'error', args, output, review }
|
|
11
|
+
* review { toolId, name, kind, decision, reason, reviewer, modelId }
|
|
12
|
+
* review-start { toolId, name, kind }
|
|
13
|
+
* waiting { toolId, name, kind, permission }
|
|
14
|
+
* file { action, path, display, bytes }
|
|
15
|
+
* done { text, usage }
|
|
16
|
+
* error { message }
|
|
17
|
+
* aborted {}
|
|
18
|
+
*
|
|
19
|
+
* 返回 { ok, aborted, text, reasoning, usage, toolLog, files, error }。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const { streamChat } = require('./llm');
|
|
24
|
+
const tools = require('./tools');
|
|
25
|
+
const skills = require('../services/skills');
|
|
26
|
+
const {
|
|
27
|
+
systemPromptFor,
|
|
28
|
+
resolveProviderAuth,
|
|
29
|
+
resolveReviewer,
|
|
30
|
+
reviewOperation,
|
|
31
|
+
looksUnfinishedWork,
|
|
32
|
+
summarizeArgs
|
|
33
|
+
} = require('./prompt');
|
|
34
|
+
const { trimHistory } = require('./sessions');
|
|
35
|
+
const { t } = require('../cli/i18n');
|
|
36
|
+
|
|
37
|
+
const HISTORY_LIMIT = 24;
|
|
38
|
+
const MAX_STEPS_CHAT = 8;
|
|
39
|
+
const MAX_STEPS_WORK = 24;
|
|
40
|
+
const MAX_CONTINUATIONS = 2;
|
|
41
|
+
|
|
42
|
+
const VALID_PERMISSIONS = ['none', 'normal', 'auto', 'full'];
|
|
43
|
+
|
|
44
|
+
function describeOperation({ kind, name, args, command }) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (kind === 'terminal') {
|
|
47
|
+
lines.push(`Command: ${command || ''}`);
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
lines.push(`Tool: ${name}`);
|
|
51
|
+
if (args?.path) lines.push(`Path: ${args.path}`);
|
|
52
|
+
if (args?.url) lines.push(`URL: ${args.url}`);
|
|
53
|
+
if (args?.command) lines.push(`Command: ${args.command}`);
|
|
54
|
+
if (args?.name) lines.push(`Name: ${args.name}`);
|
|
55
|
+
const body = summarizeArgs(args);
|
|
56
|
+
if (body && body !== '{}') lines.push(`Arguments: ${body}`);
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeHistory(messages) {
|
|
61
|
+
return (Array.isArray(messages) ? messages : [])
|
|
62
|
+
.filter((m) => m && ['user', 'assistant'].includes(m.role) && typeof m.content === 'string' && m.content.trim())
|
|
63
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolvePermissions(config) {
|
|
67
|
+
const permission = VALID_PERMISSIONS.includes(config.ui?.permission) ? config.ui.permission : 'normal';
|
|
68
|
+
if (permission === 'auto' && !resolveReviewer(config, config.activeProviderId)) {
|
|
69
|
+
// 自动审查在未配置审查模型时不可用,与界面一致自动回落到普通权限
|
|
70
|
+
return 'normal';
|
|
71
|
+
}
|
|
72
|
+
return permission;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 请求一次操作的授权。
|
|
77
|
+
* @param {object} options
|
|
78
|
+
* @param {object} options.config
|
|
79
|
+
* @param {string} options.permission
|
|
80
|
+
* @param {(request)=>Promise<boolean>} options.askUser 用户确认回调
|
|
81
|
+
* @param {(event)=>void} [options.emit]
|
|
82
|
+
*/
|
|
83
|
+
async function requestOperationApproval({ config, permission, kind, name, args, command, workspace, providerId, toolId, askUser, emit }) {
|
|
84
|
+
if (permission === 'full') return { allow: true, source: 'full', review: null };
|
|
85
|
+
if (permission !== 'normal' && permission !== 'auto') {
|
|
86
|
+
return { allow: false, source: 'none', review: null, reason: 'Current permission level forbids this operation' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (permission === 'normal') {
|
|
90
|
+
emit?.({ type: 'waiting', toolId, name, kind, permission });
|
|
91
|
+
const allowed = await askUser({
|
|
92
|
+
kind,
|
|
93
|
+
permission,
|
|
94
|
+
title: kind === 'terminal' ? t('approval_terminal') : t('approval_tool'),
|
|
95
|
+
operation: name,
|
|
96
|
+
detail: describeOperation({ kind, name, args, command }),
|
|
97
|
+
command: command || '',
|
|
98
|
+
workspace: workspace || '',
|
|
99
|
+
review: '',
|
|
100
|
+
reviewer: '',
|
|
101
|
+
reviewModel: ''
|
|
102
|
+
});
|
|
103
|
+
return { allow: Boolean(allowed), source: 'user', review: null, reason: allowed ? '' : 'The user declined this operation' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
emit?.({ type: 'review-start', toolId, name, kind });
|
|
107
|
+
const review = await reviewOperation({
|
|
108
|
+
config,
|
|
109
|
+
providerId: providerId || config.activeProviderId,
|
|
110
|
+
kind,
|
|
111
|
+
name,
|
|
112
|
+
args,
|
|
113
|
+
command,
|
|
114
|
+
workspace
|
|
115
|
+
});
|
|
116
|
+
emit?.({
|
|
117
|
+
type: 'review',
|
|
118
|
+
toolId,
|
|
119
|
+
name,
|
|
120
|
+
kind,
|
|
121
|
+
decision: review.decision,
|
|
122
|
+
reason: review.reason,
|
|
123
|
+
reviewer: review.reviewer,
|
|
124
|
+
modelId: review.modelId
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (review.decision === 'allow') return { allow: true, source: 'auto', review };
|
|
128
|
+
|
|
129
|
+
const allowed = await askUser({
|
|
130
|
+
kind,
|
|
131
|
+
permission,
|
|
132
|
+
title: kind === 'terminal' ? t('approval_terminal') : t('approval_tool'),
|
|
133
|
+
operation: name,
|
|
134
|
+
detail: describeOperation({ kind, name, args, command }),
|
|
135
|
+
command: command || '',
|
|
136
|
+
workspace: workspace || '',
|
|
137
|
+
review: review.reason,
|
|
138
|
+
reviewer: review.reviewer,
|
|
139
|
+
reviewModel: review.modelId
|
|
140
|
+
});
|
|
141
|
+
return { allow: Boolean(allowed), source: 'user', review, reason: allowed ? '' : 'The user declined this operation' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function providerOf(config, providerId) {
|
|
145
|
+
const providers = config.providers || [];
|
|
146
|
+
return providers.find((p) => p.id === providerId) || providers.find((p) => p.id === config.activeProviderId) || providers[0] || null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function modelOf(provider, modelId, config) {
|
|
150
|
+
const models = provider?.models || [];
|
|
151
|
+
return (
|
|
152
|
+
models.find((m) => m.id === modelId)?.id ||
|
|
153
|
+
models.find((m) => m.id === config.activeModelId)?.id ||
|
|
154
|
+
models[0]?.id ||
|
|
155
|
+
''
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeWorkspace(value) {
|
|
160
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
161
|
+
if (!raw) return '';
|
|
162
|
+
try {
|
|
163
|
+
const stat = fs.statSync(raw);
|
|
164
|
+
return stat.isDirectory() ? raw : '';
|
|
165
|
+
} catch {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 跑一轮完整对话(可能包含多次工具循环与自动续跑)。
|
|
172
|
+
*
|
|
173
|
+
* @param {object} options
|
|
174
|
+
* @param {object} options.config 配置快照
|
|
175
|
+
* @param {object[]} options.messages 历史消息 [{ role, content }]
|
|
176
|
+
* @param {string} [options.providerId]
|
|
177
|
+
* @param {string} [options.modelId]
|
|
178
|
+
* @param {string} [options.mode] 'chat' | 'work'
|
|
179
|
+
* @param {string} [options.permission] none|normal|auto|full
|
|
180
|
+
* @param {string} [options.workspace]
|
|
181
|
+
* @param {string} [options.skill] 本次对话启用的技能名
|
|
182
|
+
* @param {(request)=>Promise<boolean>} options.askUser
|
|
183
|
+
* @param {(event)=>void} [options.onEvent]
|
|
184
|
+
* @param {AbortSignal} [options.signal]
|
|
185
|
+
*/
|
|
186
|
+
async function runAgent({
|
|
187
|
+
config,
|
|
188
|
+
messages,
|
|
189
|
+
providerId,
|
|
190
|
+
modelId,
|
|
191
|
+
mode = 'chat',
|
|
192
|
+
permission,
|
|
193
|
+
workspace,
|
|
194
|
+
skill,
|
|
195
|
+
askUser,
|
|
196
|
+
onEvent,
|
|
197
|
+
signal
|
|
198
|
+
}) {
|
|
199
|
+
const resolvedMode = mode === 'work' ? 'work' : 'chat';
|
|
200
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
201
|
+
throw Object.assign(new Error('No messages: at least one user message is required'), { userError: true });
|
|
202
|
+
}
|
|
203
|
+
const provider = providerOf(config, providerId);
|
|
204
|
+
if (!provider) throw new Error('Provider not found. Run dgxay provider add first');
|
|
205
|
+
const targetModel = modelOf(provider, modelId, config);
|
|
206
|
+
if (!targetModel) throw new Error('This provider has no models. Add one first');
|
|
207
|
+
|
|
208
|
+
const effectivePermission = VALID_PERMISSIONS.includes(permission) ? permission : resolvePermissions(config);
|
|
209
|
+
const activeWorkspace = normalizeWorkspace(workspace || config.work?.workspace || '');
|
|
210
|
+
const useTools = resolvedMode === 'work' && Boolean(activeWorkspace);
|
|
211
|
+
|
|
212
|
+
let toolDefs = [];
|
|
213
|
+
if (useTools) toolDefs = tools.toolsFor(effectivePermission);
|
|
214
|
+
else if (effectivePermission !== 'none') toolDefs = [tools.DEFINITIONS.install_mcp_server];
|
|
215
|
+
|
|
216
|
+
const activeSkill = skill ? skills.readSkill(skill) : null;
|
|
217
|
+
|
|
218
|
+
const apiMessages = [
|
|
219
|
+
{
|
|
220
|
+
role: 'system',
|
|
221
|
+
content: systemPromptFor({
|
|
222
|
+
mode: resolvedMode,
|
|
223
|
+
permission: effectivePermission,
|
|
224
|
+
workspace: activeWorkspace,
|
|
225
|
+
customPrompt: config.chat?.systemPrompt,
|
|
226
|
+
skill: activeSkill
|
|
227
|
+
})
|
|
228
|
+
},
|
|
229
|
+
...trimHistory(normalizeHistory(messages), HISTORY_LIMIT)
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const settings = config.chat || {};
|
|
233
|
+
const providerAuth = await resolveProviderAuth(provider);
|
|
234
|
+
|
|
235
|
+
let finalText = '';
|
|
236
|
+
let reasoningText = '';
|
|
237
|
+
let totalUsage = null;
|
|
238
|
+
const toolLog = [];
|
|
239
|
+
const files = [];
|
|
240
|
+
let aborted = false;
|
|
241
|
+
|
|
242
|
+
const emit = (event) => {
|
|
243
|
+
try {
|
|
244
|
+
onEvent?.(event);
|
|
245
|
+
} catch {
|
|
246
|
+
/* 渲染层异常不应打断对话 */
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const maxSteps = useTools ? MAX_STEPS_WORK : MAX_STEPS_CHAT;
|
|
252
|
+
let continuationCount = 0;
|
|
253
|
+
|
|
254
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
255
|
+
emit({ type: 'status', text: step === 0 ? 'Working' : 'Continuing' });
|
|
256
|
+
const result = await streamChat({
|
|
257
|
+
protocol: provider.protocol || 'openai',
|
|
258
|
+
baseUrl: provider.baseUrl,
|
|
259
|
+
apiKey: providerAuth.apiKey,
|
|
260
|
+
authType: providerAuth.authType,
|
|
261
|
+
model: targetModel,
|
|
262
|
+
messages: apiMessages,
|
|
263
|
+
tools: toolDefs,
|
|
264
|
+
temperature: settings.temperature,
|
|
265
|
+
maxTokens: settings.maxTokens,
|
|
266
|
+
stream: settings.stream !== false,
|
|
267
|
+
signal,
|
|
268
|
+
onDelta: (text) => {
|
|
269
|
+
finalText += text;
|
|
270
|
+
emit({ type: 'delta', text });
|
|
271
|
+
},
|
|
272
|
+
onReasoning: (text) => {
|
|
273
|
+
reasoningText += text;
|
|
274
|
+
emit({ type: 'reasoning', text });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (result.usage) totalUsage = result.usage;
|
|
279
|
+
|
|
280
|
+
if (Array.isArray(result.toolCalls) && result.toolCalls.length) {
|
|
281
|
+
const calls = result.toolCalls
|
|
282
|
+
.filter((call) => call?.function?.name)
|
|
283
|
+
.map((call, index) => ({
|
|
284
|
+
id: call.id || `call_${step}_${index}`,
|
|
285
|
+
type: 'function',
|
|
286
|
+
function: { name: call.function.name, arguments: call.function.arguments || '{}' }
|
|
287
|
+
}));
|
|
288
|
+
if (!calls.length) break;
|
|
289
|
+
|
|
290
|
+
apiMessages.push({ role: 'assistant', content: result.content || null, tool_calls: calls });
|
|
291
|
+
|
|
292
|
+
for (const call of calls) {
|
|
293
|
+
const toolId = `t_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
294
|
+
const name = call.function.name;
|
|
295
|
+
emit({ type: 'tool', id: toolId, name, status: 'running', args: call.function.arguments });
|
|
296
|
+
let output = '';
|
|
297
|
+
let ok = true;
|
|
298
|
+
let lastReview = null;
|
|
299
|
+
try {
|
|
300
|
+
const args = call.function.arguments?.trim() ? JSON.parse(call.function.arguments) : {};
|
|
301
|
+
const requestApproval = async (operation) => {
|
|
302
|
+
const decision = await requestOperationApproval({
|
|
303
|
+
config,
|
|
304
|
+
permission: effectivePermission,
|
|
305
|
+
kind: 'tool',
|
|
306
|
+
name: operation.name,
|
|
307
|
+
args: operation.args,
|
|
308
|
+
workspace: activeWorkspace,
|
|
309
|
+
providerId: provider.id,
|
|
310
|
+
toolId,
|
|
311
|
+
askUser,
|
|
312
|
+
emit
|
|
313
|
+
});
|
|
314
|
+
lastReview = decision;
|
|
315
|
+
return decision.allow;
|
|
316
|
+
};
|
|
317
|
+
output = await tools.execute(name, args, {
|
|
318
|
+
permission: effectivePermission,
|
|
319
|
+
workspace: activeWorkspace,
|
|
320
|
+
requestApproval,
|
|
321
|
+
onFile: (file) => {
|
|
322
|
+
files.push(file);
|
|
323
|
+
emit({ type: 'file', ...file });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
ok = false;
|
|
328
|
+
output = `Error: ${err.message}`;
|
|
329
|
+
}
|
|
330
|
+
const logItem = {
|
|
331
|
+
id: toolId,
|
|
332
|
+
name,
|
|
333
|
+
args: call.function.arguments,
|
|
334
|
+
ok,
|
|
335
|
+
output,
|
|
336
|
+
review: lastReview
|
|
337
|
+
? {
|
|
338
|
+
source: lastReview.allow ? lastReview.source : 'denied',
|
|
339
|
+
decision: lastReview.review?.decision || (lastReview.allow ? 'allow' : 'review'),
|
|
340
|
+
reason: lastReview.review?.reason || lastReview.reason || ''
|
|
341
|
+
}
|
|
342
|
+
: undefined,
|
|
343
|
+
at: Date.now()
|
|
344
|
+
};
|
|
345
|
+
toolLog.push(logItem);
|
|
346
|
+
emit({
|
|
347
|
+
type: 'tool',
|
|
348
|
+
id: toolId,
|
|
349
|
+
name,
|
|
350
|
+
status: ok ? 'ok' : 'error',
|
|
351
|
+
output: output.slice(0, 4000),
|
|
352
|
+
review: logItem.review
|
|
353
|
+
});
|
|
354
|
+
apiMessages.push({ role: 'tool', tool_call_id: call.id, content: output });
|
|
355
|
+
}
|
|
356
|
+
if (finalText) {
|
|
357
|
+
finalText += '\n\n';
|
|
358
|
+
emit({ type: 'delta', text: '\n\n' });
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 工作模式:模型提前收尾时自动推进一步,避免任务只做了一半
|
|
364
|
+
if (
|
|
365
|
+
resolvedMode === 'work' &&
|
|
366
|
+
useTools &&
|
|
367
|
+
continuationCount < MAX_CONTINUATIONS &&
|
|
368
|
+
step < maxSteps - 1 &&
|
|
369
|
+
looksUnfinishedWork(result)
|
|
370
|
+
) {
|
|
371
|
+
continuationCount += 1;
|
|
372
|
+
if (result.content) apiMessages.push({ role: 'assistant', content: result.content });
|
|
373
|
+
apiMessages.push({
|
|
374
|
+
role: 'user',
|
|
375
|
+
content:
|
|
376
|
+
'Continue until the task is fully done. Do not describe a plan or promise a next step — call the tools to finish the remaining work, verify the result, then give a short summary.'
|
|
377
|
+
});
|
|
378
|
+
if (finalText) {
|
|
379
|
+
finalText += '\n\n';
|
|
380
|
+
emit({ type: 'delta', text: '\n\n' });
|
|
381
|
+
}
|
|
382
|
+
emit({ type: 'status', text: 'Continuing the task' });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
emit({ type: 'done', text: finalText, usage: totalUsage });
|
|
390
|
+
return { ok: true, text: finalText, reasoning: reasoningText, usage: totalUsage, toolLog, files };
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (err?.name === 'AbortError' || signal?.aborted) {
|
|
393
|
+
aborted = true;
|
|
394
|
+
emit({ type: 'aborted' });
|
|
395
|
+
return { ok: false, aborted: true, text: finalText, reasoning: reasoningText, usage: totalUsage, toolLog, files };
|
|
396
|
+
}
|
|
397
|
+
emit({ type: 'error', message: err.message });
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: err.message,
|
|
401
|
+
text: finalText,
|
|
402
|
+
reasoning: reasoningText,
|
|
403
|
+
usage: totalUsage,
|
|
404
|
+
toolLog,
|
|
405
|
+
files
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = {
|
|
411
|
+
HISTORY_LIMIT,
|
|
412
|
+
MAX_STEPS_CHAT,
|
|
413
|
+
MAX_STEPS_WORK,
|
|
414
|
+
requestOperationApproval,
|
|
415
|
+
describeOperation,
|
|
416
|
+
providerOf,
|
|
417
|
+
modelOf,
|
|
418
|
+
resolvePermissions,
|
|
419
|
+
runAgent
|
|
420
|
+
};
|