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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 授权确认组件(对应桌面版 permission:request 弹窗)。
|
|
5
|
+
*
|
|
6
|
+
* - 普通权限:每次操作都必须确认;
|
|
7
|
+
* - 自动审查:模型放行则直接执行,可疑操作转到这里;
|
|
8
|
+
* - 非交互模式(管道输入)下默认拒绝,避免无人值守时被 AI 静默执行;
|
|
9
|
+
* - 按 Esc 或回答 n 视为拒绝;等待超时 180 秒同样按拒绝处理。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const readline = require('readline');
|
|
13
|
+
const { COLORS, displayWidth, truncate, termWidth, wrapText } = require('./ansi');
|
|
14
|
+
|
|
15
|
+
const APPROVAL_TIMEOUT = 180000;
|
|
16
|
+
|
|
17
|
+
function frameText(lines, options = {}) {
|
|
18
|
+
const width = Math.min(options.width || 70, Math.max(24, termWidth() - 2));
|
|
19
|
+
const title = options.title || '';
|
|
20
|
+
const accentText = (text) => COLORS.accent(text);
|
|
21
|
+
const body = [];
|
|
22
|
+
if (title) {
|
|
23
|
+
const head = ` ${title} `;
|
|
24
|
+
const fill = Math.max(0, width - displayWidth(head) - 2);
|
|
25
|
+
body.push(`${accentText('┌')}${COLORS.bold(head)}${COLORS.accent(`${'─'.repeat(fill)}┐`)}`);
|
|
26
|
+
} else {
|
|
27
|
+
body.push(COLORS.accent(`┌${'─'.repeat(width)}┐`));
|
|
28
|
+
}
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const text = typeof line === 'string' ? line : line.text;
|
|
31
|
+
const raw = typeof line === 'string' ? text : line.raw || text;
|
|
32
|
+
const visible = displayWidth(raw);
|
|
33
|
+
const pad = Math.max(0, width - visible - 2);
|
|
34
|
+
body.push(`${COLORS.accent('│')} ${text}${' '.repeat(pad)} ${COLORS.accent('│')}`);
|
|
35
|
+
}
|
|
36
|
+
body.push(COLORS.accent(`└${'─'.repeat(width)}┘`));
|
|
37
|
+
return body.join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function question(prompt) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
43
|
+
rl.question(prompt, (answer) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
resolve(answer);
|
|
46
|
+
});
|
|
47
|
+
rl.on('SIGINT', () => {
|
|
48
|
+
rl.close();
|
|
49
|
+
resolve('');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 单键读取(y / n / Esc / Enter),非 TTY 时回退到整行输入 */
|
|
55
|
+
function readKey({ timeout = APPROVAL_TIMEOUT } = {}) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
if (!process.stdin.isTTY) {
|
|
58
|
+
question('').then((answer) => {
|
|
59
|
+
const text = String(answer || '').trim().toLowerCase();
|
|
60
|
+
resolve({ key: text, line: text });
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let done = false;
|
|
65
|
+
readline.emitKeypressEvents(process.stdin);
|
|
66
|
+
if (process.stdin.isTTY) {
|
|
67
|
+
try {
|
|
68
|
+
process.stdin.setRawMode(true);
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const finish = (result) => {
|
|
74
|
+
if (done) return;
|
|
75
|
+
done = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
process.stdin.removeListener('keypress', onKey);
|
|
78
|
+
if (process.stdin.isTTY) {
|
|
79
|
+
try {
|
|
80
|
+
process.stdin.setRawMode(false);
|
|
81
|
+
} catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
resolve(result);
|
|
86
|
+
};
|
|
87
|
+
const onKey = (str, key = {}) => {
|
|
88
|
+
if (key.ctrl && key.name === 'c') {
|
|
89
|
+
finish({ key: 'escape', line: '' });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (key.name === 'escape') {
|
|
93
|
+
finish({ key: 'escape', line: '' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (key.name === 'return' || key.name === 'enter' || str === '\r' || str === '\n') {
|
|
97
|
+
finish({ key: 'submit', line: '' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!key.ctrl && !key.meta && str) {
|
|
101
|
+
const text = String(str).toLowerCase();
|
|
102
|
+
if (text === 'y' || text === 'n') {
|
|
103
|
+
finish({ key: text, line: text });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (text === 'q') {
|
|
107
|
+
finish({ key: 'escape', line: '' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// 其他字符先忽略,避免误触
|
|
111
|
+
process.stdout.write('\x07');
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const timer = setTimeout(() => finish({ key: 'timeout', line: '' }), timeout);
|
|
115
|
+
process.stdin.on('keypress', onKey);
|
|
116
|
+
process.stdin.resume();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 请求用户授权一次操作。
|
|
122
|
+
* @returns {Promise<boolean>}
|
|
123
|
+
*/
|
|
124
|
+
async function askApproval(request, options = {}) {
|
|
125
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
126
|
+
if (!interactive) {
|
|
127
|
+
// 非交互:一律拒绝,并提示如何绕过
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`${COLORS.yellow('[permission]')} ${request.title}:${request.operation}\n` +
|
|
130
|
+
`${COLORS.dim(' Not an interactive terminal — denied. Use --permission full or run interactively.')}\n`
|
|
131
|
+
);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push({ text: `${COLORS.faint('op')} ${COLORS.bold(request.operation)}`, raw: `op ${request.operation}` });
|
|
137
|
+
for (const line of String(request.detail || '').split('\n')) {
|
|
138
|
+
if (!line.trim()) continue;
|
|
139
|
+
const idx = line.indexOf(':');
|
|
140
|
+
if (idx > 0) {
|
|
141
|
+
const label = line.slice(0, idx);
|
|
142
|
+
const value = line.slice(idx + 1);
|
|
143
|
+
for (const piece of wrapText(value, 56)) {
|
|
144
|
+
lines.push({
|
|
145
|
+
text: `${COLORS.faint(truncate(label, 6))} ${piece}`,
|
|
146
|
+
raw: `${label} ${piece}`
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
for (const piece of wrapText(line, 60)) lines.push({ text: piece, raw: piece });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (request.workspace) {
|
|
154
|
+
lines.push({ text: '', raw: '' });
|
|
155
|
+
lines.push({ text: COLORS.faint(`workspace ${request.workspace}`), raw: `workspace ${request.workspace}` });
|
|
156
|
+
}
|
|
157
|
+
if (request.review) {
|
|
158
|
+
lines.push({ text: '', raw: '' });
|
|
159
|
+
const who = request.reviewer ? `reviewer ${request.reviewer}` : 'auto-review';
|
|
160
|
+
lines.push({ text: COLORS.red(`${who} flagged this`), raw: `${who} flagged this` });
|
|
161
|
+
for (const piece of wrapText(request.review, 58)) {
|
|
162
|
+
lines.push({ text: COLORS.dim(` ${piece}`), raw: ` ${piece}` });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
process.stdout.write(`${frameText(lines, { title: request.title })}\n`);
|
|
167
|
+
|
|
168
|
+
const promptText = `${COLORS.accent('Allow this?')} ${COLORS.faint('[y] yes [n] no')} `;
|
|
169
|
+
process.stdout.write(promptText);
|
|
170
|
+
const result = await readKey({ timeout: options.timeout || APPROVAL_TIMEOUT });
|
|
171
|
+
|
|
172
|
+
if (result.key === 'y') {
|
|
173
|
+
process.stdout.write(`${COLORS.green('allowed')}\n`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
const label =
|
|
177
|
+
result.key === 'timeout'
|
|
178
|
+
? COLORS.yellow('timed out — denied')
|
|
179
|
+
: result.key === 'escape'
|
|
180
|
+
? COLORS.dim('denied')
|
|
181
|
+
: COLORS.red('denied');
|
|
182
|
+
process.stdout.write(`${label}\n`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { askApproval, readKey, question, frameText, APPROVAL_TIMEOUT };
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 轻量参数解析:支持 --flag、--flag value、--flag=value、-abc 组合、-- 终止符。
|
|
5
|
+
*
|
|
6
|
+
* 每个命令声明自己的选项:{ name, alias, type: 'string'|'boolean'|'number'|'list', desc, values }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function camel(name) {
|
|
10
|
+
return String(name).replace(/-([a-z])/g, (m, c) => c.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv, spec = [], parseOptions = {}) {
|
|
14
|
+
const allowUnknown = Boolean(parseOptions.allowUnknown);
|
|
15
|
+
const options = {};
|
|
16
|
+
const positionals = [];
|
|
17
|
+
const lookup = new Map();
|
|
18
|
+
for (const item of spec) {
|
|
19
|
+
lookup.set(`--${item.name}`, item);
|
|
20
|
+
if (item.alias) {
|
|
21
|
+
for (const alias of [].concat(item.alias)) lookup.set(alias.length === 1 ? `-${alias}` : alias, item);
|
|
22
|
+
}
|
|
23
|
+
if (item.type === 'list') options[camel(item.name)] = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rest = argv.slice();
|
|
27
|
+
while (rest.length) {
|
|
28
|
+
const token = rest.shift();
|
|
29
|
+
if (token === '--') {
|
|
30
|
+
positionals.push(...rest);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
// -h / -v 是约定俗成的通用开关;但命令自己声明了同名短选项时以命令为准
|
|
34
|
+
const takesHelp = !lookup.has('-h');
|
|
35
|
+
const takesVersion = !lookup.has('-v');
|
|
36
|
+
if ((token === '-h' || token === '--help') && takesHelp) {
|
|
37
|
+
options.help = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if ((token === '-v' || token === '--version') && (takesVersion || token === '--version')) {
|
|
41
|
+
options.version = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (token.startsWith('--')) {
|
|
46
|
+
const eq = token.indexOf('=');
|
|
47
|
+
const name = eq === -1 ? token : token.slice(0, eq);
|
|
48
|
+
const inline = eq === -1 ? undefined : token.slice(eq + 1);
|
|
49
|
+
const item = lookup.get(name);
|
|
50
|
+
if (!item) {
|
|
51
|
+
if (allowUnknown) {
|
|
52
|
+
// 全局解析阶段:不认识的选项原样保留,交给子命令自己解析
|
|
53
|
+
positionals.push(token);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const error = new Error(`未知选项:${name}`);
|
|
57
|
+
error.userError = true;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const key = camel(item.name);
|
|
61
|
+
if (item.type === 'boolean') {
|
|
62
|
+
options[key] = inline === undefined ? true : !/^(false|0|no)$/i.test(inline);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
let value = inline;
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
value = rest.shift();
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
const error = new Error(`选项 ${name} 需要一个值`);
|
|
70
|
+
error.userError = true;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (item.type === 'list') options[key] = options[key].concat([value]);
|
|
75
|
+
else if (item.type === 'number') options[key] = Number(value);
|
|
76
|
+
else options[key] = value;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 任意 -x / -abc 组合短选项(未知的仍然报错,避免把 -5 这类输入吞掉)
|
|
81
|
+
if (/^-[A-Za-z]/.test(token)) {
|
|
82
|
+
// 组合短选项:-abc,只有最后一位可以带值
|
|
83
|
+
const chars = token.slice(1).split('');
|
|
84
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
85
|
+
const item = lookup.get(`-${chars[i]}`);
|
|
86
|
+
if (!item) {
|
|
87
|
+
if (allowUnknown) {
|
|
88
|
+
positionals.push(token);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
const error = new Error(`未知选项:-${chars[i]}`);
|
|
92
|
+
error.userError = true;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const key = camel(item.name);
|
|
96
|
+
if (item.type === 'boolean') {
|
|
97
|
+
options[key] = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const value = i === chars.length - 1 ? rest.shift() : chars.slice(i + 1).join('');
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
const error = new Error(`选项 -${chars[i]} 需要一个值`);
|
|
103
|
+
error.userError = true;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (item.type === 'list') options[key] = options[key].concat([value]);
|
|
107
|
+
else if (item.type === 'number') options[key] = Number(value);
|
|
108
|
+
else options[key] = value;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
positionals.push(token);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { options, positionals };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 多级子命令解析:第一个位置参数是动作名,剩余参数才按 spec 解析选项。
|
|
122
|
+
* 返回 { action, rest, options, positionals }
|
|
123
|
+
*/
|
|
124
|
+
function actionFirst(argv, spec = []) {
|
|
125
|
+
const list = argv.slice();
|
|
126
|
+
const actionSpec = {};
|
|
127
|
+
for (const item of Array.isArray(spec) ? spec : []) {
|
|
128
|
+
if (item && item.name) actionSpec[item.name] = item;
|
|
129
|
+
}
|
|
130
|
+
const defaultAction = spec && spec.action && typeof spec.action === 'string' ? spec.action : '';
|
|
131
|
+
const actionIndex = list.findIndex((token) => !token.startsWith('-'));
|
|
132
|
+
const action = actionIndex === -1 ? defaultAction : list[actionIndex];
|
|
133
|
+
if (actionIndex !== -1) list.splice(actionIndex, 1);
|
|
134
|
+
const parsed = parseArgs(list, Object.values(actionSpec));
|
|
135
|
+
return { action, rest: parsed.positionals, options: parsed.options, positionals: parsed.positionals };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 简单选择器:把用户输入按名称或序号解析成列表中的一项。
|
|
140
|
+
* 序号 1 表示第一项(与 /chat list 的输出保持一致)。
|
|
141
|
+
*/
|
|
142
|
+
function pick(list, ref, label = '项目') {
|
|
143
|
+
if (!list.length) return null;
|
|
144
|
+
if (!ref) return list[0];
|
|
145
|
+
const text = String(ref).trim();
|
|
146
|
+
const index = Number(text);
|
|
147
|
+
if (Number.isInteger(index) && index >= 1 && index <= list.length) return list[index - 1];
|
|
148
|
+
const exact = list.find((item) => item.id === text || String(item.name || '').toLowerCase() === text.toLowerCase());
|
|
149
|
+
if (exact) return exact;
|
|
150
|
+
const partial = list.filter(
|
|
151
|
+
(item) => String(item.id).startsWith(text) || String(item.name || '').toLowerCase().startsWith(text.toLowerCase())
|
|
152
|
+
);
|
|
153
|
+
if (partial.length === 1) return partial[0];
|
|
154
|
+
if (partial.length > 1) {
|
|
155
|
+
const error = new Error(`${label}名称不唯一:「${text}」匹配到 ${partial.length} 项`);
|
|
156
|
+
error.userError = true;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { parseArgs, actionFirst, pick, camel };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 子命令注册表。每个命令:{ name, aliases, summary, usage, options, run(ctx) }
|
|
5
|
+
* ctx = { argv, io, config, print, error, exitCode }
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chat = require('../commands/chat');
|
|
9
|
+
const config = require('../commands/config');
|
|
10
|
+
const provider = require('../commands/provider');
|
|
11
|
+
const model = require('../commands/model');
|
|
12
|
+
const oauth = require('../commands/oauth');
|
|
13
|
+
const mcp = require('../commands/mcp');
|
|
14
|
+
const skill = require('../commands/skill');
|
|
15
|
+
const workspace = require('../commands/workspace');
|
|
16
|
+
const permission = require('../commands/permission');
|
|
17
|
+
const context = require('../commands/context');
|
|
18
|
+
const run = require('../commands/run');
|
|
19
|
+
const exportCmd = require('../commands/export');
|
|
20
|
+
const doctor = require('../commands/doctor');
|
|
21
|
+
|
|
22
|
+
const COMMANDS = [
|
|
23
|
+
run,
|
|
24
|
+
chat,
|
|
25
|
+
config,
|
|
26
|
+
provider,
|
|
27
|
+
model,
|
|
28
|
+
oauth,
|
|
29
|
+
mcp,
|
|
30
|
+
skill,
|
|
31
|
+
workspace,
|
|
32
|
+
permission,
|
|
33
|
+
context,
|
|
34
|
+
exportCmd,
|
|
35
|
+
doctor
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function findCommand(name) {
|
|
39
|
+
const target = String(name || '').toLowerCase();
|
|
40
|
+
return (
|
|
41
|
+
COMMANDS.find((cmd) => cmd.name === target) ||
|
|
42
|
+
COMMANDS.find((cmd) => (cmd.aliases || []).includes(target)) ||
|
|
43
|
+
null
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 汇总所有子命令的选项(用于全局解析后分发给命令自身) */
|
|
48
|
+
function specFor(command) {
|
|
49
|
+
return command?.options || [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { COMMANDS, findCommand, specFor };
|