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.
Files changed (45) hide show
  1. package/LICENSE +674 -0
  2. package/README-zh.md +200 -0
  3. package/README.md +182 -2
  4. package/bin/dgxay.js +42 -0
  5. package/package.json +47 -4
  6. package/src/cli/ansi.js +509 -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 +574 -0
  11. package/src/cli/index.js +228 -0
  12. package/src/cli/print.js +148 -0
  13. package/src/cli/render.js +324 -0
  14. package/src/cli/repl-state.js +85 -0
  15. package/src/cli/repl.js +1167 -0
  16. package/src/cli/slash.js +74 -0
  17. package/src/cli/ui.js +52 -0
  18. package/src/commands/_shared.js +20 -0
  19. package/src/commands/chat.js +212 -0
  20. package/src/commands/config.js +363 -0
  21. package/src/commands/context.js +43 -0
  22. package/src/commands/doctor.js +192 -0
  23. package/src/commands/export.js +102 -0
  24. package/src/commands/mcp.js +257 -0
  25. package/src/commands/model.js +223 -0
  26. package/src/commands/oauth.js +246 -0
  27. package/src/commands/permission.js +108 -0
  28. package/src/commands/provider.js +332 -0
  29. package/src/commands/run.js +172 -0
  30. package/src/commands/skill.js +94 -0
  31. package/src/commands/workspace.js +100 -0
  32. package/src/core/agent.js +419 -0
  33. package/src/core/llm.js +709 -0
  34. package/src/core/mcp.js +271 -0
  35. package/src/core/oauth.js +737 -0
  36. package/src/core/prompt.js +269 -0
  37. package/src/core/sessions.js +208 -0
  38. package/src/core/tools.js +391 -0
  39. package/src/services/config.js +182 -0
  40. package/src/services/input.js +91 -0
  41. package/src/services/session-bus.js +104 -0
  42. package/src/services/settings.js +340 -0
  43. package/src/services/skills.js +125 -0
  44. package/src/services/terminal.js +124 -0
  45. package/src/services/version.js +25 -0
@@ -0,0 +1,574 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 行编辑器:支持中文输入、历史、自动补全、多行输入,以及在流式输出期间中断。
5
+ *
6
+ * 终端兼容性(实测得出):部分 Windows 控制台会忽略 `\x1b[nA`(光标上移)、
7
+ * `\x1b[nC`(光标右移)等序列,凡是依赖它们做「擦除重画」的实现,
8
+ * 都会退化成「每敲一个字就多印一行」。所以这里只用最基础的能力:
9
+ * - `\r` 回到当前行行首;
10
+ * - 空格覆盖多余内容;
11
+ * - 需要时整行重写(单行内完成,绝不跨行)。
12
+ *
13
+ * 多行输入(行尾 `\`)不支持跨行光标定位,改为在提示行里用 ⏎ 标记显示换行,
14
+ * 提交时再还原成真正的换行,功能不减、显示稳定。
15
+ */
16
+
17
+ const readline = require('readline');
18
+ const { COLORS, displayWidth, truncate, termWidth, useUnicode, isInteractive } = require('./ansi');
19
+
20
+ class LineEditor {
21
+ constructor(options = {}) {
22
+ this.input = options.input || process.stdin;
23
+ this.output = options.output || process.stdout;
24
+ this.prompt = options.prompt || '> ';
25
+ this.history = Array.isArray(options.history) ? options.history.slice() : [];
26
+ this.historyIndex = this.history.length;
27
+ this.completer = options.completer || null;
28
+ this.multiline = options.multiline !== false;
29
+ this.maxHistory = options.maxHistory || 500;
30
+
31
+ this.value = '';
32
+ this.cursor = 0;
33
+ /** 当前提示行屏幕上的实际内容(含提示符),用于 \r 覆盖时擦尾巴 */
34
+ this.onScreen = '';
35
+ this.popup = null;
36
+ this.draft = '';
37
+ this.active = false;
38
+ this.finished = false;
39
+ this.resolveQuestion = null;
40
+ this.keyHandler = null;
41
+ this.keyQueue = Promise.resolve();
42
+ this.abortHandler = null;
43
+ this.newlineMark = useUnicode() ? '⏎' : '\\n';
44
+ }
45
+
46
+ /* ------------------------------ 生命周期 ------------------------------ */
47
+
48
+ setAbortHandler(handler) {
49
+ this.abortHandler = typeof handler === 'function' ? handler : null;
50
+ }
51
+
52
+ question(prompt) {
53
+ if (prompt !== undefined) this.prompt = prompt;
54
+ this.value = '';
55
+ this.cursor = 0;
56
+ this.popup = null;
57
+ this.onScreen = '';
58
+ this.historyIndex = this.history.length;
59
+ this.active = true;
60
+ this.finished = false;
61
+
62
+ return new Promise((resolve) => {
63
+ this.resolveQuestion = resolve;
64
+ readline.emitKeypressEvents(this.input);
65
+ if (this.input.isTTY) {
66
+ try {
67
+ this.input.setRawMode(true);
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ }
72
+ this.input.resume();
73
+
74
+ this.keyHandler = (str, key) => {
75
+ // 按键必须按顺序处理:handleKey 是异步的(Ctrl+C 可能等待中断回调),
76
+ // 用队列串行化,避免快速输入时状态错乱。
77
+ this.keyQueue = this.keyQueue.then(() => this.handleKey(str || '', key || {})).catch(() => {});
78
+ };
79
+ this.input.on('keypress', this.keyHandler);
80
+ this.repaint();
81
+ });
82
+ }
83
+
84
+ finish(result) {
85
+ if (this.finished) return;
86
+ this.finished = true;
87
+ this.active = false;
88
+ this.popup = null;
89
+ if (this.keyHandler) {
90
+ this.input.removeListener('keypress', this.keyHandler);
91
+ this.keyHandler = null;
92
+ }
93
+ if (this.input.isTTY) {
94
+ try {
95
+ this.input.setRawMode(false);
96
+ } catch {
97
+ /* ignore */
98
+ }
99
+ }
100
+ // 提交时保留用户看到的那一行,只把光标移到下一行开头
101
+ this.output.write('\n');
102
+ this.onScreen = '';
103
+ const resolve = this.resolveQuestion;
104
+ this.resolveQuestion = null;
105
+ if (resolve) resolve(result);
106
+ }
107
+
108
+ /** 流式输出前:把提示行清掉,让输出独占终端 */
109
+ suspend() {
110
+ this.popup = null;
111
+ this.clearLine();
112
+ }
113
+
114
+ /** 流式输出结束后:重新画出提示行 */
115
+ resume() {
116
+ if (!this.active) return;
117
+ this.onScreen = '';
118
+ this.repaint();
119
+ }
120
+
121
+ /* ------------------------------ 屏幕操作 ------------------------------ */
122
+
123
+ /** 用 \r + 空格覆盖当前物理行(不跨行、不用 ANSI 光标序列) */
124
+ writeLine(text) {
125
+ const visible = displayWidth(text);
126
+ const previous = displayWidth(this.onScreen);
127
+ const pad = Math.max(0, previous - visible);
128
+ this.output.write(`\r${text}${' '.repeat(pad)}`);
129
+ // 把光标退回行尾,方便下一次覆盖
130
+ if (pad) this.output.write('\b'.repeat(pad));
131
+ this.onScreen = text;
132
+ }
133
+
134
+ clearLine() {
135
+ if (!this.onScreen) {
136
+ this.output.write('\r');
137
+ return;
138
+ }
139
+ const width = displayWidth(this.onScreen);
140
+ this.output.write(`\r${' '.repeat(width)}\r`);
141
+ this.onScreen = '';
142
+ }
143
+
144
+ /* ------------------------------ 绘制 ------------------------------ */
145
+
146
+ /** 把内部文本转成单行显示:换行用 ⏎ 表示(部分终端不支持跨行光标定位) */
147
+ displayText() {
148
+ return this.value.replace(/\n/g, ` ${this.newlineMark} `);
149
+ }
150
+
151
+ /** 计算光标在显示文本中的位置 */
152
+ cursorDisplayIndex() {
153
+ const before = Array.from(this.value.slice(0, this.cursor));
154
+ let length = 0;
155
+ for (const char of before) {
156
+ if (char === '\n') length += 1 + this.newlineMark.length + 1;
157
+ else length += char.length;
158
+ }
159
+ return length;
160
+ }
161
+
162
+ repaint() {
163
+ if (!this.active) return;
164
+ const promptText = this.prompt;
165
+ const promptWidth = displayWidth(promptText);
166
+ const totalWidth = Math.max(20, termWidth() - 1);
167
+ const available = Math.max(10, totalWidth - promptWidth);
168
+
169
+ let text = this.displayText();
170
+ let cursor = this.cursorDisplayIndex();
171
+
172
+ // 超宽时横向滚动:保留光标前后一部分,避免行被终端自动折行(折行后无法定位)
173
+ let offset = 0;
174
+ if (displayWidth(text) > available) {
175
+ const beforeCursor = Array.from(text).slice(0, cursor);
176
+ const cursorWidth = displayWidth(beforeCursor.join(''));
177
+ if (cursorWidth > available - 4) {
178
+ // 从前面裁掉
179
+ let cut = 0;
180
+ let removed = 0;
181
+ const target = cursorWidth - (available - 8);
182
+ while (cut < beforeCursor.length && removed < target) {
183
+ removed += displayWidth(beforeCursor[cut]);
184
+ cut += 1;
185
+ }
186
+ const head = useUnicode() ? '…' : '...';
187
+ text = head + Array.from(text).slice(cut).join('');
188
+ offset = displayWidth(head);
189
+ cursor = cursor - removed + offset;
190
+ }
191
+ }
192
+
193
+ const clipped = truncate(text, Math.max(1, available));
194
+ this.writeLine(`${promptText}${clipped}`);
195
+
196
+ // 光标定位:从行尾往回退(依赖 \b,不依赖 ANSI 光标序列)
197
+ const endWidth = promptWidth + displayWidth(clipped);
198
+ const back = endWidth - (promptWidth + cursor);
199
+ if (back > 0) this.output.write('\b'.repeat(back));
200
+ }
201
+
202
+ /* ------------------------------ 补全弹层 ------------------------------ */
203
+
204
+ refreshPopup() {
205
+ if (!this.active) return;
206
+ const trigger = this.completer ? this.completer(this.value, this.cursor) : null;
207
+ if (!trigger || !trigger.items || !trigger.items.length) {
208
+ this.popup = null;
209
+ this.repaint();
210
+ return;
211
+ }
212
+ let index = 0;
213
+ if (this.popup && this.popup.items[this.popup.index]) {
214
+ const previous = this.popup.items[this.popup.index].apply || this.popup.items[this.popup.index].label;
215
+ const found = trigger.items.findIndex((item) => (item.apply || item.label) === previous);
216
+ if (found >= 0) index = found;
217
+ }
218
+ this.popup = { ...trigger, index };
219
+ this.repaint();
220
+ this.drawPopup();
221
+ }
222
+
223
+ /**
224
+ * 弹层只在下方的临时区域显示候选,输入提示行始终是最后一行。
225
+ * 由于部分终端不支持光标回退,这里不会把提示行往上搬,而是在下方列出候选后重画提示行。
226
+ */
227
+ drawPopup() {
228
+ if (!this.popup) return;
229
+ const { items, index, title } = this.popup;
230
+ if (!items.length) return;
231
+ const width = Math.min(64, termWidth() - 4);
232
+
233
+ // 先把提示行清掉,候选从空行开始打印
234
+ this.clearLine();
235
+ this.output.write('\n');
236
+ if (title) this.output.write(`${COLORS.faint(` ${title}`)}\n`);
237
+ const shown = items.slice(0, 12);
238
+ shown.forEach((item, i) => {
239
+ const active = i === index;
240
+ const label = truncate(item.label, width - 8);
241
+ const hintWidth = Math.max(0, width - 10 - displayWidth(label));
242
+ const hint = item.hint && hintWidth > 4 ? COLORS.faint(` ${truncate(item.hint, hintWidth)}`) : '';
243
+ this.output.write(`${active ? COLORS.accent('›') : ' '} ${active ? COLORS.accent(label) : label}${hint}\n`);
244
+ });
245
+ if (items.length > shown.length) {
246
+ this.output.write(COLORS.faint(` …还有 ${items.length - shown.length} 项\n`));
247
+ }
248
+
249
+ // 重画提示行,让它回到最下方
250
+ this.onScreen = '';
251
+ this.repaint();
252
+ }
253
+
254
+ closePopup() {
255
+ if (!this.popup) return;
256
+ this.popup = null;
257
+ this.repaint();
258
+ }
259
+
260
+ /* ------------------------------ 编辑 ------------------------------ */
261
+
262
+ insert(text) {
263
+ const chars = Array.from(this.value);
264
+ const insertChars = Array.from(text);
265
+ chars.splice(this.cursor, 0, ...insertChars);
266
+ this.cursor += insertChars.length;
267
+ this.value = chars.join('');
268
+ }
269
+
270
+ pushHistory(entry) {
271
+ const text = String(entry || '').trim();
272
+ if (!text) return;
273
+ if (this.history[this.history.length - 1] === text) return;
274
+ this.history.push(text);
275
+ if (this.history.length > this.maxHistory) this.history.splice(0, this.history.length - this.maxHistory);
276
+ this.historyIndex = this.history.length;
277
+ }
278
+
279
+ /* ------------------------------ 按键 ------------------------------ */
280
+
281
+ async handleKey(str, key) {
282
+ if (!this.active) return;
283
+ const ctrl = Boolean(key.ctrl);
284
+ const chars = Array.from(this.value);
285
+ const isSubmitKey = key.name === 'return' || key.name === 'enter' || str === '\r' || str === '\n';
286
+
287
+ /* --- 弹层激活 --- */
288
+ if (this.popup) {
289
+ const { items } = this.popup;
290
+ if (key.name === 'up') {
291
+ this.popup.index = (this.popup.index - 1 + items.length) % items.length;
292
+ this.drawPopup();
293
+ return;
294
+ }
295
+ if (key.name === 'down' || key.name === 'tab') {
296
+ this.popup.index = (this.popup.index + 1) % items.length;
297
+ this.drawPopup();
298
+ return;
299
+ }
300
+ if (key.name === 'escape') {
301
+ this.popup = null;
302
+ this.repaint();
303
+ return;
304
+ }
305
+ if (isSubmitKey) {
306
+ const item = items[this.popup.index];
307
+ this.popup = null;
308
+ if (item) {
309
+ // 注意:不能写 `this.completer.apply(...)` —— 那是 Function.prototype.apply,
310
+ // 永远存在,会把补全器的返回值吃成 undefined。这里只认显式的 applyItem 方法,
311
+ // 否则回落到 item 上的 apply 字符串。
312
+ let applied = null;
313
+ if (this.completer && typeof this.completer.applyItem === 'function') {
314
+ applied = this.completer.applyItem(this.value, this.cursor, item);
315
+ }
316
+ if (applied && typeof applied.value === 'string') {
317
+ this.value = applied.value;
318
+ this.cursor = Number.isInteger(applied.cursor) ? applied.cursor : Array.from(applied.value).length;
319
+ } else if (typeof item.apply === 'string') {
320
+ this.value = item.apply;
321
+ this.cursor = Array.from(item.apply).length;
322
+ } else if (typeof item.value === 'string') {
323
+ this.value = item.value;
324
+ this.cursor = Array.from(item.value).length;
325
+ }
326
+ this.repaint();
327
+ // 补全后把选择权交回用户:再按一次回车才提交
328
+ return;
329
+ }
330
+ }
331
+ }
332
+
333
+ /* --- 控制键 --- */
334
+ if (ctrl && key.name === 'c') {
335
+ if (this.value) {
336
+ this.value = '';
337
+ this.cursor = 0;
338
+ this.popup = null;
339
+ this.repaint();
340
+ return;
341
+ }
342
+ this.clearLine();
343
+ this.active = false;
344
+ if (this.keyHandler) {
345
+ this.input.removeListener('keypress', this.keyHandler);
346
+ this.keyHandler = null;
347
+ }
348
+ if (this.input.isTTY) {
349
+ try {
350
+ this.input.setRawMode(false);
351
+ } catch {
352
+ /* ignore */
353
+ }
354
+ }
355
+ this.output.write(`${COLORS.dim('^C')}\n`);
356
+ const handled = this.abortHandler ? await this.abortHandler() : false;
357
+ if (handled) {
358
+ this.active = true;
359
+ this.finished = false;
360
+ this.value = '';
361
+ this.cursor = 0;
362
+ this.onScreen = '';
363
+ if (this.input.isTTY) {
364
+ try {
365
+ this.input.setRawMode(true);
366
+ } catch {
367
+ /* ignore */
368
+ }
369
+ }
370
+ this.keyHandler = (s, k) => {
371
+ this.keyQueue = this.keyQueue.then(() => this.handleKey(s || '', k || {})).catch(() => {});
372
+ };
373
+ this.input.on('keypress', this.keyHandler);
374
+ this.repaint();
375
+ return;
376
+ }
377
+ this.finish({ type: 'cancel', value: '' });
378
+ return;
379
+ }
380
+
381
+ if (ctrl && key.name === 'd') {
382
+ if (!this.value) {
383
+ this.finish({ type: 'eof', value: '' });
384
+ return;
385
+ }
386
+ chars.splice(this.cursor, 1);
387
+ this.value = chars.join('');
388
+ this.repaint();
389
+ return;
390
+ }
391
+
392
+ if (ctrl && key.name === 'u') {
393
+ chars.splice(0, this.cursor);
394
+ this.value = chars.join('');
395
+ this.cursor = 0;
396
+ this.repaint();
397
+ return;
398
+ }
399
+
400
+ if (ctrl && key.name === 'k') {
401
+ chars.splice(this.cursor);
402
+ this.value = chars.join('');
403
+ this.repaint();
404
+ return;
405
+ }
406
+
407
+ if (ctrl && key.name === 'w') {
408
+ let i = this.cursor;
409
+ while (i > 0 && /\s/.test(chars[i - 1])) i -= 1;
410
+ while (i > 0 && !/\s/.test(chars[i - 1])) i -= 1;
411
+ chars.splice(i, this.cursor - i);
412
+ this.cursor = i;
413
+ this.value = chars.join('');
414
+ this.repaint();
415
+ return;
416
+ }
417
+
418
+ if (ctrl && key.name === 'a') {
419
+ this.cursor = 0;
420
+ this.repaint();
421
+ return;
422
+ }
423
+
424
+ if (ctrl && key.name === 'e') {
425
+ this.cursor = chars.length;
426
+ this.repaint();
427
+ return;
428
+ }
429
+
430
+ if (ctrl && key.name === 'l') {
431
+ this.output.write('\x1b[2J\x1b[H');
432
+ this.onScreen = '';
433
+ this.repaint();
434
+ return;
435
+ }
436
+
437
+ /* --- 回车 --- */
438
+ if (isSubmitKey) {
439
+ if (this.multiline && chars[this.cursor - 1] === '\\') {
440
+ chars.splice(this.cursor - 1, 1, '\n');
441
+ this.value = chars.join('');
442
+ this.repaint();
443
+ return;
444
+ }
445
+ this.submit();
446
+ return;
447
+ }
448
+
449
+ /* --- 光标与删除 --- */
450
+ if (key.name === 'backspace') {
451
+ if (this.cursor > 0) {
452
+ chars.splice(this.cursor - 1, 1);
453
+ this.cursor -= 1;
454
+ this.value = chars.join('');
455
+ this.refreshPopup();
456
+ }
457
+ return;
458
+ }
459
+ if (key.name === 'delete') {
460
+ if (this.cursor < chars.length) {
461
+ chars.splice(this.cursor, 1);
462
+ this.value = chars.join('');
463
+ this.refreshPopup();
464
+ }
465
+ return;
466
+ }
467
+ if (key.name === 'left') {
468
+ this.cursor = Math.max(0, this.cursor - 1);
469
+ this.popup = null;
470
+ this.repaint();
471
+ return;
472
+ }
473
+ if (key.name === 'right') {
474
+ this.cursor = Math.min(chars.length, this.cursor + 1);
475
+ this.popup = null;
476
+ this.repaint();
477
+ return;
478
+ }
479
+ if (key.name === 'home') {
480
+ this.cursor = 0;
481
+ this.repaint();
482
+ return;
483
+ }
484
+ if (key.name === 'end') {
485
+ this.cursor = chars.length;
486
+ this.repaint();
487
+ return;
488
+ }
489
+ if (key.name === 'up') {
490
+ if (!this.history.length) return;
491
+ if (this.historyIndex === this.history.length) this.draft = this.value;
492
+ this.historyIndex = Math.max(0, this.historyIndex - 1);
493
+ this.value = this.history[this.historyIndex] || '';
494
+ this.cursor = Array.from(this.value).length;
495
+ this.popup = null;
496
+ this.repaint();
497
+ return;
498
+ }
499
+ if (key.name === 'down') {
500
+ if (this.historyIndex >= this.history.length) return;
501
+ this.historyIndex = Math.min(this.history.length, this.historyIndex + 1);
502
+ this.value = this.historyIndex === this.history.length ? this.draft : this.history[this.historyIndex] || '';
503
+ this.cursor = Array.from(this.value).length;
504
+ this.popup = null;
505
+ this.repaint();
506
+ return;
507
+ }
508
+ if (key.name === 'tab') {
509
+ this.refreshPopup();
510
+ return;
511
+ }
512
+ if (key.name === 'escape') {
513
+ return;
514
+ }
515
+
516
+ /* --- 普通字符 --- */
517
+ if (str && !ctrl && !key.meta) {
518
+ const filtered = String(str).replace(/[\r\n]/g, '');
519
+ if (filtered) {
520
+ this.insert(filtered);
521
+ this.repaint();
522
+ }
523
+ }
524
+ }
525
+
526
+ submit() {
527
+ const value = this.value;
528
+ this.value = '';
529
+ this.cursor = 0;
530
+ this.popup = null;
531
+ this.finish({ type: 'submit', value });
532
+ }
533
+ }
534
+
535
+ /* ------------------------------ 非 TTY 回退 ------------------------------ */
536
+
537
+ /**
538
+ * 非 TTY(管道 / 重定向)用的极简行读取:不做任何光标控制。
539
+ */
540
+ function createLineReader(stream = process.stdin) {
541
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
542
+ const iterator = rl[Symbol.asyncIterator]();
543
+ let closed = false;
544
+ return {
545
+ async question() {
546
+ if (closed) return { type: 'eof', value: '' };
547
+ const { value, done } = await iterator.next();
548
+ if (done) {
549
+ closed = true;
550
+ rl.close();
551
+ return { type: 'eof', value: '' };
552
+ }
553
+ return { type: 'submit', value };
554
+ },
555
+ setAbortHandler() {},
556
+ history: [],
557
+ pushHistory() {},
558
+ suspend() {},
559
+ resume() {},
560
+ finish(result) {
561
+ closed = true;
562
+ rl.close();
563
+ return result;
564
+ },
565
+ get prompt() {
566
+ return '';
567
+ },
568
+ set prompt(value) {
569
+ void value;
570
+ }
571
+ };
572
+ }
573
+
574
+ module.exports = { LineEditor, createLineReader };