dgxay-code-cli 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README-zh.md +242 -0
- package/README.md +223 -2
- package/bin/dgxay.js +42 -0
- package/package.json +47 -4
- package/src/cli/ansi.js +539 -0
- package/src/cli/approval.js +186 -0
- package/src/cli/args.js +162 -0
- package/src/cli/command-table.js +52 -0
- package/src/cli/editor.js +828 -0
- package/src/cli/i18n.js +276 -0
- package/src/cli/index.js +228 -0
- package/src/cli/print.js +148 -0
- package/src/cli/render.js +395 -0
- package/src/cli/repl-state.js +85 -0
- package/src/cli/repl.js +1231 -0
- package/src/cli/slash.js +75 -0
- package/src/cli/ui.js +52 -0
- package/src/commands/_shared.js +20 -0
- package/src/commands/chat.js +212 -0
- package/src/commands/config.js +363 -0
- package/src/commands/context.js +43 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/export.js +102 -0
- package/src/commands/mcp.js +257 -0
- package/src/commands/model.js +223 -0
- package/src/commands/oauth.js +246 -0
- package/src/commands/permission.js +108 -0
- package/src/commands/provider.js +332 -0
- package/src/commands/run.js +172 -0
- package/src/commands/skill.js +94 -0
- package/src/commands/workspace.js +100 -0
- package/src/core/agent.js +420 -0
- package/src/core/llm.js +709 -0
- package/src/core/mcp.js +271 -0
- package/src/core/oauth.js +737 -0
- package/src/core/prompt.js +269 -0
- package/src/core/sessions.js +208 -0
- package/src/core/tools.js +391 -0
- package/src/services/config.js +182 -0
- package/src/services/input.js +91 -0
- package/src/services/session-bus.js +104 -0
- package/src/services/settings.js +340 -0
- package/src/services/skills.js +125 -0
- package/src/services/terminal.js +124 -0
- package/src/services/version.js +25 -0
package/src/cli/slash.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 交互模式里的斜杠命令。
|
|
5
|
+
*
|
|
6
|
+
* 桌面版只有 /压缩 与 /技能;CLI 额外提供会话、模型、权限、MCP 等
|
|
7
|
+
* 「原本在界面按钮上」的操作,保证去掉 UI 后功能不丢。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { t } = require('./i18n');
|
|
11
|
+
|
|
12
|
+
const SLASH_COMMANDS = [
|
|
13
|
+
{ name: '/help', desc: 'Show all commands', usage: '/help' },
|
|
14
|
+
{ name: '/new', desc: 'Start a new conversation', usage: '/new' },
|
|
15
|
+
{ name: '/list', desc: 'List saved conversations', usage: '/list' },
|
|
16
|
+
{ name: '/resume', desc: 'Restore a conversation', usage: '/resume <n|id>' },
|
|
17
|
+
{ name: '/delete', desc: 'Delete a conversation', usage: '/delete <n|id>' },
|
|
18
|
+
{ name: '/rename', desc: 'Rename current conversation', usage: '/rename <title>' },
|
|
19
|
+
{ name: '/compress', desc: 'Compress context (lossy)', usage: '/compress', aliases: ['/压缩'] },
|
|
20
|
+
{ name: '/skill', desc: 'List or enable skills', usage: '/skill [name] [prompt]', aliases: ['/技能'] },
|
|
21
|
+
{ name: '/model', desc: 'Show or switch model', usage: '/model [n|id]' },
|
|
22
|
+
{ name: '/provider', desc: 'Show or switch provider', usage: '/provider [n|name]' },
|
|
23
|
+
{ name: '/mode', desc: 'Switch chat / work mode', usage: '/mode [chat|work]' },
|
|
24
|
+
{ name: '/permission', desc: 'Switch permission level', usage: '/permission [level]' },
|
|
25
|
+
{ name: '/cd', desc: 'Change workspace', usage: '/cd <dir>' },
|
|
26
|
+
{ name: '/ls', desc: 'List workspace files', usage: '/ls [dir]' },
|
|
27
|
+
{ name: '/pwd', desc: 'Show current workspace', usage: '/pwd' },
|
|
28
|
+
{ name: '/term', desc: 'Run a shell command', usage: '/term <command>' },
|
|
29
|
+
{ name: '/context', desc: 'Show context usage', usage: '/context' },
|
|
30
|
+
{ name: '/mcp', desc: 'Manage MCP servers', usage: '/mcp [subcommand]' },
|
|
31
|
+
{ name: '/theme', desc: 'Toggle color output', usage: '/theme [on|off]' },
|
|
32
|
+
{ name: '/clear', desc: 'Clear the screen', usage: '/clear' },
|
|
33
|
+
{ name: '/exit', desc: 'Exit', usage: '/exit', aliases: ['/quit', '/q'] }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function findCommand(text) {
|
|
37
|
+
const token = String(text || '').trim().split(/\s+/)[0].toLowerCase();
|
|
38
|
+
if (!token) return null;
|
|
39
|
+
return (
|
|
40
|
+
SLASH_COMMANDS.find((cmd) => cmd.name === token) ||
|
|
41
|
+
SLASH_COMMANDS.find((cmd) => (cmd.aliases || []).includes(token)) ||
|
|
42
|
+
null
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 行编辑器的补全器:输入以 / 开头且尚未输入参数时给出候选。
|
|
48
|
+
* 返回 { items: [{ label, hint, apply }], title, hint }
|
|
49
|
+
*/
|
|
50
|
+
function completer(value, cursor) {
|
|
51
|
+
if (!value || !value.startsWith('/')) return null;
|
|
52
|
+
if (value.includes('\n')) return null;
|
|
53
|
+
const before = value.slice(0, cursor);
|
|
54
|
+
if (before.includes(' ')) return null;
|
|
55
|
+
const query = before.slice(1).toLowerCase();
|
|
56
|
+
const items = SLASH_COMMANDS.filter(
|
|
57
|
+
(cmd) =>
|
|
58
|
+
!query ||
|
|
59
|
+
cmd.name.slice(1).toLowerCase().startsWith(query) ||
|
|
60
|
+
(cmd.aliases || []).some((alias) => alias.slice(1).toLowerCase().startsWith(query))
|
|
61
|
+
).map((cmd) => ({
|
|
62
|
+
label: cmd.name,
|
|
63
|
+
hint: cmd.desc,
|
|
64
|
+
apply: `${cmd.name} `,
|
|
65
|
+
command: cmd
|
|
66
|
+
}));
|
|
67
|
+
if (!items.length) return null;
|
|
68
|
+
return {
|
|
69
|
+
title: t('menu_title'),
|
|
70
|
+
hint: t('menu_hint'),
|
|
71
|
+
items
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { SLASH_COMMANDS, findCommand, completer };
|
package/src/cli/ui.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 上下文用量(对应桌面版的上下文圆环)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { estimateChatTokens, contextLimit, formatTokens } = require('../core/sessions');
|
|
8
|
+
const { COLORS, ring, truncate, displayWidth, termWidth } = require('./ansi');
|
|
9
|
+
|
|
10
|
+
function contextInfo(chat, model) {
|
|
11
|
+
const used = estimateChatTokens(chat);
|
|
12
|
+
const limit = contextLimit(model);
|
|
13
|
+
const ratio = limit > 0 ? Math.min(1, used / limit) : 0;
|
|
14
|
+
const messageCount = (chat?.messages || []).filter((msg) => msg.role !== 'terminal').length;
|
|
15
|
+
return { used, limit, ratio, messageCount, model, skill: chat?.skill || '' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 输入框下方的上下文环(单行) */
|
|
19
|
+
function ringLine(info) {
|
|
20
|
+
const percent = Math.round(info.ratio * 100);
|
|
21
|
+
return `${ring(info.ratio)} ${COLORS.faint(`${formatTokens(info.used)}/${formatTokens(info.limit)} · ${percent}%`)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** /context 命令的详细输出 */
|
|
25
|
+
function contextReport(info) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const percent = (info.ratio * 100).toFixed(info.ratio < 0.1 ? 1 : 0);
|
|
28
|
+
lines.push(`${COLORS.accent('Context')} ${COLORS.bold(`${percent}%`)} ${COLORS.faint(
|
|
29
|
+
`${formatTokens(info.used)} / ${formatTokens(info.limit)}`
|
|
30
|
+
)}`);
|
|
31
|
+
const width = Math.max(10, Math.min(40, termWidth() - 12));
|
|
32
|
+
const filled = Math.round(info.ratio * width);
|
|
33
|
+
lines.push(`${COLORS.accent('█'.repeat(filled))}${COLORS.faint('░'.repeat(Math.max(0, width - filled)))}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
const rows = [
|
|
36
|
+
['Messages', String(info.messageCount)],
|
|
37
|
+
['Context limit', formatTokens(info.limit)],
|
|
38
|
+
['Remaining', formatTokens(Math.max(0, info.limit - info.used))],
|
|
39
|
+
['Model', info.model?.name || 'none'],
|
|
40
|
+
['Provider', info.model?.providerName || ''],
|
|
41
|
+
['Skill', info.skill || 'none']
|
|
42
|
+
];
|
|
43
|
+
const labelWidth = Math.max(...rows.map(([label]) => displayWidth(label)));
|
|
44
|
+
for (const [label, value] of rows) {
|
|
45
|
+
lines.push(` ${COLORS.faint(truncate(label, 12).padEnd(labelWidth))} ${value}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(COLORS.faint('Estimated from character counts; real usage comes from the model. Use /compress when near the limit.'));
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { contextInfo, ringLine, contextReport };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** 子命令之间共享的小工具。 */
|
|
4
|
+
|
|
5
|
+
const sessions = require('../core/sessions');
|
|
6
|
+
|
|
7
|
+
function resolveChatArg(opts = {}, runtime = {}) {
|
|
8
|
+
if (opts.resume || runtime.resume) return sessions.getChat(String(opts.resume || runtime.resume));
|
|
9
|
+
if (opts.continue || runtime.continueLast) return sessions.listChats()[0] || null;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 失败时给出“下一步”提示而不是直接抛错 */
|
|
14
|
+
function requireProviders(config) {
|
|
15
|
+
if (!config.providers?.length) {
|
|
16
|
+
throw Object.assign(new Error('还没有配置供应商。运行 dgxay config init 开始配置'), { userError: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { resolveChatArg, requireProviders };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* chat:会话管理与在新会话中提问。
|
|
5
|
+
*
|
|
6
|
+
* dgxay chat new 新建会话
|
|
7
|
+
* dgxay chat list 列出历史会话
|
|
8
|
+
* dgxay chat show <序号|ID> 查看某个会话
|
|
9
|
+
* dgxay chat resume <序号|ID> 进入交互模式并恢复该会话
|
|
10
|
+
* dgxay chat rm <序号|ID> 删除会话
|
|
11
|
+
* dgxay chat rename <序号|ID> <标题>
|
|
12
|
+
* dgxay chat clear 清空全部会话
|
|
13
|
+
* dgxay chat "问题" 在最近会话上继续提问
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const print = require('../cli/print');
|
|
18
|
+
const { parseArgs, actionFirst } = require('../cli/args');
|
|
19
|
+
const { COLORS, renderMarkdown, termWidth } = require('../cli/ansi');
|
|
20
|
+
const sessions = require('../core/sessions');
|
|
21
|
+
|
|
22
|
+
const options = [
|
|
23
|
+
{ name: 'print', alias: 'p', type: 'string', valueName: 'text', desc: 'Ask directly (same as dgxay run)' },
|
|
24
|
+
{ name: 'mode', alias: 'm', type: 'string', valueName: 'mode', desc: 'chat / work' },
|
|
25
|
+
{ name: 'permission', type: 'string', valueName: 'level', desc: 'none / normal / auto / full' },
|
|
26
|
+
{ name: 'workspace', alias: 'C', type: 'string', valueName: 'dir', desc: 'Workspace directory' },
|
|
27
|
+
{ name: 'yes', alias: 'y', type: 'boolean', desc: 'Auto-approve permission requests' },
|
|
28
|
+
{ name: 'yes-all', type: 'boolean', desc: 'Skip confirmation when clearing conversations' }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function relativeTime(ts) {
|
|
32
|
+
const diff = Date.now() - Number(ts || 0);
|
|
33
|
+
if (diff < 60000) return 'just now';
|
|
34
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
|
|
35
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)} h ago`;
|
|
36
|
+
const date = new Date(ts);
|
|
37
|
+
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listChats() {
|
|
41
|
+
const chats = sessions.listChats();
|
|
42
|
+
print.blank();
|
|
43
|
+
if (!chats.length) {
|
|
44
|
+
print.hint('No saved conversations yet');
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
print.heading(`Conversation history (${chats.length}, at most ${sessions.MAX_CHATS} kept)`);
|
|
48
|
+
const rows = chats.map((chat, index) => {
|
|
49
|
+
const messages = chat.messages.filter((m) => m.role !== 'terminal');
|
|
50
|
+
const last = messages[messages.length - 1];
|
|
51
|
+
return [
|
|
52
|
+
String(index + 1),
|
|
53
|
+
chat.title || 'New conversation',
|
|
54
|
+
String(messages.length),
|
|
55
|
+
relativeTime(chat.updatedAt),
|
|
56
|
+
chat.mode === 'work' ? 'work' : 'chat',
|
|
57
|
+
chat.skill || '',
|
|
58
|
+
last ? String(last.content || '').replace(/\s+/g, ' ').slice(0, 40) : ''
|
|
59
|
+
];
|
|
60
|
+
});
|
|
61
|
+
print.table(
|
|
62
|
+
[
|
|
63
|
+
{ title: '#', max: 3 },
|
|
64
|
+
{ title: 'Title', max: 30 },
|
|
65
|
+
{ title: 'Messages', max: 4 },
|
|
66
|
+
{ title: 'Updated', max: 10 },
|
|
67
|
+
{ title: 'Mode', max: 4 },
|
|
68
|
+
{ title: 'Skill', max: 12 },
|
|
69
|
+
{ title: 'Last message', max: 40 }
|
|
70
|
+
],
|
|
71
|
+
rows
|
|
72
|
+
);
|
|
73
|
+
print.blank();
|
|
74
|
+
print.hint('dgxay chat resume 1 resume conversation 1');
|
|
75
|
+
print.hint('dgxay -r 1 resume and enter interactive mode');
|
|
76
|
+
print.hint('dgxay chat show 1 view contents only');
|
|
77
|
+
print.blank();
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showChat(ref) {
|
|
82
|
+
const chat = sessions.resolveChat(ref);
|
|
83
|
+
if (!chat) {
|
|
84
|
+
print.fail(`Conversation not found: ${ref}`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
print.blank();
|
|
88
|
+
print.write(`${COLORS.bold(chat.title)} ${COLORS.faint(`${chat.id} · ${relativeTime(chat.updatedAt)}`)}`);
|
|
89
|
+
for (const message of chat.messages) {
|
|
90
|
+
if (message.role === 'user') {
|
|
91
|
+
print.write(`\n${COLORS.accent('▍You')}`);
|
|
92
|
+
print.write(message.content || '');
|
|
93
|
+
} else if (message.role === 'assistant') {
|
|
94
|
+
print.write(`\n${COLORS.accent('▍DgxayCode')}`);
|
|
95
|
+
process.stdout.write(renderMarkdown(message.content || '', { width: termWidth() }));
|
|
96
|
+
if (message.error) print.write(COLORS.red(message.error));
|
|
97
|
+
if (message.tools?.length) {
|
|
98
|
+
for (const tool of message.tools) {
|
|
99
|
+
print.write(COLORS.faint(` · ${tool.name} ${tool.ok ? 'ok' : 'failed'}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else if (message.role === 'terminal') {
|
|
103
|
+
print.write(`\n${COLORS.faint(`▍Terminal $ ${message.command}`)}`);
|
|
104
|
+
const output = `${message.output || ''}${message.stderr ? `\n${message.stderr}` : ''}`;
|
|
105
|
+
print.write(output.slice(0, 4000));
|
|
106
|
+
print.write(COLORS.faint(` exit=${message.exitCode ?? '?'}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
print.blank();
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function run({ argv, runtime }) {
|
|
114
|
+
const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
|
|
115
|
+
const action = rawAction || 'list';
|
|
116
|
+
|
|
117
|
+
switch (action) {
|
|
118
|
+
case 'list':
|
|
119
|
+
case 'ls':
|
|
120
|
+
return listChats();
|
|
121
|
+
|
|
122
|
+
case 'show':
|
|
123
|
+
case 'view': {
|
|
124
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay chat show <index|id>'), { userError: true });
|
|
125
|
+
return showChat(rest[0]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'new': {
|
|
129
|
+
print.ok(opts.print ? 'New conversation started (single question, not saved to history)' : 'A conversation is created automatically when interactive mode starts');
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'resume': {
|
|
134
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay chat resume <index|id>'), { userError: true });
|
|
135
|
+
const chat = sessions.resolveChat(rest[0]);
|
|
136
|
+
if (!chat) {
|
|
137
|
+
print.fail(`Conversation not found: ${rest[0]}`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
const repl = require('../cli/repl');
|
|
141
|
+
return repl.start({ ...runtime, resume: chat.id, single: false });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'rm':
|
|
145
|
+
case 'delete': {
|
|
146
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay chat rm <index|id>'), { userError: true });
|
|
147
|
+
const chat = sessions.resolveChat(rest[0]);
|
|
148
|
+
if (!chat) {
|
|
149
|
+
print.fail(`Conversation not found: ${rest[0]}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
sessions.deleteChat(chat.id);
|
|
153
|
+
print.ok(`Deleted "${chat.title}"`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'rename': {
|
|
158
|
+
const ref = rest[0];
|
|
159
|
+
const title = rest.slice(1).join(' ').trim();
|
|
160
|
+
if (!ref || !title) throw Object.assign(new Error('Usage: dgxay chat rename <index|id> <new title>'), { userError: true });
|
|
161
|
+
const chat = sessions.resolveChat(ref);
|
|
162
|
+
if (!chat) {
|
|
163
|
+
print.fail(`Conversation not found: ${ref}`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
chat.title = title;
|
|
167
|
+
chat.updatedAt = Date.now();
|
|
168
|
+
sessions.saveChat(chat);
|
|
169
|
+
print.ok(`Renamed to "${title}"`);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'clear': {
|
|
174
|
+
if (!opts.yesAll && process.stdin.isTTY) {
|
|
175
|
+
const { confirm } = require('../services/input');
|
|
176
|
+
const ok = await confirm('Clear all conversation history? (cannot be undone)', false);
|
|
177
|
+
if (!ok) {
|
|
178
|
+
print.hint('Cancelled');
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
sessions.clearChats();
|
|
183
|
+
print.ok('All conversations cleared');
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
default: {
|
|
188
|
+
// 非子命令:当作提示词,在最近会话上继续提问
|
|
189
|
+
const prompt = positionals.join(' ').trim();
|
|
190
|
+
const repl = require('../cli/repl');
|
|
191
|
+
return repl.start({ ...runtime, initialPrompt: prompt, single: true, continueLast: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
name: 'chat',
|
|
198
|
+
aliases: ['c'],
|
|
199
|
+
summary: 'Manage conversations: list, show, resume, delete',
|
|
200
|
+
usage: 'dgxay chat <list|show|resume|rm|rename|clear>',
|
|
201
|
+
options,
|
|
202
|
+
subcommands: [
|
|
203
|
+
['list', 'List conversation history'],
|
|
204
|
+
['show <index|id>', 'View conversation contents'],
|
|
205
|
+
['resume <index|id>', 'Resume in interactive mode'],
|
|
206
|
+
['rm <index|id>', 'Delete a conversation'],
|
|
207
|
+
['rename <index|id> <title>', 'Rename a conversation'],
|
|
208
|
+
['clear', 'Clear all conversations']
|
|
209
|
+
],
|
|
210
|
+
examples: ['dgxay chat list', 'dgxay chat resume 1', 'dgxay chat rm 2', 'dgxay chat show 1'],
|
|
211
|
+
run
|
|
212
|
+
};
|