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,1231 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 交互模式(REPL):把桌面版的对话界面完整搬到终端。
5
+ *
6
+ * dgxay 进入交互模式(自动恢复最近会话)
7
+ * dgxay "问题" 单次对话
8
+ * dgxay -c / -r <id> 继续最近会话 / 指定会话
9
+ *
10
+ * 保留的桌面能力:会话列表与恢复、斜杠命令、技能、四档权限与自动审查、
11
+ * 上下文压缩、`!` 终端、模型/供应商切换、MCP 管理、OAuth、工作空间选择。
12
+ * 去掉的只有 UI:上下文圆环改为状态行 + /context,弹窗改为终端里的确认框。
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+
19
+ const { COLORS, SIGN, termWidth, setColorMode, colorEnabled, displayWidth, clearScreen, useUnicode } = require('./ansi');
20
+ const { t } = require('./i18n');
21
+ const { LineEditor, createLineReader, detectCapabilities, wrapDisplay } = require('./editor');
22
+ const { parseArgs } = require('./args');
23
+ const print = require('./print');
24
+ const { askApproval } = require('./approval');
25
+ const { SLASH_COMMANDS, findCommand, completer } = require('./slash');
26
+ const { StreamRenderer } = require('./render');
27
+ const { contextInfo, ringLine, contextReport } = require('./ui');
28
+ const state = require('./repl-state');
29
+
30
+ const sessions = require('../core/sessions');
31
+ const settings = require('../services/settings');
32
+ const cfg = require('../services/config');
33
+ const skills = require('../services/skills');
34
+ const mcp = require('../core/mcp');
35
+ const oauth = require('../core/oauth');
36
+ const terminal = require('../services/terminal');
37
+ const sessionBus = require('../services/session-bus');
38
+ const { runAgent, HISTORY_LIMIT } = require('../core/agent');
39
+ const { compressMessages } = require('../core/prompt');
40
+ const { VERSION, BIN } = require('../services/version');
41
+
42
+ const GLOBAL_REPL_OPTIONS = [
43
+ { name: 'print', alias: 'p', type: 'boolean' },
44
+ { name: 'prompt', type: 'string' },
45
+ { name: 'mode', alias: 'm', type: 'string' },
46
+ { name: 'permission', type: 'string' },
47
+ { name: 'workspace', alias: 'C', type: 'string' },
48
+ { name: 'model', type: 'string' },
49
+ { name: 'provider', type: 'string' },
50
+ { name: 'skill', type: 'string' },
51
+ { name: 'resume', alias: 'r', type: 'string' },
52
+ { name: 'continue', alias: 'c', type: 'boolean' },
53
+ { name: 'output', alias: 'o', type: 'string' },
54
+ { name: 'yes', alias: 'y', type: 'boolean' }
55
+ ];
56
+
57
+ function createRuntime(options = {}) {
58
+ const st = state.createState();
59
+ const config = settings.readConfig();
60
+ const model = settings.activeModel(config);
61
+ const provider = settings.activeProvider(config);
62
+
63
+ st.runtime.mode = ['chat', 'work'].includes(options.mode) ? options.mode : settings.currentMode(config);
64
+ st.runtime.permission = settings.PERMISSIONS[options.permission]
65
+ ? options.permission
66
+ : settings.currentPermission(config);
67
+ if (st.runtime.permission === 'auto' && !settings.hasReviewModel(config)) st.runtime.permission = 'normal';
68
+ st.runtime.modelId = options.model || model?.id || '';
69
+ st.runtime.providerId = options.provider
70
+ ? settings.findProvider(options.provider)?.id || provider?.id || ''
71
+ : provider?.id || '';
72
+ st.runtime.skill = String(options.skill || '');
73
+
74
+ if (options.workspace) {
75
+ const resolved = path.resolve(options.workspace);
76
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) st.cwd = resolved;
77
+ else throw Object.assign(new Error(`Workspace not found: ${options.workspace}`), { userError: true });
78
+ } else if (config.work?.workspace && fs.existsSync(config.work.workspace)) {
79
+ st.cwd = config.work.workspace;
80
+ } else {
81
+ st.cwd = process.cwd();
82
+ }
83
+
84
+ if (options.resume) {
85
+ st.chat = sessions.getChat(String(options.resume));
86
+ if (!st.chat) throw Object.assign(new Error(`Conversation not found: ${options.resume}`), { userError: true });
87
+ } else if (options.continueLast) {
88
+ st.chat = sessions.listChats()[0] || null;
89
+ }
90
+
91
+ st.yes = Boolean(options.yes);
92
+ return st;
93
+ }
94
+
95
+ /* ------------------------------ 展示 ------------------------------ */
96
+
97
+ function permissionBadge(st) {
98
+ const tone =
99
+ st.runtime.permission === 'full' ? COLORS.red : st.runtime.permission === 'auto' ? COLORS.green : COLORS.faint;
100
+ return tone(t(`perm_${st.runtime.permission}`));
101
+ }
102
+
103
+ function modeBadge(st) {
104
+ return st.runtime.mode === 'work' ? COLORS.accent(t('mode_work')) : COLORS.faint(t('mode_chat'));
105
+ }
106
+
107
+ /** 输入框上方的一行说明(模型 / 技能) */
108
+ function promptFor(st) {
109
+ const model = settings.activeModel(settings.readConfig());
110
+ const name = model?.name || t('no_model');
111
+ const skill = st.runtime.skill ? ` · ⛭ ${st.runtime.skill}` : '';
112
+ return `${name}${skill}`;
113
+ }
114
+
115
+ function showStatus(st, { withRing = true } = {}) {
116
+ const chat = st.chat;
117
+ const model = settings.activeModel(settings.readConfig());
118
+ const info = contextInfo(chat, model);
119
+ const parts = [
120
+ modeBadge(st),
121
+ permissionBadge(st),
122
+ state.workspaceLabel(st.cwd),
123
+ `${chat ? chat.messages.filter((m) => m.role !== 'terminal').length : 0} ${t('messages')}`
124
+ ];
125
+ const left = parts.filter(Boolean).join(COLORS.faint(' · '));
126
+ const right = withRing ? ringLine(info) : '';
127
+ const width = termWidth();
128
+ const gap = Math.max(2, width - stripLen(left) - stripLen(right) - 2);
129
+ process.stdout.write(`\n${left}${' '.repeat(gap)}${right}\n\n`);
130
+ }
131
+
132
+ function stripLen(text) {
133
+ return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '').length;
134
+ }
135
+
136
+ function showWelcome(st) {
137
+ require('./index').showBanner();
138
+ const config = settings.readConfig();
139
+ const model = settings.activeModel(config);
140
+ const chat = st.chat;
141
+ if (chat && chat.messages.length) {
142
+ print.hint(
143
+ `${t('restored')} "${chat.title}" (${chat.messages.filter((m) => m.role !== 'terminal').length} ${t('messages')})`
144
+ );
145
+ }
146
+ print.keyValue([
147
+ {
148
+ label: t('status_model'),
149
+ value: model ? `${model.providerName} / ${model.name}` : COLORS.yellow(t('no_model'))
150
+ },
151
+ { label: t('status_workspace'), value: st.cwd },
152
+ {
153
+ label: t('status_permission'),
154
+ value: `${t(`perm_${st.runtime.permission}`)} · ${settings.PERMISSIONS[st.runtime.permission].desc}`
155
+ },
156
+ { label: t('status_mode'), value: st.runtime.mode === 'work' ? t('mode_work') : t('mode_chat') }
157
+ ]);
158
+ print.blank();
159
+ print.hint(t('hint_start'));
160
+ print.blank();
161
+ void chat;
162
+ }
163
+
164
+ /* ------------------------------ 消息与历史 ------------------------------ */
165
+
166
+ function ensureChat(st) {
167
+ if (st.chat) return st.chat;
168
+ const chat = sessions.createChat({ mode: st.runtime.mode, skill: st.runtime.skill });
169
+ st.chat = chat;
170
+ return chat;
171
+ }
172
+
173
+ function pushMessage(chat, message) {
174
+ chat.messages.push({ ts: Date.now(), ...message });
175
+ chat.updatedAt = Date.now();
176
+ }
177
+
178
+ function persist(st) {
179
+ if (!st.chat) return;
180
+ const chat = st.chat;
181
+ if (!chat.messages.length) return;
182
+ if (!chat.title || chat.title === 'New conversation') {
183
+ const firstUser = chat.messages.find((m) => m.role === 'user' && m.content);
184
+ if (firstUser) chat.title = sessions.titleFrom(firstUser.content);
185
+ }
186
+ try {
187
+ sessions.saveChat(chat);
188
+ } catch {
189
+ /* ignore */
190
+ }
191
+ }
192
+
193
+ /** 每轮请求都会重建 apiMessages,这里与桌面版一致:只发送最近 24 条 */
194
+ function historyForAgent(st) {
195
+ return sessions.trimHistory(sessions.buildHistory(st.chat), HISTORY_LIMIT);
196
+ }
197
+
198
+ /**
199
+ * 回显用户消息:与输入框同样的边框样式,让对话记录看起来是「框」。
200
+ */
201
+ function printUserMessage(text) {
202
+ const width = Math.max(20, Math.min(termWidth() - 1, 100));
203
+ const inner = width - 2;
204
+ const unicode = useUnicode();
205
+ const box = unicode
206
+ ? { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }
207
+ : { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' };
208
+
209
+ const lines = [];
210
+ for (const raw of String(text).split('\n')) {
211
+ for (const piece of wrapDisplay(raw, Math.max(4, inner - 2))) lines.push(piece);
212
+ }
213
+ if (!lines.length) lines.push('');
214
+
215
+ const label = ` ${t('you')} `;
216
+ const topFill = Math.max(0, width - 2 - displayWidth(label));
217
+ process.stdout.write(`\n${COLORS.accent(box.tl + label + box.h.repeat(topFill) + box.tr)}\n`);
218
+ for (const line of lines) {
219
+ const pad = Math.max(0, inner - displayWidth(line) - 1);
220
+ process.stdout.write(`${COLORS.accent(box.v)} ${line}${' '.repeat(pad)}${COLORS.accent(box.v)}\n`);
221
+ }
222
+ process.stdout.write(`${COLORS.accent(box.bl + box.h.repeat(width - 2) + box.br)}\n`);
223
+ }
224
+
225
+ /* ------------------------------ 对话 ------------------------------ */
226
+
227
+ async function sendMessage(st, editor, text, options = {}) {
228
+ const promptText = String(text || '').trim();
229
+ if (!promptText) return 0;
230
+ const config = settings.readConfig();
231
+ const model = settings.activeModel({
232
+ ...config,
233
+ activeProviderId: st.runtime.providerId || config.activeProviderId,
234
+ activeModelId: st.runtime.modelId || config.activeModelId
235
+ });
236
+ if (!model) {
237
+ print.warn(t('no_model'));
238
+ return 1;
239
+ }
240
+
241
+ const chat = ensureChat(st);
242
+ chat.mode = st.runtime.mode;
243
+ chat.skill = st.runtime.skill;
244
+ pushMessage(chat, { role: 'user', content: promptText, mode: st.runtime.mode, permission: st.runtime.permission });
245
+ persist(st);
246
+ // 记住上一条提示词,供输入框按 ↑ 回填
247
+ st.lastUserPrompt = promptText;
248
+ if (editor) editor.lastUserPrompt = promptText;
249
+
250
+ // 把用户消息按输入框的样式回显一遍,形成对话记录
251
+ printUserMessage(promptText);
252
+
253
+ const jsonMode = String(options.output || 'text') === 'json';
254
+ const renderer = new StreamRenderer({
255
+ output: process.stdout,
256
+ editor,
257
+ redraw: Boolean(process.stdout.isTTY) && !jsonMode
258
+ });
259
+ const controller = new AbortController();
260
+ st.requestController = controller;
261
+ st.streaming = true;
262
+ st.abortRequested = false;
263
+ editor.setAbortHandler(() => {
264
+ if (!st.streaming || st.abortRequested) return false;
265
+ st.abortRequested = true;
266
+ controller.abort();
267
+ process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} ${t('interrupted')}`)}\n`);
268
+ return true;
269
+ });
270
+
271
+ const startedAt = Date.now();
272
+ let result;
273
+ try {
274
+ result = await runAgent({
275
+ config,
276
+ messages: historyForAgent(st),
277
+ providerId: st.runtime.providerId,
278
+ modelId: st.runtime.modelId,
279
+ mode: st.runtime.mode,
280
+ permission: st.runtime.permission,
281
+ workspace: st.cwd,
282
+ skill: st.runtime.skill,
283
+ signal: controller.signal,
284
+ askUser: async (request) => {
285
+ if (st.yes) return true;
286
+ // 先让渲染器把已收到的正文落地并收起预览行,再暂停编辑器后弹出确认框
287
+ renderer.endInline();
288
+ editor.suspend();
289
+ const allowed = await askApproval(request);
290
+ editor.resume();
291
+ return allowed;
292
+ },
293
+ onEvent: (event) => {
294
+ if (jsonMode && !['review', 'review-start', 'waiting', 'file'].includes(event.type)) return;
295
+ renderer.handle(event);
296
+ }
297
+ });
298
+ } finally {
299
+ st.streaming = false;
300
+ st.abortRequested = false;
301
+ st.requestController = null;
302
+ editor.setAbortHandler(null);
303
+ }
304
+
305
+ const assistantMessage = {
306
+ role: 'assistant',
307
+ content: result.text || '',
308
+ error: result.error || '',
309
+ reasoningChars: result.reasoning?.length || 0,
310
+ usage: result.usage || null,
311
+ toolLog: result.toolLog,
312
+ files: result.files,
313
+ elapsedMs: Date.now() - startedAt
314
+ };
315
+ pushMessage(chat, assistantMessage);
316
+ if (chat.title === 'New conversation') chat.title = sessions.titleFrom(promptText);
317
+ persist(st);
318
+ st.lastResponse = result;
319
+ updateBusState(st);
320
+
321
+ if (result.error) return 1;
322
+ return 0;
323
+ }
324
+
325
+ /* ------------------------------ 终端命令 ------------------------------ */
326
+
327
+ async function runTerminal(st, editor, rawCommand) {
328
+ const command = String(rawCommand || '').trim();
329
+ if (!command) {
330
+ print.warn('Enter a command first');
331
+ return 1;
332
+ }
333
+ if (st.terminalBusy) {
334
+ print.warn('A command is already running');
335
+ return 1;
336
+ }
337
+ const config = settings.readConfig();
338
+ const permission = st.runtime.permission;
339
+ if (permission === 'none') {
340
+ print.warn('No permission to run shell commands');
341
+ return 1;
342
+ }
343
+
344
+ if (permission === 'normal' || permission === 'auto') {
345
+ editor.suspend();
346
+ const allowed = await askApproval(
347
+ {
348
+ title: t('approval_terminal'),
349
+ operation: 'run_terminal',
350
+ detail: `Command: ${command}`,
351
+ command,
352
+ workspace: st.cwd,
353
+ review: '',
354
+ reviewer: ''
355
+ },
356
+ {}
357
+ );
358
+ editor.resume();
359
+ if (!allowed) return 1;
360
+ }
361
+
362
+ const chat = ensureChat(st);
363
+ const message = {
364
+ role: 'terminal',
365
+ command,
366
+ cwd: st.cwd,
367
+ output: '',
368
+ stderr: '',
369
+ status: 'running',
370
+ ts: Date.now()
371
+ };
372
+ chat.messages.push(message);
373
+ st.terminalBusy = true;
374
+
375
+ editor.suspend();
376
+ process.stdout.write(`\n${COLORS.faint(` ${SIGN.arrow} ${command}`)}\n`);
377
+ const result = await terminal.runShellCommand({
378
+ command,
379
+ workspace: st.cwd,
380
+ onOutput: (chunk) => {
381
+ process.stdout.write(chunk);
382
+ if (!message._streamed) message._streamed = '';
383
+ message._streamed += chunk;
384
+ }
385
+ });
386
+ st.terminalBusy = false;
387
+
388
+ message.output = result.output;
389
+ message.stderr = result.stderr;
390
+ message.exitCode = Number.isInteger(result.code) ? result.code : null;
391
+ message.status = result.aborted ? 'done' : result.code === 0 ? 'done' : 'error';
392
+ if (result.error) message.error = result.error;
393
+ if (chat.title === 'New conversation') chat.title = sessions.titleFrom(`Terminal · ${command}`);
394
+ chat.updatedAt = Date.now();
395
+ persist(st);
396
+
397
+ const code = message.exitCode;
398
+ if (result.aborted) print.hint(t('stopped'));
399
+ else if (code === 0) process.stdout.write(COLORS.green(` ${SIGN.ok} done\n`));
400
+ else process.stdout.write(COLORS.red(` ${SIGN.fail} ${t('exit_code')} ${code ?? '?'}\n`));
401
+ editor.resume();
402
+ return code === 0 ? 0 : 1;
403
+ }
404
+
405
+ /* ------------------------------ 状态栏刷新 ------------------------------ */
406
+
407
+ function updateBusState(st) {
408
+ try {
409
+ sessionBus.write({
410
+ pid: process.pid,
411
+ cwd: st.cwd,
412
+ chatId: st.chat?.id || '',
413
+ title: st.chat?.title || '',
414
+ mode: st.runtime.mode,
415
+ permission: st.runtime.permission,
416
+ providerId: st.runtime.providerId,
417
+ modelId: st.runtime.modelId,
418
+ skill: st.runtime.skill,
419
+ streaming: st.streaming
420
+ });
421
+ } catch {
422
+ /* ignore */
423
+ }
424
+ }
425
+
426
+ /* ------------------------------ 斜杠命令 ------------------------------ */
427
+
428
+ async function handleSlash(st, editor, input) {
429
+ const text = String(input || '').trim();
430
+ const [head, ...restParts] = text.split(/\s+/);
431
+ const rest = restParts.join(' ').trim();
432
+ const command = findCommand(head);
433
+
434
+ if (!command) {
435
+ print.warn(`Unknown command: ${head}. Type /help to list commands`);
436
+ return;
437
+ }
438
+
439
+ switch (command.name) {
440
+ case '/help':
441
+ showHelp();
442
+ return;
443
+ case '/new': {
444
+ persist(st);
445
+ st.chat = sessions.createChat({ mode: st.runtime.mode, skill: st.runtime.skill });
446
+ updateBusState(st);
447
+ print.ok(t('new_chat'));
448
+ return;
449
+ }
450
+ case '/list':
451
+ listChats(st);
452
+ return;
453
+ case '/resume': {
454
+ const chats = sessions.listChats();
455
+ if (!chats.length) {
456
+ print.warn(t('no_chats'));
457
+ return;
458
+ }
459
+ let target;
460
+ try {
461
+ target = sessions.resolveChat(rest || '1', chats);
462
+ } catch (err) {
463
+ print.fail(err.message);
464
+ return;
465
+ }
466
+ if (!target) {
467
+ print.fail(`Conversation not found: ${rest}`);
468
+ return;
469
+ }
470
+ persist(st);
471
+ st.chat = target;
472
+ updateBusState(st);
473
+ print.ok(`Restored "${target.title}" (${target.messages.filter((m) => m.role !== 'terminal').length} messages)`);
474
+ printThread(st);
475
+ return;
476
+ }
477
+ case '/delete': {
478
+ const chats = sessions.listChats();
479
+ const target = rest ? sessions.resolveChat(rest, chats) : st.chat;
480
+ if (!target) {
481
+ print.fail(`Conversation not found: ${rest}`);
482
+ return;
483
+ }
484
+ sessions.deleteChat(target.id);
485
+ if (st.chat?.id === target.id) st.chat = sessions.listChats()[0] || null;
486
+ updateBusState(st);
487
+ print.ok(`Deleted "${target.title}"`);
488
+ return;
489
+ }
490
+ case '/rename': {
491
+ if (!st.chat) {
492
+ print.warn('No active conversation');
493
+ return;
494
+ }
495
+ if (!rest) {
496
+ print.fail('Usage: /rename <title>');
497
+ return;
498
+ }
499
+ st.chat.title = rest;
500
+ persist(st);
501
+ print.ok(`Renamed to "${rest}"`);
502
+ return;
503
+ }
504
+ case '/compress':
505
+ await compress(st, editor);
506
+ return;
507
+ case '/skill':
508
+ await handleSkillCommand(st, rest);
509
+ return;
510
+ case '/model':
511
+ handleModelCommand(st, rest);
512
+ return;
513
+ case '/provider':
514
+ handleProviderCommand(st, rest);
515
+ return;
516
+ case '/mode':
517
+ handleModeCommand(st, rest);
518
+ return;
519
+ case '/permission':
520
+ handlePermissionCommand(st, rest);
521
+ return;
522
+ case '/cd':
523
+ handleCd(st, rest);
524
+ return;
525
+ case '/ls':
526
+ handleLs(st, rest);
527
+ return;
528
+ case '/pwd':
529
+ print.write(st.cwd);
530
+ return;
531
+ case '/term':
532
+ await runTerminal(st, editor, rest);
533
+ return;
534
+ case '/context': {
535
+ const config = settings.readConfig();
536
+ const model = settings.activeModel(config);
537
+ process.stdout.write(`\n${contextReport(contextInfo(st.chat, model))}\n\n`);
538
+ return;
539
+ }
540
+ case '/mcp':
541
+ await handleMcpCommand(st, rest);
542
+ return;
543
+ case '/theme':
544
+ handleTheme(rest);
545
+ return;
546
+ case '/clear':
547
+ process.stdout.write('\x1b[2J\x1b[H');
548
+ return;
549
+ case '/exit':
550
+ st.exitRequested = true;
551
+ return;
552
+ default:
553
+ print.warn(`Not implemented yet: ${command.name}`);
554
+ }
555
+ }
556
+
557
+ function showHelp() {
558
+ print.blank();
559
+ print.heading(t('menu_title'));
560
+ // 命令名列按显示宽度对齐(中文别名占两格,不能用 padEnd)
561
+ const nameWidth = Math.max(...SLASH_COMMANDS.map((cmd) => displayWidth(cmd.name))) + 2;
562
+ for (const cmd of SLASH_COMMANDS) {
563
+ const name = `${cmd.name}${' '.repeat(Math.max(0, nameWidth - displayWidth(cmd.name)))}`;
564
+ print.write(` ${COLORS.accent(name)}${cmd.desc}`);
565
+ if (cmd.usage !== cmd.name) print.write(` ${' '.repeat(nameWidth)}${COLORS.faint(cmd.usage)}`);
566
+ }
567
+ print.blank();
568
+ print.heading('Keys');
569
+ const tips = [
570
+ ['! cmd', 'Run a shell command; output joins the context (e.g. !git status)'],
571
+ ['\\', 'Continue on the next line (multi-line input)'],
572
+ ['Tab', 'Open / cycle the completion menu'],
573
+ ['Ctrl+C', 'Interrupt generation · exit when idle · clear input'],
574
+ ['Ctrl+D', 'Exit'],
575
+ ['↑ / ↓', 'Recalls the previous prompt']
576
+ ];
577
+ for (const [key, desc] of tips) {
578
+ print.write(` ${COLORS.cyan(key)} ${desc}`);
579
+ }
580
+ print.blank();
581
+ print.hint(`Everything is also available as a subcommand: ${BIN} provider list · ${BIN} mcp start`);
582
+ print.blank();
583
+ }
584
+
585
+ function listChats(st) {
586
+ const chats = sessions.listChats();
587
+ print.blank();
588
+ if (!chats.length) {
589
+ print.hint(t('no_chats'));
590
+ return;
591
+ }
592
+ print.heading(`Conversations (${chats.length})`);
593
+ const rows = chats.map((chat, index) => {
594
+ const messages = chat.messages.filter((m) => m.role !== 'terminal');
595
+ const last = messages[messages.length - 1];
596
+ const date = new Date(chat.updatedAt || chat.createdAt);
597
+ const stamp = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(
598
+ date.getHours()
599
+ ).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
600
+ return [
601
+ chat.id === st.chat?.id ? COLORS.accent('*') : ' ',
602
+ String(index + 1),
603
+ chat.title || 'New conversation',
604
+ String(messages.length),
605
+ stamp,
606
+ chat.mode === 'work' ? t('mode_work') : t('mode_chat'),
607
+ last?.content ? last.content.slice(0, 40) : ''
608
+ ];
609
+ });
610
+ print.table(
611
+ [
612
+ { title: '', max: 1 },
613
+ { title: '#', max: 3 },
614
+ { title: 'Title', max: 30 },
615
+ { title: 'Msgs', max: 4 },
616
+ { title: 'Updated', max: 12 },
617
+ { title: 'Mode', max: 4 },
618
+ { title: 'Last message', max: 40 }
619
+ ],
620
+ rows
621
+ );
622
+ print.blank();
623
+ print.hint('/resume <n> restore · /delete <n> remove');
624
+ }
625
+
626
+ function printThread(st) {
627
+ const chat = st.chat;
628
+ if (!chat?.messages?.length) return;
629
+ for (const message of chat.messages) {
630
+ if (message.role === 'user') {
631
+ process.stdout.write(`\n${COLORS.faint('› You')}\n${message.content}\n`);
632
+ } else if (message.role === 'assistant') {
633
+ process.stdout.write(`\n${COLORS.accent('•')} ${COLORS.bold('DgxayCode')}\n`);
634
+ if (message.content) process.stdout.write(require('./ansi').renderMarkdown(message.content, { width: termWidth() }));
635
+ if (message.error) process.stdout.write(COLORS.red(message.error) + '\n');
636
+ } else if (message.role === 'terminal') {
637
+ process.stdout.write(`\n${COLORS.faint(`! ${message.command}`)}\n`);
638
+ process.stdout.write(`${message.output || ''}${message.stderr || ''}`.slice(-4000));
639
+ if (!String(message.output || '').endsWith('\n')) process.stdout.write('\n');
640
+ }
641
+ }
642
+ process.stdout.write('\n');
643
+ }
644
+
645
+ async function compress(st, editor) {
646
+ if (st.streaming) {
647
+ print.warn(t('wait_current'));
648
+ return;
649
+ }
650
+ const chat = st.chat;
651
+ if (!chat?.messages?.length) {
652
+ print.warn(t('compress_nothing'));
653
+ return;
654
+ }
655
+ const config = settings.readConfig();
656
+ const model = settings.activeModel(config);
657
+ if (!model) {
658
+ print.warn(t('no_model'));
659
+ return;
660
+ }
661
+ const before = sessions.estimateChatTokens(chat);
662
+ editor.suspend();
663
+ process.stdout.write(`${COLORS.faint(`${SIGN.run} Compressing context…`)}`);
664
+ try {
665
+ const result = await compressMessages({
666
+ config,
667
+ providerId: st.runtime.providerId || config.activeProviderId,
668
+ modelId: st.runtime.modelId || config.activeModelId,
669
+ messages: sessions.buildHistory(chat)
670
+ });
671
+ if (!result.summary) throw new Error('the model returned no summary');
672
+ chat.messages = [
673
+ { role: 'assistant', content: `> Context compressed; only key information kept.\n\n${result.summary}`, ts: Date.now(), compressed: true }
674
+ ];
675
+ chat.updatedAt = Date.now();
676
+ persist(st);
677
+ const after = sessions.estimateChatTokens(chat);
678
+ process.stdout.write('\r\x1b[2K');
679
+ print.ok(`Context compressed: ${sessions.formatTokens(before)} → ${sessions.formatTokens(after)} tokens (estimated)`);
680
+ process.stdout.write(`\n${require('./ansi').renderMarkdown(result.summary, { width: termWidth() })}\n`);
681
+ } catch (err) {
682
+ process.stdout.write('\r\x1b[2K');
683
+ print.fail(err.message || t('compress_failed'));
684
+ } finally {
685
+ editor.resume();
686
+ }
687
+ }
688
+
689
+ async function handleSkillCommand(st, rest) {
690
+ if (!rest) {
691
+ print.blank();
692
+ print.heading(`Skills (${settings.homeRelative(cfg.SKILLS_DIR)})`);
693
+ const list = skills.listSkills();
694
+ if (!list.length) {
695
+ print.hint(' No skills yet. Put a name.md or <dir>/SKILL.md into ~/.dcb/skills');
696
+ print.blank();
697
+ return;
698
+ }
699
+ const rows = list.map((skill, index) => [
700
+ String(index + 1),
701
+ skill.name,
702
+ st.runtime.skill === skill.name ? COLORS.green('enabled') : '',
703
+ skill.description
704
+ ]);
705
+ print.table(
706
+ [
707
+ { title: '#', max: 3 },
708
+ { title: 'Name', max: 22 },
709
+ { title: 'Status', max: 8 },
710
+ { title: 'Description', max: 60 }
711
+ ],
712
+ rows
713
+ );
714
+ print.blank();
715
+ print.hint('/skill off to disable · /skill <name> [prompt] to enable for this conversation');
716
+ print.blank();
717
+ return;
718
+ }
719
+ if (rest === 'off' || rest === 'clear') {
720
+ st.runtime.skill = '';
721
+ if (st.chat) st.chat.skill = '';
722
+ updateBusState(st);
723
+ print.ok(t('skills_off'));
724
+ return;
725
+ }
726
+ const list = skills.listSkills();
727
+ const picked = list
728
+ .filter((item) => rest === item.name || rest.startsWith(`${item.name} `))
729
+ .sort((a, b) => b.name.length - a.name.length)[0];
730
+ if (!picked) {
731
+ print.warn(`Skill not found: ${rest}. Type /skill to list them`);
732
+ return;
733
+ }
734
+ st.runtime.skill = picked.name;
735
+ if (st.chat) st.chat.skill = picked.name;
736
+ updateBusState(st);
737
+ print.ok(`Skill enabled for this conversation: ${picked.name}`);
738
+ const prompt = rest.slice(picked.name.length).trim();
739
+ if (prompt) return prompt;
740
+ return undefined;
741
+ }
742
+
743
+ function handleModelCommand(st, rest) {
744
+ const config = settings.readConfig();
745
+ const provider = settings.findProvider(st.runtime.providerId) || settings.activeProvider(config);
746
+ const models = provider?.models || [];
747
+ if (!models.length) {
748
+ print.warn('This provider has no models. Run dgxay provider add');
749
+ return;
750
+ }
751
+ const current = models.find((m) => m.id === st.runtime.modelId) || models[0];
752
+ if (!rest) {
753
+ print.blank();
754
+ print.heading(`Models (${provider.name})`);
755
+ models.forEach((model, index) => {
756
+ const active = model.id === current?.id;
757
+ print.write(
758
+ ` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(String(index + 1).padStart(2))}. ${
759
+ active ? COLORS.accent(model.name || model.id) : model.name || model.id
760
+ } ${COLORS.faint(model.id !== (model.name || model.id) ? `(${model.id})` : '')}${
761
+ model.contextWindow ? COLORS.faint(` · ${sessions.formatTokens(model.contextWindow)}`) : ''
762
+ }`
763
+ );
764
+ });
765
+ print.blank();
766
+ print.hint('/model <n|id> to switch · saved to config');
767
+ print.blank();
768
+ return;
769
+ }
770
+ const index = Number(rest);
771
+ const target =
772
+ (Number.isInteger(index) && index >= 1 && index <= models.length ? models[index - 1] : null) ||
773
+ models.find((m) => m.id === rest) ||
774
+ models.find((m) => m.id.includes(rest));
775
+ if (!target) {
776
+ print.fail(`Model not found: ${rest}`);
777
+ return;
778
+ }
779
+ settings.setActiveModel(provider.id, target.id);
780
+ st.runtime.modelId = target.id;
781
+ st.runtime.providerId = provider.id;
782
+ updateBusState(st);
783
+ print.ok(`Switched to ${target.name || target.id}`);
784
+ }
785
+
786
+ function handleProviderCommand(st, rest) {
787
+ const config = settings.readConfig();
788
+ const providers = config.providers || [];
789
+ if (!providers.length) {
790
+ print.warn('No providers yet. Run dgxay config init');
791
+ return;
792
+ }
793
+ const current = settings.findProvider(st.runtime.providerId) || settings.activeProvider(config);
794
+ if (!rest) {
795
+ print.blank();
796
+ print.heading(t('provider_list_title'));
797
+ providers.forEach((provider, index) => {
798
+ const active = provider.id === current?.id;
799
+ print.write(
800
+ ` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(String(index + 1).padStart(2))}. ${
801
+ active ? COLORS.accent(provider.name) : provider.name
802
+ } ${COLORS.faint(settings.PROTOCOL_META[provider.protocol || 'openai'].short)} ${COLORS.faint(
803
+ `${(provider.models || []).length} models`
804
+ )}`
805
+ );
806
+ });
807
+ print.blank();
808
+ print.hint('/provider <n|name> to switch');
809
+ print.blank();
810
+ return;
811
+ }
812
+ const index = Number(rest);
813
+ const target =
814
+ (Number.isInteger(index) && index >= 1 && index <= providers.length ? providers[index - 1] : null) ||
815
+ settings.findProvider(rest);
816
+ if (!target) {
817
+ print.fail(`Provider not found: ${rest}`);
818
+ return;
819
+ }
820
+ const model = target.models?.[0];
821
+ if (!model) {
822
+ print.fail(`Provider ${target.name} has no models`);
823
+ return;
824
+ }
825
+ settings.saveConfig({ activeProviderId: target.id, activeModelId: model.id });
826
+ st.runtime.providerId = target.id;
827
+ st.runtime.modelId = model.id;
828
+ updateBusState(st);
829
+ print.ok(`Switched to ${target.name} / ${model.name || model.id}`);
830
+ }
831
+
832
+ function handleModeCommand(st, rest) {
833
+ const next = rest ? (rest === 'work' ? 'work' : 'chat') : st.runtime.mode === 'work' ? 'chat' : 'work';
834
+ st.runtime.mode = next;
835
+ if (st.chat) st.chat.mode = next;
836
+ updateBusState(st);
837
+ print.ok(next === 'work' ? `${t('mode_set')}: ${t('mode_work')}` : `${t('mode_set')}: ${t('mode_chat')}`);
838
+ if (next === 'work' && !st.cwd) print.hint('No workspace selected. Use /cd <dir>');
839
+ }
840
+
841
+ function handlePermissionCommand(st, rest) {
842
+ const config = settings.readConfig();
843
+ if (!rest) {
844
+ print.blank();
845
+ print.heading(t('perms_title'));
846
+ const available = settings.availablePermissions(config);
847
+ Object.entries(settings.PERMISSIONS).forEach(([key, meta]) => {
848
+ const usable = available.includes(key);
849
+ const active = st.runtime.permission === key;
850
+ print.write(
851
+ ` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(key.padEnd(8))} ${
852
+ usable ? meta.label : COLORS.faint(`${meta.label} (unavailable)`)
853
+ } ${COLORS.faint(meta.desc)}`
854
+ );
855
+ });
856
+ if (!available.includes('auto')) print.hint('Auto review needs a valid review model');
857
+ print.blank();
858
+ return;
859
+ }
860
+ if (!settings.PERMISSIONS[rest]) {
861
+ print.fail('Level must be none / normal / auto / full');
862
+ return;
863
+ }
864
+ if (rest === 'auto' && !settings.hasReviewModel(config)) {
865
+ print.fail(t('perm_auto_unavailable'));
866
+ return;
867
+ }
868
+ st.runtime.permission = rest;
869
+ settings.setPermission(rest);
870
+ updateBusState(st);
871
+ print.ok(`Permission set to: ${settings.PERMISSIONS[rest].label}`);
872
+ if (rest === 'full') print.warn(t('perm_full_warning'));
873
+ }
874
+
875
+ function handleCd(st, rest) {
876
+ if (!rest) {
877
+ print.write(st.cwd);
878
+ return;
879
+ }
880
+ const target = path.resolve(st.cwd, rest);
881
+ let stat;
882
+ try {
883
+ stat = fs.statSync(target);
884
+ } catch {
885
+ print.fail(`Directory not found: ${target}`);
886
+ return;
887
+ }
888
+ if (!stat.isDirectory()) {
889
+ print.fail(`Not a directory: ${target}`);
890
+ return;
891
+ }
892
+ st.cwd = target;
893
+ settings.setWorkspace(target);
894
+ updateBusState(st);
895
+ print.ok(`Workspace: ${target}`);
896
+ }
897
+
898
+ function handleLs(st, rest) {
899
+ const target = rest ? path.resolve(st.cwd, rest) : st.cwd;
900
+ let entries = [];
901
+ try {
902
+ entries = fs.readdirSync(target, { withFileTypes: true });
903
+ } catch (err) {
904
+ print.fail(`Cannot read ${target}:${err.code || err.message}`);
905
+ return;
906
+ }
907
+ entries.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
908
+ print.blank();
909
+ print.write(COLORS.faint(`${target}`));
910
+ for (const entry of entries.slice(0, 200)) {
911
+ if (entry.name.startsWith('.') && entry.name !== '.env.example') continue;
912
+ let size = '';
913
+ if (!entry.isDirectory()) {
914
+ try {
915
+ size = `${fs.statSync(path.join(target, entry.name)).size} B`;
916
+ } catch {
917
+ size = '';
918
+ }
919
+ }
920
+ print.write(` ${entry.isDirectory() ? COLORS.accent(`${entry.name}/`) : entry.name}${COLORS.faint(size ? ` ${size}` : '')}`);
921
+ }
922
+ print.blank();
923
+ }
924
+
925
+ async function handleMcpCommand(st, rest) {
926
+ const [action, ...others] = rest.split(/\s+/).filter(Boolean);
927
+ const arg = others.join(' ');
928
+ if (!action || action === 'list' || action === 'ls') {
929
+ const list = mcp.listServers();
930
+ print.blank();
931
+ if (!list.length) {
932
+ print.hint(`No MCP servers. Use /mcp add or ${BIN} mcp add`);
933
+ print.blank();
934
+ return;
935
+ }
936
+ print.heading(`MCP Server(${list.length})`);
937
+ const rows = list.map((server, index) => [
938
+ String(index + 1),
939
+ server.name,
940
+ server.status === 'running' ? COLORS.green('running') : server.status === 'error' ? COLORS.red('error') : 'stopped',
941
+ server.pid ? String(server.pid) : '',
942
+ `${server.command} ${(server.args || []).join(' ')}`
943
+ ]);
944
+ print.table(
945
+ [
946
+ { title: '#', max: 3 },
947
+ { title: 'Name', max: 18 },
948
+ { title: 'Status', max: 8 },
949
+ { title: 'PID', max: 8 },
950
+ { title: 'Command', max: 50 }
951
+ ],
952
+ rows
953
+ );
954
+ print.blank();
955
+ print.hint('/mcp start <name> · /mcp stop <name> · /mcp logs <name> · /mcp add name command args');
956
+ print.blank();
957
+ return;
958
+ }
959
+ const server = mcp.findServer(arg);
960
+ if (action === 'start') {
961
+ if (!server) return print.fail(`MCP not found: ${arg}`);
962
+ try {
963
+ const result = mcp.startServer(server.id);
964
+ print.ok(`Started ${server.name}(pid ${result.pid})`);
965
+ } catch (err) {
966
+ print.fail(err.message);
967
+ }
968
+ return;
969
+ }
970
+ if (action === 'stop') {
971
+ if (!server) return print.fail(`MCP not found: ${arg}`);
972
+ mcp.stopServer(server.id);
973
+ print.ok(`Stopped ${server.name}`);
974
+ return;
975
+ }
976
+ if (action === 'logs') {
977
+ if (!server) return print.fail(`MCP not found: ${arg}`);
978
+ const logs = mcp.statusOf(server.id).logs || [];
979
+ if (!logs.length) return print.hint('No logs yet');
980
+ for (const entry of logs) {
981
+ print.write(`${entry.stream === 'stderr' ? COLORS.red(entry.text) : entry.text}`);
982
+ }
983
+ return;
984
+ }
985
+ if (action === 'add') {
986
+ const [name, command, ...args] = others;
987
+ if (!name || !command) return print.fail('Usage: /mcp add <name> <command> [args...]');
988
+ const created = mcp.addServer({ name, command, args });
989
+ print.ok(`Added MCP "${created.name}" — start it with /mcp start ${created.name}`);
990
+ return;
991
+ }
992
+ if (action === 'rm' || action === 'remove') {
993
+ if (!server) return print.fail(`MCP not found: ${arg}`);
994
+ mcp.removeServer(server.id);
995
+ print.ok(`Deleted ${server.name}`);
996
+ return;
997
+ }
998
+ print.fail(`Unknown /mcp action: ${action}`);
999
+ }
1000
+
1001
+ function handleTheme(rest) {
1002
+ const mode = String(rest || '').toLowerCase();
1003
+ if (mode === 'on' || mode === 'color') {
1004
+ setColorMode('ansi256');
1005
+ print.ok('Color output on');
1006
+ return;
1007
+ }
1008
+ if (mode === 'off' || mode === 'plain') {
1009
+ setColorMode('none');
1010
+ print.ok('Color output off');
1011
+ return;
1012
+ }
1013
+ print.write(`Color output: ${colorEnabled() ? 'on' : 'off'} (/theme on | off)`);
1014
+ }
1015
+
1016
+ /* ------------------------------ 输入分发 ------------------------------ */
1017
+
1018
+ async function handleInput(st, editor, raw) {
1019
+ const line = String(raw || '');
1020
+ const text = line.trim();
1021
+ if (!text) return;
1022
+
1023
+ if (text.startsWith('/')) {
1024
+ const command = findCommand(text);
1025
+ if (command?.name === '/skill') {
1026
+ const result = await handleSkillCommand(st, text.split(/\s+/).slice(1).join(' ').trim());
1027
+ if (typeof result === 'string') return sendMessage(st, editor, result);
1028
+ return;
1029
+ }
1030
+ await handleSlash(st, editor, text);
1031
+ return;
1032
+ }
1033
+
1034
+ if (text.startsWith('!') && text.length > 1) {
1035
+ editor.pushHistory(line);
1036
+ await runTerminal(st, editor, text.slice(1).trim());
1037
+ return;
1038
+ }
1039
+
1040
+ // 技能命令后面可以附带提示词:/skill name 提示词
1041
+ editor.pushHistory(line);
1042
+ await sendMessage(st, editor, text);
1043
+ }
1044
+
1045
+ /* ------------------------------ 主循环 ------------------------------ */
1046
+
1047
+ /** 单次运行(dgxay "问题" / -p):与 run 子命令共用同一套逻辑 */
1048
+ async function singleShot(options) {
1049
+ const argv = [];
1050
+ const prompt = String(options.initialPrompt || '').trim();
1051
+ if (prompt) argv.push('-p', prompt);
1052
+ if (options.mode) argv.push('--mode', options.mode);
1053
+ if (options.permission) argv.push('--permission', options.permission);
1054
+ if (options.workspace) argv.push('--workspace', options.workspace);
1055
+ if (options.model) argv.push('--model', options.model);
1056
+ if (options.provider) argv.push('--provider', options.provider);
1057
+ if (options.skill) argv.push('--skill', options.skill);
1058
+ if (options.resume) argv.push('--resume', options.resume);
1059
+ if (options.continueLast) argv.push('--continue');
1060
+ if (options.output) argv.push('--output', options.output);
1061
+ if (options.yes) argv.push('--yes');
1062
+ if (!prompt) {
1063
+ print.fail('A prompt is required');
1064
+ return 2;
1065
+ }
1066
+ return require('../commands/run').run({ argv, runtime: options });
1067
+ }
1068
+
1069
+ async function start(options = {}) {
1070
+ if (options.noColor) setColorMode('none');
1071
+ if (options.single || options.print) return singleShot(options);
1072
+ return startInteractive(options);
1073
+ }
1074
+
1075
+ /** 交互模式主体 */
1076
+ async function startInteractive(options) {
1077
+ const config = settings.readConfig();
1078
+ if (!config.providers?.length) {
1079
+ // 还没有任何供应商:交互终端里直接进入配置引导
1080
+ print.warn('No provider configured yet');
1081
+ if (!process.stdin.isTTY) {
1082
+ print.hint('Run dgxay config init to set one up');
1083
+ return 1;
1084
+ }
1085
+ const result = await require('../commands/config').run({
1086
+ argv: ['init'],
1087
+ runtime: options
1088
+ });
1089
+ if (result !== 0) return result;
1090
+ }
1091
+
1092
+ const st = createRuntime(options);
1093
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
1094
+
1095
+ // 启动时先清屏,再渲染界面(管道模式不清屏,保持输出干净)
1096
+ if (interactive) {
1097
+ clearScreen(process.stdout);
1098
+ process.stdout.write('\x1b[3J');
1099
+ }
1100
+
1101
+ // 历史:优先把当前会话里用户说过的话填进去,↑ 可以直接回填
1102
+ const previousPrompts = (st.chat?.messages || [])
1103
+ .filter((message) => message.role === 'user' && typeof message.content === 'string' && message.content.trim())
1104
+ .map((message) => message.content.trim());
1105
+ const lastUserPrompt = previousPrompts[previousPrompts.length - 1] || '';
1106
+
1107
+ const editor = interactive
1108
+ ? new LineEditor({
1109
+ prompt: promptFor(st),
1110
+ placeholder: t('box_placeholder'),
1111
+ completer,
1112
+ multiline: true,
1113
+ capabilities: detectCapabilities(process.stdout),
1114
+ history: previousPrompts,
1115
+ lastUserPrompt
1116
+ })
1117
+ : createLineReader(process.stdin);
1118
+
1119
+ showWelcome(st);
1120
+ updateBusState(st);
1121
+ const heartbeat = new sessionBus.Heartbeat(() => ({
1122
+ pid: process.pid,
1123
+ cwd: st.cwd,
1124
+ chatId: st.chat?.id || '',
1125
+ title: st.chat?.title || '',
1126
+ mode: st.runtime.mode,
1127
+ permission: st.runtime.permission,
1128
+ providerId: st.runtime.providerId,
1129
+ modelId: st.runtime.modelId,
1130
+ skill: st.runtime.skill
1131
+ }));
1132
+ heartbeat.start();
1133
+
1134
+ const cleanup = () => {
1135
+ heartbeat.stop();
1136
+ sessionBus.clear();
1137
+ editor.finish({ type: 'eof', value: '' });
1138
+ persist(st);
1139
+ mcp.killAll();
1140
+ try {
1141
+ editor.close?.();
1142
+ } catch {
1143
+ /* ignore */
1144
+ }
1145
+ };
1146
+ // 只在交互终端里接管信号;管道 / 脚本模式下不能注册 SIGINT 监听,
1147
+ // 否则 Node 会认为“有人在处理信号”,进程永远无法自然退出。
1148
+ const signalHandlers = {};
1149
+ if (process.stdin.isTTY) {
1150
+ signalHandlers.SIGINT = () => {
1151
+ if (st.streaming) {
1152
+ st.abortRequested = true;
1153
+ st.requestController?.abort();
1154
+ return;
1155
+ }
1156
+ cleanup();
1157
+ process.exit(0);
1158
+ };
1159
+ signalHandlers.SIGTERM = () => {
1160
+ cleanup();
1161
+ process.exit(0);
1162
+ };
1163
+ process.on('SIGINT', signalHandlers.SIGINT);
1164
+ process.on('SIGTERM', signalHandlers.SIGTERM);
1165
+ }
1166
+
1167
+ editor.setAbortHandler(() => {
1168
+ if (st.streaming && !st.abortRequested) {
1169
+ st.abortRequested = true;
1170
+ st.requestController?.abort();
1171
+ process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} ${t('interrupted')}`)}\n`);
1172
+ return true;
1173
+ }
1174
+ if (st.terminalBusy) return true;
1175
+ return false;
1176
+ });
1177
+
1178
+ try {
1179
+ while (!st.exitRequested) {
1180
+ // 输入框上方的一行说明:模型 / 技能
1181
+ if (interactive) editor.prompt = promptFor(st);
1182
+ // ↑ 回填:始终指向最近一条用户提示词
1183
+ if (interactive) {
1184
+ const prompts = (st.chat?.messages || []).filter(
1185
+ (message) => message.role === 'user' && typeof message.content === 'string' && message.content.trim()
1186
+ );
1187
+ editor.lastUserPrompt = prompts.length ? prompts[prompts.length - 1].content.trim() : '';
1188
+ editor.history = prompts.map((message) => message.content.trim());
1189
+ }
1190
+ const answer = await editor.question();
1191
+ if (answer.type === 'eof' || answer.type === 'cancel') break;
1192
+ try {
1193
+ await handleInput(st, editor, answer.value);
1194
+ } catch (err) {
1195
+ if (err?.userError) print.fail(err.message);
1196
+ else {
1197
+ print.fail(err?.message || String(err));
1198
+ if (process.env.DGXAY_DEBUG === '1' && err?.stack) process.stderr.write(`${err.stack}\n`);
1199
+ }
1200
+ }
1201
+ }
1202
+ } finally {
1203
+ if (signalHandlers.SIGINT) process.removeListener('SIGINT', signalHandlers.SIGINT);
1204
+ if (signalHandlers.SIGTERM) process.removeListener('SIGTERM', signalHandlers.SIGTERM);
1205
+ heartbeat.stop();
1206
+ sessionBus.clear();
1207
+ persist(st);
1208
+ mcp.killAll();
1209
+ try {
1210
+ editor.close?.();
1211
+ } catch {
1212
+ /* ignore */
1213
+ }
1214
+ if (typeof process.stdin.pause === 'function') process.stdin.pause();
1215
+ }
1216
+
1217
+ process.stdout.write(`${COLORS.faint(t('bye'))}\n`);
1218
+ return 0;
1219
+ }
1220
+
1221
+ module.exports = {
1222
+ start,
1223
+ startInteractive,
1224
+ createRuntime,
1225
+ handleInput,
1226
+ sendMessage,
1227
+ runTerminal,
1228
+ showHelp,
1229
+ permissionBadge,
1230
+ promptFor
1231
+ };