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,395 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 流式渲染:把 agent 事件流翻译成终端输出。
5
+ *
6
+ * 终端兼容性:只用 `\r` 回行首、空格擦尾、`\b` 退光标这些最基础能力,
7
+ * 不依赖光标上移(部分 Windows 控制台会忽略)。
8
+ *
9
+ * 三段输出:
10
+ * 1. 思考:实时显示计时 + 思考内容(超长时只保留末尾几行),结束后折叠成一行摘要;
11
+ * 2. 正文:流式期间只在最后一行刷新预览,结束时一次性按 Markdown 排版;
12
+ * 3. 工具:单行状态 + 结果摘要。
13
+ */
14
+
15
+ const { COLORS, SIGN, displayWidth, truncate, termWidth, renderMarkdown } = require('./ansi');
16
+ const { t } = require('./i18n');
17
+
18
+ const TOOL_KEYS = {
19
+ list_directory: 'tool_list',
20
+ read_file: 'tool_read',
21
+ write_file: 'tool_write',
22
+ create_directory: 'tool_mkdir',
23
+ delete_path: 'tool_delete',
24
+ run_command: 'tool_command',
25
+ fetch_url: 'tool_fetch',
26
+ install_mcp_server: 'tool_mcp'
27
+ };
28
+
29
+ function toolSummary(name, argsRaw) {
30
+ let args = {};
31
+ try {
32
+ args = argsRaw ? JSON.parse(argsRaw) : {};
33
+ } catch {
34
+ args = {};
35
+ }
36
+ const title = t(TOOL_KEYS[name] || name);
37
+ const target = args.path || args.url || args.command || args.name || '';
38
+ return target
39
+ ? `${title} ${truncate(String(target).replace(/\s+/g, ' '), Math.max(20, termWidth() - 24))}`
40
+ : title;
41
+ }
42
+
43
+ /** 按显示宽度折行(纯文本预览用) */
44
+ function foldLine(text, width) {
45
+ const limit = Math.max(20, width);
46
+ if (displayWidth(text) <= limit) return [text];
47
+ const out = [];
48
+ let current = '';
49
+ let currentWidth = 0;
50
+ for (const char of text) {
51
+ const w = displayWidth(char);
52
+ if (currentWidth + w > limit) {
53
+ out.push(current);
54
+ current = '';
55
+ currentWidth = 0;
56
+ }
57
+ current += char;
58
+ currentWidth += w;
59
+ }
60
+ if (current) out.push(current);
61
+ return out.length ? out : [''];
62
+ }
63
+
64
+ function formatSeconds(ms) {
65
+ const seconds = ms / 1000;
66
+ return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
67
+ }
68
+
69
+ class StreamRenderer {
70
+ constructor(options = {}) {
71
+ this.out = options.output || process.stdout;
72
+ this.editor = options.editor || null;
73
+ const tty = Boolean(this.out.isTTY);
74
+ this.live = options.redraw !== undefined ? Boolean(options.redraw) : tty;
75
+ this.width = Math.max(20, (options.width || termWidth()) - 1);
76
+ /** 是否显示思考内容(默认 true;DGXAY_SHOW_REASONING=0 可关闭) */
77
+ this.showReasoning = process.env.DGXAY_SHOW_REASONING !== '0';
78
+ /** 思考内容最多显示几行 */
79
+ this.reasoningLines = Number(process.env.DGXAY_REASONING_LINES) || 6;
80
+ this.timer = null;
81
+ this.reset();
82
+ }
83
+
84
+ reset() {
85
+ this.started = false;
86
+ this.text = '';
87
+ this.onScreen = '';
88
+ this.newlines = 0;
89
+ this.tools = new Map();
90
+
91
+ this.reasoning = '';
92
+ this.reasoningStart = 0;
93
+ this.reasoningMs = 0;
94
+ this.reasoningRows = 0;
95
+ this.reasoningDone = false;
96
+ this.suspended = false;
97
+ this.statusShown = false;
98
+ }
99
+
100
+ /* ------------------------------ 编辑器协调 ------------------------------ */
101
+
102
+ suspend() {
103
+ if (this.suspended) return;
104
+ this.editor?.suspend();
105
+ this.suspended = true;
106
+ }
107
+
108
+ resume() {
109
+ if (!this.suspended) return;
110
+ this.suspended = false;
111
+ this.editor?.resume();
112
+ }
113
+
114
+ /* ------------------------------ 基础绘制 ------------------------------ */
115
+
116
+ repaintLine(text) {
117
+ const visible = displayWidth(text);
118
+ const previous = displayWidth(this.onScreen);
119
+ const pad = Math.max(0, previous - visible);
120
+ this.out.write(`\r${text}${' '.repeat(pad)}`);
121
+ if (pad) this.out.write('\b'.repeat(pad));
122
+ this.onScreen = text;
123
+ }
124
+
125
+ breakLine() {
126
+ this.out.write('\n');
127
+ this.onScreen = '';
128
+ }
129
+
130
+ ensureHeader() {
131
+ if (this.started) return;
132
+ this.started = true;
133
+ this.suspend();
134
+ this.out.write(`\n${COLORS.accent('•')} ${COLORS.bold(t('assistant'))}\n`);
135
+ this.onScreen = '';
136
+ }
137
+
138
+ /* ------------------------------ 思考 ------------------------------ */
139
+
140
+ /** 开始思考:立刻给出提示,用户不用干等 */
141
+ beginReasoning() {
142
+ this.ensureHeader();
143
+ if (this.reasoningStart) return;
144
+ this.reasoningStart = Date.now();
145
+ this.reasoningDone = false;
146
+ this.reasoningRows = 0;
147
+ this.paintReasoning();
148
+ // 每 100ms 刷新一次计时,让用户看到“还在跑”
149
+ this.timer = setInterval(() => this.paintReasoning(), 100);
150
+ this.timer.unref?.();
151
+ }
152
+
153
+ /**
154
+ * 把已收到的思考内容画出来(末尾若干行),并带上实时计时。
155
+ * 只用 \r + 空格覆盖当前物理行,不依赖光标上移:
156
+ * - 计时和最新一行思考内容合并在同一行刷新;
157
+ * - 更早的思考内容不逐行打印,避免需要回退光标。
158
+ */
159
+ paintReasoning() {
160
+ if (!this.reasoningStart || this.reasoningDone) return;
161
+ if (!this.live) return;
162
+ const elapsed = Date.now() - this.reasoningStart;
163
+ const width = Math.max(20, this.width - 6);
164
+
165
+ if (!this.showReasoning) {
166
+ this.repaintLine(COLORS.faint(` ${SIGN.run} ${t('thinking')} ${formatSeconds(elapsed)}`));
167
+ return;
168
+ }
169
+
170
+ // 最新一行思考内容(把换行压成空格,保持单行刷新)
171
+ const latest = this.reasoning.replace(/\s+/g, ' ').trim();
172
+ const tailText = latest.length > width ? `…${latest.slice(-width)}` : latest;
173
+ const head = `${COLORS.faint(` ${SIGN.run} ${t('thinking')} ${formatSeconds(elapsed)}`)}`;
174
+ this.repaintLine(tailText ? `${head} ${COLORS.faint(tailText)}` : head);
175
+ }
176
+
177
+ /** 思考结束:当前位置就是摘要行,直接覆盖即可,不需要上移光标 */
178
+ endReasoning() {
179
+ if (!this.reasoningStart || this.reasoningDone) return;
180
+ this.reasoningDone = true;
181
+ this.reasoningMs = Date.now() - this.reasoningStart;
182
+ if (this.timer) {
183
+ clearInterval(this.timer);
184
+ this.timer = null;
185
+ }
186
+ if (this.live) {
187
+ // 覆盖掉“思考中”的实时行
188
+ this.repaintLine('');
189
+ this.out.write('\r');
190
+ this.onScreen = '';
191
+ }
192
+ if (this.reasoning.trim()) {
193
+ this.out.write(
194
+ `${COLORS.faint(` ⌁ ${t('thought_for')} ${formatSeconds(this.reasoningMs)} · ${this.reasoning.length} ${t('chars')}`)}\n`
195
+ );
196
+ // 详细内容用灰色缩进段落呈现,最多若干行,避免刷屏
197
+ if (this.showReasoning) {
198
+ const width = Math.max(20, this.width - 4);
199
+ const lines = [];
200
+ for (const line of this.reasoning.split('\n')) {
201
+ for (const piece of foldLine(line, width)) lines.push(piece);
202
+ }
203
+ const max = Math.max(1, this.reasoningLines);
204
+ const shown = lines.length > max ? lines.slice(-max) : lines;
205
+ if (lines.length > max) this.out.write(`${COLORS.faint(' │ …')}\n`);
206
+ for (const piece of shown) {
207
+ if (piece.trim()) this.out.write(`${COLORS.faint(` │ ${piece}`)}\n`);
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ /* ------------------------------ 正文 ------------------------------ */
214
+
215
+ writeText(text) {
216
+ this.endReasoning();
217
+ this.ensureHeader();
218
+ this.text += text;
219
+ this.newlines += (text.match(/\n/g) || []).length;
220
+ if (!this.live) return;
221
+
222
+ const lines = this.text.split('\n');
223
+ const tail = lines[lines.length - 1];
224
+ const folded = foldLine(tail, this.width);
225
+ this.repaintLine(folded[folded.length - 1]);
226
+ }
227
+
228
+ finalizeText() {
229
+ if (!this.text) {
230
+ this.onScreen = '';
231
+ return;
232
+ }
233
+ if (this.live && this.onScreen) this.repaintLine('');
234
+ const rendered = renderMarkdown(this.text, { width: this.width });
235
+ if (rendered && rendered.trim()) {
236
+ this.out.write(rendered);
237
+ if (!rendered.endsWith('\n')) this.breakLine();
238
+ }
239
+ this.text = '';
240
+ this.newlines = 0;
241
+ this.onScreen = '';
242
+ }
243
+
244
+ /* ------------------------------ 事件分发 ------------------------------ */
245
+
246
+ handle(event) {
247
+ switch (event.type) {
248
+ case 'status':
249
+ return;
250
+ case 'reasoning':
251
+ if (!event.text) return;
252
+ this.reasoning += event.text;
253
+ this.beginReasoning();
254
+ if (this.showReasoning && this.live) this.paintReasoning();
255
+ return;
256
+ case 'delta':
257
+ if (!event.text) return;
258
+ this.writeText(event.text);
259
+ return;
260
+ case 'tool':
261
+ this.onTool(event);
262
+ return;
263
+ case 'review-start':
264
+ this.endInline();
265
+ this.out.write(`${COLORS.faint(` ${SIGN.run} ${t('reviewing')} ${event.name}…`)}\n`);
266
+ return;
267
+ case 'review':
268
+ this.endInline();
269
+ if (event.decision === 'allow') {
270
+ this.out.write(
271
+ `${COLORS.green(` ${SIGN.ok}`)} ${COLORS.green(t('review_allow'))} ${COLORS.faint(
272
+ `${event.reviewer || ''} · ${event.reason || ''}`
273
+ )}\n`
274
+ );
275
+ } else {
276
+ this.out.write(
277
+ `${COLORS.yellow(` ${SIGN.run}`)} ${COLORS.yellow(t('review_suspect'))} ${COLORS.faint(event.reason || '')}\n`
278
+ );
279
+ }
280
+ return;
281
+ case 'waiting':
282
+ this.endInline();
283
+ this.out.write(`${COLORS.faint(` ${SIGN.run} ${t('waiting_confirm')} ${event.name}…`)}\n`);
284
+ return;
285
+ case 'file':
286
+ this.endInline();
287
+ this.out.write(
288
+ `${COLORS.green(` ${SIGN.ok}`)} ${COLORS.dim(
289
+ event.action === 'deleted'
290
+ ? t('file_deleted')
291
+ : event.action === 'updated'
292
+ ? t('file_updated')
293
+ : t('file_created')
294
+ )} ${COLORS.cyan(event.display || event.path)}${event.bytes ? COLORS.faint(` (${event.bytes} B)`) : ''}\n`
295
+ );
296
+ return;
297
+ case 'done':
298
+ case 'error':
299
+ case 'aborted':
300
+ this.finish(event);
301
+ return;
302
+ default:
303
+ return;
304
+ }
305
+ }
306
+
307
+ endInline() {
308
+ this.endReasoning();
309
+ if (this.text) this.finalizeText();
310
+ if (this.onScreen) this.breakLine();
311
+ }
312
+
313
+ onTool(event) {
314
+ this.endInline();
315
+ const record = this.tools.get(event.id) || {};
316
+ if (event.args) record.args = event.args;
317
+ const summary = record.summary || toolSummary(event.name, event.args || record.args);
318
+ this.tools.set(event.id, { ...record, name: event.name, status: event.status, summary });
319
+
320
+ if (event.status === 'running') {
321
+ this.out.write(`${COLORS.faint(` ${SIGN.run} ${summary}…`)}\n`);
322
+ return;
323
+ }
324
+ const detail = this.toolDetail(event);
325
+ if (event.status === 'ok') {
326
+ this.out.write(
327
+ `${COLORS.green(` ${SIGN.ok}`)} ${COLORS.dim(summary)}${detail ? COLORS.faint(` · ${detail}`) : ''}\n`
328
+ );
329
+ } else {
330
+ const firstLine = String(event.output || '')
331
+ .split('\n')
332
+ .find((line) => line.trim() && !line.startsWith('$ '));
333
+ this.out.write(
334
+ `${COLORS.red(` ${SIGN.fail}`)} ${COLORS.dim(summary)}${
335
+ firstLine ? COLORS.red(` · ${truncate(firstLine, 80)}`) : ''
336
+ }\n`
337
+ );
338
+ }
339
+ if (event.review?.reason) {
340
+ const source = event.review.source === 'auto' ? t('reviewing') : t('approval_op');
341
+ this.out.write(`${COLORS.faint(` ${source} · ${event.review.reason}`)}\n`);
342
+ }
343
+ }
344
+
345
+ toolDetail(event) {
346
+ const output = String(event.output || '');
347
+ if (event.name === 'read_file') return `${output.length} ${t('chars')}`;
348
+ if (event.name === 'run_command') {
349
+ const exit = output.match(/\[exit\] code=(\S+)/);
350
+ if (exit) return `${t('exit_code')} ${exit[1]}`;
351
+ return `${output.split('\n').filter(Boolean).length} ${t('lines')}`;
352
+ }
353
+ if (event.name === 'list_directory') return `${output.split('\n').filter(Boolean).length} ${t('items')}`;
354
+ if (event.name === 'fetch_url') {
355
+ const match = output.match(/^HTTP (\d{3})/);
356
+ return match ? `HTTP ${match[1]}` : '';
357
+ }
358
+ if (event.name === 'install_mcp_server') return 'saved';
359
+ return '';
360
+ }
361
+
362
+ finish(event) {
363
+ const hadText = Boolean(this.text.trim());
364
+ this.endReasoning();
365
+
366
+ if (event.type === 'error') {
367
+ this.endInline();
368
+ this.out.write(`\n${COLORS.red(`${SIGN.fail} ${t('generation_error')}: ${event.message}`)}\n`);
369
+ } else if (event.type === 'aborted') {
370
+ this.finalizeText();
371
+ this.out.write(`${COLORS.yellow(`${SIGN.bullet} ${t('stopped')}`)}\n`);
372
+ } else {
373
+ this.finalizeText();
374
+ if (!hadText && !this.tools.size && !this.reasoning.trim()) {
375
+ this.out.write(`${COLORS.faint(t('no_content'))}\n`);
376
+ }
377
+ }
378
+
379
+ if (event.usage) {
380
+ const usage = event.usage;
381
+ const input = usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? null;
382
+ const output = usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? null;
383
+ if (input != null || output != null) {
384
+ this.out.write(`${COLORS.faint(` ${input ?? '?'} ${t('in_out')} ${output ?? '?'} ${t('out')}`)}\n`);
385
+ }
386
+ }
387
+ if (this.timer) {
388
+ clearInterval(this.timer);
389
+ this.timer = null;
390
+ }
391
+ this.resume();
392
+ }
393
+ }
394
+
395
+ module.exports = { StreamRenderer, toolSummary, foldLine, TOOL_KEYS, formatSeconds };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * REPL 的运行时状态。
5
+ * 配置以「每次操作重新读取」为主,但 UI 模式 / 权限 / 工作空间 / 模型在会话内可临时覆盖,
6
+ * 用 overrides 表示;用 cfg.patchConfig() 持久化修改后再刷新覆盖值。
7
+ */
8
+
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ function createState() {
13
+ return {
14
+ cwd: process.cwd(),
15
+ overrides: {},
16
+ runtime: {
17
+ mode: 'chat',
18
+ permission: 'normal',
19
+ providerId: '',
20
+ modelId: '',
21
+ skill: ''
22
+ },
23
+ chat: null,
24
+ streaming: false,
25
+ abortRequested: false,
26
+ terminalBusy: false,
27
+ requestController: null,
28
+ lastResponse: null,
29
+ pendingInsert: ''
30
+ };
31
+ }
32
+
33
+ function recentWorkspaces(config) {
34
+ const list = config?.work?.recentWorkspaces || [];
35
+ return list.filter((item) => typeof item === 'string' && item.trim());
36
+ }
37
+
38
+ function activeProvider(config) {
39
+ const providers = config?.providers || [];
40
+ return providers.find((p) => p.id === config?.activeProviderId) || providers[0] || null;
41
+ }
42
+
43
+ function activeModel(config) {
44
+ const provider = activeProvider(config);
45
+ if (!provider) return null;
46
+ const models = provider.models || [];
47
+ const model = models.find((m) => m.id === config?.activeModelId) || models[0];
48
+ if (!model) return null;
49
+ return {
50
+ id: model.id,
51
+ name: model.name || model.id,
52
+ providerId: provider.id,
53
+ providerName: provider.name,
54
+ protocol: provider.protocol || 'openai',
55
+ contextWindow: Number(model.contextWindow) || 0,
56
+ reviewModelId: provider.reviewModelId || ''
57
+ };
58
+ }
59
+
60
+ function workspaceOf(state, config) {
61
+ return state.cwd || config?.work?.workspace || os.homedir();
62
+ }
63
+
64
+ function workspaceLabel(workspace) {
65
+ if (!workspace) return '未选择';
66
+ const parts = String(workspace)
67
+ .split(/[\\/]/)
68
+ .filter(Boolean);
69
+ return parts[parts.length - 1] || workspace;
70
+ }
71
+
72
+ function relativePath(base, target) {
73
+ const rel = path.relative(base, target);
74
+ return rel === '' ? '.' : rel;
75
+ }
76
+
77
+ module.exports = {
78
+ createState,
79
+ recentWorkspaces,
80
+ activeProvider,
81
+ activeModel,
82
+ workspaceOf,
83
+ workspaceLabel,
84
+ relativePath
85
+ };