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.
Files changed (46) hide show
  1. package/LICENSE +674 -0
  2. package/README-zh.md +242 -0
  3. package/README.md +223 -2
  4. package/bin/dgxay.js +42 -0
  5. package/package.json +47 -4
  6. package/src/cli/ansi.js +539 -0
  7. package/src/cli/approval.js +186 -0
  8. package/src/cli/args.js +162 -0
  9. package/src/cli/command-table.js +52 -0
  10. package/src/cli/editor.js +828 -0
  11. package/src/cli/i18n.js +276 -0
  12. package/src/cli/index.js +228 -0
  13. package/src/cli/print.js +148 -0
  14. package/src/cli/render.js +395 -0
  15. package/src/cli/repl-state.js +85 -0
  16. package/src/cli/repl.js +1231 -0
  17. package/src/cli/slash.js +75 -0
  18. package/src/cli/ui.js +52 -0
  19. package/src/commands/_shared.js +20 -0
  20. package/src/commands/chat.js +212 -0
  21. package/src/commands/config.js +363 -0
  22. package/src/commands/context.js +43 -0
  23. package/src/commands/doctor.js +192 -0
  24. package/src/commands/export.js +102 -0
  25. package/src/commands/mcp.js +257 -0
  26. package/src/commands/model.js +223 -0
  27. package/src/commands/oauth.js +246 -0
  28. package/src/commands/permission.js +108 -0
  29. package/src/commands/provider.js +332 -0
  30. package/src/commands/run.js +172 -0
  31. package/src/commands/skill.js +94 -0
  32. package/src/commands/workspace.js +100 -0
  33. package/src/core/agent.js +420 -0
  34. package/src/core/llm.js +709 -0
  35. package/src/core/mcp.js +271 -0
  36. package/src/core/oauth.js +737 -0
  37. package/src/core/prompt.js +269 -0
  38. package/src/core/sessions.js +208 -0
  39. package/src/core/tools.js +391 -0
  40. package/src/services/config.js +182 -0
  41. package/src/services/input.js +91 -0
  42. package/src/services/session-bus.js +104 -0
  43. package/src/services/settings.js +340 -0
  44. package/src/services/skills.js +125 -0
  45. package/src/services/terminal.js +124 -0
  46. 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
+ };