dgxay-code-cli 1.0.0 → 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/README-zh.md +50 -8
- package/README.md +50 -9
- package/package.json +1 -1
- package/src/cli/ansi.js +38 -8
- package/src/cli/approval.js +11 -11
- package/src/cli/editor.js +456 -202
- package/src/cli/i18n.js +276 -0
- package/src/cli/index.js +34 -34
- package/src/cli/print.js +5 -5
- package/src/cli/render.js +147 -76
- package/src/cli/repl.js +205 -141
- package/src/cli/slash.js +29 -28
- package/src/cli/ui.js +8 -8
- package/src/commands/chat.js +45 -45
- package/src/commands/config.js +81 -81
- package/src/commands/context.js +5 -5
- package/src/commands/doctor.js +34 -34
- package/src/commands/export.js +13 -13
- package/src/commands/mcp.js +51 -51
- package/src/commands/model.js +38 -38
- package/src/commands/oauth.js +61 -61
- package/src/commands/permission.js +19 -19
- package/src/commands/provider.js +70 -70
- package/src/commands/run.js +20 -20
- package/src/commands/skill.js +15 -15
- package/src/commands/workspace.js +18 -18
- package/src/core/agent.js +19 -18
- package/src/core/llm.js +10 -10
- package/src/core/mcp.js +5 -5
- package/src/core/oauth.js +36 -36
- package/src/core/prompt.js +46 -46
- package/src/core/sessions.js +7 -7
- package/src/core/tools.js +24 -24
- package/src/services/input.js +1 -1
- package/src/services/settings.js +28 -28
- package/src/services/skills.js +1 -1
- package/src/services/terminal.js +3 -3
package/src/core/prompt.js
CHANGED
|
@@ -10,80 +10,80 @@ const oauth = require('./oauth');
|
|
|
10
10
|
const REVIEW_TIMEOUT = 25000;
|
|
11
11
|
|
|
12
12
|
const REVIEW_SYSTEM = [
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'allow
|
|
16
|
-
'review
|
|
17
|
-
'
|
|
13
|
+
'You are the local-operation safety reviewer for DgxayCode. Before the AI assistant is allowed to act on the user\'s machine, you decide whether an operation could harm the computer or leak data.',
|
|
14
|
+
'Output JSON only, no code fences: {"decision":"allow","reason":"..."} or {"decision":"review","reason":"..."}',
|
|
15
|
+
'allow: reading files directly relevant to the task, writing ordinary text files inside the workspace, listing directories, read-only queries, fetching public documentation.',
|
|
16
|
+
'review: deleting or overwriting important files, touching system or user directories outside the workspace, installing/uninstalling/elevating/services/bulk scripts, reading .ssh/.env/keys/browser data, uploading local data, ambiguous commands or targets, anything irreversible.',
|
|
17
|
+
'When unsure, choose review. Keep the reason under 60 characters.'
|
|
18
18
|
].join('\n');
|
|
19
19
|
|
|
20
20
|
function systemPromptFor({ mode, permission, workspace, customPrompt, skill }) {
|
|
21
21
|
const parts = [];
|
|
22
22
|
parts.push(
|
|
23
|
-
'
|
|
23
|
+
'You are the AI assistant built into the DgxayCode command-line tool. Always answer in English unless the user writes in another language. Be clear and concise, and use Markdown where it helps.'
|
|
24
24
|
);
|
|
25
25
|
if (skill?.content) {
|
|
26
|
-
parts.push(
|
|
26
|
+
parts.push(`[Skill enabled for this conversation: ${skill.name}]\n${skill.content}`);
|
|
27
27
|
}
|
|
28
28
|
if (customPrompt && String(customPrompt).trim()) {
|
|
29
|
-
parts.push(
|
|
29
|
+
parts.push(`User instructions:\n${String(customPrompt).trim()}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (mode !== 'work') {
|
|
33
|
-
parts.push('
|
|
33
|
+
parts.push('You are in "chat mode": no filesystem or shell tools are available. Answer in prose.');
|
|
34
34
|
if (permission !== 'none') {
|
|
35
35
|
parts.push(
|
|
36
|
-
'
|
|
36
|
+
'If the user explicitly asks to install an MCP server you may call install_mcp_server to save the launch configuration, then tell them to run dgxay mcp start.'
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
return parts.join('\n\n');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
parts.push(
|
|
42
|
+
parts.push(`You are in "work mode". Workspace directory: ${workspace || '(not selected)'}.`);
|
|
43
43
|
if (permission === 'none') {
|
|
44
44
|
parts.push(
|
|
45
|
-
'
|
|
46
|
-
'
|
|
45
|
+
'Permission level: none. You may only read files inside the workspace. Writing, creating, deleting, network access and shell commands are all forbidden. ' +
|
|
46
|
+
'If the user asks for any of those, explain that they need to switch to normal, auto-review or full permission.'
|
|
47
47
|
);
|
|
48
48
|
} else if (permission === 'normal') {
|
|
49
49
|
parts.push(
|
|
50
|
-
'
|
|
51
|
-
'
|
|
50
|
+
'Permission level: normal. You have every capability: read/write any file, run shell commands, access the network. ' +
|
|
51
|
+
'Every tool call requires explicit user approval in the terminal first; if the user declines, do not retry — answer with what you already know.'
|
|
52
52
|
);
|
|
53
53
|
} else if (permission === 'auto') {
|
|
54
54
|
parts.push(
|
|
55
|
-
'
|
|
56
|
-
'
|
|
55
|
+
'Permission level: auto review. You have every capability: read/write any file, run shell commands, access the network. ' +
|
|
56
|
+
'Each operation is first judged by the reviewer model; safe ones run automatically and suspicious ones are handed to the user. If declined, do not retry.'
|
|
57
57
|
);
|
|
58
58
|
} else {
|
|
59
59
|
parts.push(
|
|
60
|
-
'
|
|
60
|
+
'Permission level: full access. You may run shell commands, touch any file and access the network without review. Confirm destructive actions with the user first.'
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
parts.push(
|
|
65
|
-
'
|
|
65
|
+
'MCP: if the user explicitly asks to install an MCP server, call install_mcp_server to save it, then tell them to run dgxay mcp start (or use /mcp). Never fabricate the result.'
|
|
66
66
|
);
|
|
67
67
|
|
|
68
68
|
if (workspace) {
|
|
69
69
|
parts.push(
|
|
70
70
|
[
|
|
71
|
-
'
|
|
72
|
-
'1.
|
|
73
|
-
'2.
|
|
74
|
-
'3.
|
|
71
|
+
'Work-mode rules:',
|
|
72
|
+
'1. Whenever you need to inspect or change files, call a tool — never assume file contents. Read before you write.',
|
|
73
|
+
'2. Save every generated file inside the workspace, preferring relative paths.',
|
|
74
|
+
'3. When the task is done, summarize in a short list which files you created or modified.',
|
|
75
75
|
'',
|
|
76
|
-
'
|
|
77
|
-
'•
|
|
78
|
-
'•
|
|
79
|
-
'•
|
|
80
|
-
'•
|
|
81
|
-
'•
|
|
82
|
-
'•
|
|
76
|
+
'[Execution discipline]',
|
|
77
|
+
'• Break the goal into actionable steps and start calling tools immediately. Do not output only a plan or a promise ("I will..." / "Next I will...").',
|
|
78
|
+
'• Do not stop until the task is genuinely finished, and do not wait for the user to say "continue". After each tool result, proceed to the next step automatically.',
|
|
79
|
+
'• When the task needs several reads, writes or commands, run them all before wrapping up; do not stop after one step.',
|
|
80
|
+
'• On tool errors, analyse the cause and retry differently. Only stop when information is missing, permission was denied, or the task truly cannot continue — and state the exact blocker.',
|
|
81
|
+
'• Verify before finishing: re-read key files, check command output, or run the relevant tests.',
|
|
82
|
+
'• Then report what was done and how it was verified. Never end with "let me know if you want me to continue".'
|
|
83
83
|
].join('\n')
|
|
84
84
|
);
|
|
85
85
|
} else {
|
|
86
|
-
parts.push('
|
|
86
|
+
parts.push('No workspace is selected: do not call file tools. Ask the user to pick a directory with /cd.');
|
|
87
87
|
}
|
|
88
88
|
return parts.join('\n\n');
|
|
89
89
|
}
|
|
@@ -101,7 +101,7 @@ function summarizeArgs(args) {
|
|
|
101
101
|
if (typeof args !== 'object') return String(args).slice(0, 1800);
|
|
102
102
|
const copy = { ...args };
|
|
103
103
|
if (typeof copy.content === 'string' && copy.content.length > 1200) {
|
|
104
|
-
copy.content = `${copy.content.slice(0, 1200)}
|
|
104
|
+
copy.content = `${copy.content.slice(0, 1200)}… (${copy.content.length} chars total)`;
|
|
105
105
|
}
|
|
106
106
|
let text = JSON.stringify(copy);
|
|
107
107
|
if (text.length > 2000) text = `${text.slice(0, 2000)}…`;
|
|
@@ -130,7 +130,7 @@ function hasReviewModel(config) {
|
|
|
130
130
|
async function reviewOperation({ config, providerId, kind, name, args, command, workspace }) {
|
|
131
131
|
const reviewer = resolveReviewer(config, providerId);
|
|
132
132
|
if (!reviewer) {
|
|
133
|
-
return { decision: 'review', reason: '
|
|
133
|
+
return { decision: 'review', reason: 'No review model configured — asking you instead', reviewer: null, modelId: null };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const controller = new AbortController();
|
|
@@ -148,12 +148,12 @@ async function reviewOperation({ config, providerId, kind, name, args, command,
|
|
|
148
148
|
{
|
|
149
149
|
role: 'user',
|
|
150
150
|
content: [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
command ?
|
|
154
|
-
|
|
155
|
-
args ?
|
|
156
|
-
'
|
|
151
|
+
`Operation type: ${kind === 'terminal' ? 'shell command' : 'tool call'}`,
|
|
152
|
+
`Operation: ${name}`,
|
|
153
|
+
command ? `Command: ${command}` : '',
|
|
154
|
+
`Workspace: ${workspace || '(none)'}`,
|
|
155
|
+
args ? `Arguments: ${summarizeArgs(args)}` : '',
|
|
156
|
+
'Is this operation safe?'
|
|
157
157
|
]
|
|
158
158
|
.filter(Boolean)
|
|
159
159
|
.join('\n')
|
|
@@ -167,7 +167,7 @@ async function reviewOperation({ config, providerId, kind, name, args, command,
|
|
|
167
167
|
|
|
168
168
|
const text = String(result.content || '').trim();
|
|
169
169
|
let decision = 'review';
|
|
170
|
-
let reason = text.slice(0, 160) || '
|
|
170
|
+
let reason = text.slice(0, 160) || 'The reviewer gave no verdict';
|
|
171
171
|
const left = text.indexOf('{');
|
|
172
172
|
const right = text.lastIndexOf('}');
|
|
173
173
|
if (left !== -1 && right > left) {
|
|
@@ -190,7 +190,7 @@ async function reviewOperation({ config, providerId, kind, name, args, command,
|
|
|
190
190
|
// 超时、网络失败或返回无法解析时一律按“可疑”处理,转交用户确认
|
|
191
191
|
return {
|
|
192
192
|
decision: 'review',
|
|
193
|
-
reason:
|
|
193
|
+
reason: `Auto-review failed: ${err.message}`,
|
|
194
194
|
reviewer: reviewer.provider.name,
|
|
195
195
|
modelId: reviewer.modelId,
|
|
196
196
|
source: reviewer.source
|
|
@@ -214,7 +214,7 @@ function looksUnfinishedWork(result) {
|
|
|
214
214
|
async function compressMessages({ config, providerId, modelId, messages }) {
|
|
215
215
|
const provider = (config.providers || []).find((item) => item.id === providerId) || (config.providers || [])[0];
|
|
216
216
|
const targetModel = modelId || provider?.models?.[0]?.id;
|
|
217
|
-
if (!provider || !targetModel) throw new Error('
|
|
217
|
+
if (!provider || !targetModel) throw new Error('No model available to compress the context');
|
|
218
218
|
|
|
219
219
|
const history = (Array.isArray(messages) ? messages : []).filter(
|
|
220
220
|
(m) => m && ['user', 'assistant'].includes(m.role) && typeof m.content === 'string' && m.content.trim()
|
|
@@ -222,7 +222,7 @@ async function compressMessages({ config, providerId, modelId, messages }) {
|
|
|
222
222
|
if (!history.length) return { ok: true, summary: '', usage: null };
|
|
223
223
|
|
|
224
224
|
const transcript = history
|
|
225
|
-
.map((message) => `${message.role === 'user' ? '
|
|
225
|
+
.map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${message.content}`)
|
|
226
226
|
.join('\n\n')
|
|
227
227
|
.slice(-150000);
|
|
228
228
|
|
|
@@ -236,12 +236,12 @@ async function compressMessages({ config, providerId, modelId, messages }) {
|
|
|
236
236
|
messages: [
|
|
237
237
|
{
|
|
238
238
|
role: 'system',
|
|
239
|
-
content: '
|
|
239
|
+
content: 'You compress conversations into a summary that lets work continue. Output only the summary body: no greeting, no code fences.'
|
|
240
240
|
},
|
|
241
241
|
{
|
|
242
242
|
role: 'user',
|
|
243
243
|
content:
|
|
244
|
-
'
|
|
244
|
+
'Compress the conversation below. Keep: user goals and preferences, key facts and numbers, decisions made, file paths and code locations, open items, and important tool results. Drop greetings, repetition and noise. Write in English, at most 800 words.\n\n' +
|
|
245
245
|
transcript
|
|
246
246
|
}
|
|
247
247
|
],
|
|
@@ -251,7 +251,7 @@ async function compressMessages({ config, providerId, modelId, messages }) {
|
|
|
251
251
|
});
|
|
252
252
|
|
|
253
253
|
const summary = String(result.content || '').trim();
|
|
254
|
-
if (!summary) throw new Error('
|
|
254
|
+
if (!summary) throw new Error('The model returned no summary, try again later');
|
|
255
255
|
return { ok: true, summary, usage: result.usage || null };
|
|
256
256
|
}
|
|
257
257
|
|
package/src/core/sessions.js
CHANGED
|
@@ -22,7 +22,7 @@ function titleFrom(text) {
|
|
|
22
22
|
const cleaned = String(text || '')
|
|
23
23
|
.replace(/\s+/g, ' ')
|
|
24
24
|
.trim();
|
|
25
|
-
if (!cleaned) return '
|
|
25
|
+
if (!cleaned) return 'New conversation';
|
|
26
26
|
return cleaned.length > 26 ? `${cleaned.slice(0, 26)}…` : cleaned;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ function normalizeChat(chat) {
|
|
|
30
30
|
if (!chat || typeof chat !== 'object') return null;
|
|
31
31
|
return {
|
|
32
32
|
id: String(chat.id || newId()),
|
|
33
|
-
title: String(chat.title || '
|
|
33
|
+
title: String(chat.title || 'New conversation'),
|
|
34
34
|
mode: chat.mode === 'work' ? 'work' : 'chat',
|
|
35
35
|
skill: typeof chat.skill === 'string' ? chat.skill : '',
|
|
36
36
|
createdAt: Number(chat.createdAt) || Date.now(),
|
|
@@ -64,7 +64,7 @@ function resolveChat(ref, chats = listChats()) {
|
|
|
64
64
|
const prefixMatches = chats.filter((chat) => chat.id.startsWith(text));
|
|
65
65
|
if (prefixMatches.length === 1) return prefixMatches[0];
|
|
66
66
|
if (prefixMatches.length > 1) {
|
|
67
|
-
const error = new Error(
|
|
67
|
+
const error = new Error(`Ambiguous id "${text}" matches ${prefixMatches.length} conversations; use a longer id`);
|
|
68
68
|
error.code = 'AMBIGUOUS';
|
|
69
69
|
throw error;
|
|
70
70
|
}
|
|
@@ -74,7 +74,7 @@ function resolveChat(ref, chats = listChats()) {
|
|
|
74
74
|
/** 保存单段会话(插入或更新),返回保存后的会话 */
|
|
75
75
|
function saveChat(chat) {
|
|
76
76
|
const normalized = normalizeChat(chat);
|
|
77
|
-
if (!normalized) throw new Error('
|
|
77
|
+
if (!normalized) throw new Error('Invalid conversation');
|
|
78
78
|
if (!normalized.messages.length) return normalized;
|
|
79
79
|
const chats = listChats();
|
|
80
80
|
const index = chats.findIndex((item) => item.id === normalized.id);
|
|
@@ -101,7 +101,7 @@ function clearChats() {
|
|
|
101
101
|
function createChat(overrides = {}) {
|
|
102
102
|
return {
|
|
103
103
|
id: newId('chat'),
|
|
104
|
-
title: '
|
|
104
|
+
title: 'New conversation',
|
|
105
105
|
mode: overrides.mode === 'work' ? 'work' : 'chat',
|
|
106
106
|
skill: typeof overrides.skill === 'string' ? overrides.skill : '',
|
|
107
107
|
createdAt: Date.now(),
|
|
@@ -117,13 +117,13 @@ function buildHistory(chat) {
|
|
|
117
117
|
return (chat?.messages || [])
|
|
118
118
|
.map((msg) => {
|
|
119
119
|
if (!msg || !msg.role) return null;
|
|
120
|
-
if (msg.compressed) return { role: 'user', content: `[
|
|
120
|
+
if (msg.compressed) return { role: 'user', content: `[Context summary]\n${String(msg.content || '').replace(/^> Context compressed[^\n]*\n+/, '')}` };
|
|
121
121
|
if (msg.role === 'terminal') {
|
|
122
122
|
const output = `${msg.output || ''}${msg.stderr ? `\n${msg.stderr}` : ''}`.slice(0, 4000);
|
|
123
123
|
return {
|
|
124
124
|
role: 'user',
|
|
125
125
|
content: `[终端命令] ${msg.command}\n[输出]\n${output}\n[退出码] ${
|
|
126
|
-
msg.exitCode ?? (msg.status === 'running' ? '
|
|
126
|
+
msg.exitCode ?? (msg.status === 'running' ? 'running' : 'unknown')
|
|
127
127
|
}`
|
|
128
128
|
};
|
|
129
129
|
}
|
package/src/core/tools.js
CHANGED
|
@@ -185,7 +185,7 @@ function sameOrInside(root, target) {
|
|
|
185
185
|
|
|
186
186
|
function resolveTarget(input, workspace) {
|
|
187
187
|
const raw = String(input || '').trim();
|
|
188
|
-
if (!raw) throw new Error('
|
|
188
|
+
if (!raw) throw new Error('Missing path argument');
|
|
189
189
|
const base = workspace || os.homedir();
|
|
190
190
|
return path.normalize(path.isAbsolute(raw) ? raw : path.resolve(base, raw));
|
|
191
191
|
}
|
|
@@ -200,19 +200,19 @@ function displayPath(abs, workspace) {
|
|
|
200
200
|
|
|
201
201
|
function assertReadable(target, ctx) {
|
|
202
202
|
if (ctx.permission === 'none') {
|
|
203
|
-
if (!ctx.workspace) throw new Error('
|
|
203
|
+
if (!ctx.workspace) throw new Error('No workspace selected in read-only mode: cannot read files');
|
|
204
204
|
if (!sameOrInside(ctx.workspace, target)) {
|
|
205
|
-
throw new Error('
|
|
205
|
+
throw new Error('Read-only mode only allows files inside the workspace');
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
function assertWritable(target, ctx) {
|
|
211
|
-
if (ctx.permission === 'none') throw new Error('
|
|
211
|
+
if (ctx.permission === 'none') throw new Error('Read-only mode forbids modifying files');
|
|
212
212
|
const roots = [path.parse(target).root, os.homedir(), ctx.workspace || ''].filter(Boolean);
|
|
213
213
|
for (const root of roots) {
|
|
214
214
|
if (path.normalize(target).toLowerCase() === path.normalize(root).toLowerCase()) {
|
|
215
|
-
throw new Error('
|
|
215
|
+
throw new Error('Refusing to write to or delete a root directory');
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
}
|
|
@@ -223,7 +223,7 @@ async function walkTree(dir, depth, prefix, acc, limit) {
|
|
|
223
223
|
try {
|
|
224
224
|
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
225
225
|
} catch (err) {
|
|
226
|
-
acc.push(`${prefix}[
|
|
226
|
+
acc.push(`${prefix}[cannot read: ${err.code || err.message}]`);
|
|
227
227
|
return;
|
|
228
228
|
}
|
|
229
229
|
entries.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
@@ -232,7 +232,7 @@ async function walkTree(dir, depth, prefix, acc, limit) {
|
|
|
232
232
|
.slice(0, 80);
|
|
233
233
|
for (const entry of visible) {
|
|
234
234
|
if (acc.length >= limit) {
|
|
235
|
-
acc.push(`${prefix}... (
|
|
235
|
+
acc.push(`${prefix}... (truncated)`);
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
238
238
|
const full = path.join(dir, entry.name);
|
|
@@ -255,23 +255,23 @@ async function walkTree(dir, depth, prefix, acc, limit) {
|
|
|
255
255
|
|
|
256
256
|
function truncate(text, limit) {
|
|
257
257
|
if (text.length <= limit) return text;
|
|
258
|
-
return `${text.slice(0, limit)}\n\n... (
|
|
258
|
+
return `${text.slice(0, limit)}\n\n... (output truncated, ${text.length} chars total)`;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
async function execute(name, args, ctx) {
|
|
262
262
|
const permission = ctx.permission || 'none';
|
|
263
263
|
const allowed = PERMISSION_TOOLS[permission] || [];
|
|
264
264
|
if (!allowed.includes(name)) {
|
|
265
|
-
throw new Error(
|
|
265
|
+
throw new Error(`Permission level (${permission}) does not allow tool ${name}`);
|
|
266
266
|
}
|
|
267
267
|
const workspace = ctx.workspace || '';
|
|
268
268
|
|
|
269
269
|
if (permission === 'normal' || permission === 'auto') {
|
|
270
270
|
if (typeof ctx.requestApproval !== 'function') {
|
|
271
|
-
throw new Error('
|
|
271
|
+
throw new Error('Missing approval callback for this permission level — blocked');
|
|
272
272
|
}
|
|
273
273
|
const ok = await ctx.requestApproval({ name, args });
|
|
274
|
-
if (!ok) throw new Error('
|
|
274
|
+
if (!ok) throw new Error('The user declined this operation — do not retry');
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
switch (name) {
|
|
@@ -279,7 +279,7 @@ async function execute(name, args, ctx) {
|
|
|
279
279
|
const target = resolveTarget(args.path || '.', workspace);
|
|
280
280
|
assertReadable(target, ctx);
|
|
281
281
|
const stat = await fsp.stat(target);
|
|
282
|
-
if (!stat.isDirectory()) throw new Error(`${displayPath(target, workspace)}
|
|
282
|
+
if (!stat.isDirectory()) throw new Error(`${displayPath(target, workspace)} is not a directory`);
|
|
283
283
|
const acc = [`${displayPath(target, workspace)}/`];
|
|
284
284
|
await walkTree(target, Math.min(Math.max(Number(args.depth) || 1, 1), 3), ' ', acc, 200);
|
|
285
285
|
return acc.join('\n');
|
|
@@ -288,10 +288,10 @@ async function execute(name, args, ctx) {
|
|
|
288
288
|
const target = resolveTarget(args.path, workspace);
|
|
289
289
|
assertReadable(target, ctx);
|
|
290
290
|
const stat = await fsp.stat(target);
|
|
291
|
-
if (stat.isDirectory()) throw new Error('
|
|
292
|
-
if (stat.size > 2 * 1024 * 1024) throw new Error('
|
|
291
|
+
if (stat.isDirectory()) throw new Error('That path is a directory — use list_directory');
|
|
292
|
+
if (stat.size > 2 * 1024 * 1024) throw new Error('File too large (>2MB) — use a shell command instead');
|
|
293
293
|
const content = await fsp.readFile(target, 'utf8');
|
|
294
|
-
return
|
|
294
|
+
return `File: ${displayPath(target, workspace)}\n\n${truncate(content, MAX_READ)}`;
|
|
295
295
|
}
|
|
296
296
|
case 'write_file': {
|
|
297
297
|
const target = resolveTarget(args.path, workspace);
|
|
@@ -308,13 +308,13 @@ async function execute(name, args, ctx) {
|
|
|
308
308
|
await fsp.writeFile(target, content, 'utf8');
|
|
309
309
|
const bytes = Buffer.byteLength(content, 'utf8');
|
|
310
310
|
ctx.onFile?.({ action, path: target, display: displayPath(target, workspace), bytes });
|
|
311
|
-
return
|
|
311
|
+
return `${action === 'created' ? 'Created' : 'Updated'} ${displayPath(target, workspace)} (${bytes} bytes)`;
|
|
312
312
|
}
|
|
313
313
|
case 'create_directory': {
|
|
314
314
|
const target = resolveTarget(args.path, workspace);
|
|
315
315
|
assertWritable(target, ctx);
|
|
316
316
|
await fsp.mkdir(target, { recursive: true });
|
|
317
|
-
return
|
|
317
|
+
return `Created directory ${displayPath(target, workspace)}`;
|
|
318
318
|
}
|
|
319
319
|
case 'delete_path': {
|
|
320
320
|
const target = resolveTarget(args.path, workspace);
|
|
@@ -322,7 +322,7 @@ async function execute(name, args, ctx) {
|
|
|
322
322
|
const stat = await fsp.stat(target);
|
|
323
323
|
await fsp.rm(target, { recursive: stat.isDirectory(), force: false });
|
|
324
324
|
ctx.onFile?.({ action: 'deleted', path: target, display: displayPath(target, workspace), bytes: 0 });
|
|
325
|
-
return
|
|
325
|
+
return `Deleted ${displayPath(target, workspace)}`;
|
|
326
326
|
}
|
|
327
327
|
case 'run_command': {
|
|
328
328
|
const cwd = args.cwd ? resolveTarget(args.cwd, workspace) : workspace || os.homedir();
|
|
@@ -335,8 +335,8 @@ async function execute(name, args, ctx) {
|
|
|
335
335
|
const parts = [`$ ${args.command}`];
|
|
336
336
|
if (stdout) parts.push(String(stdout).trim());
|
|
337
337
|
if (stderr) parts.push(`[stderr]\n${String(stderr).trim()}`);
|
|
338
|
-
if (err) parts.push(`[exit] code=${err.code ?? 'unknown'} ${err.killed ? '(
|
|
339
|
-
if (!stdout && !stderr && !err) parts.push('(
|
|
338
|
+
if (err) parts.push(`[exit] code=${err.code ?? 'unknown'} ${err.killed ? '(timeout or killed)' : ''}`);
|
|
339
|
+
if (!stdout && !stderr && !err) parts.push('(no output)');
|
|
340
340
|
resolve(truncate(parts.join('\n\n'), MAX_OUTPUT));
|
|
341
341
|
}
|
|
342
342
|
);
|
|
@@ -348,9 +348,9 @@ async function execute(name, args, ctx) {
|
|
|
348
348
|
try {
|
|
349
349
|
parsed = new URL(url);
|
|
350
350
|
} catch {
|
|
351
|
-
throw new Error('URL
|
|
351
|
+
throw new Error('Invalid URL');
|
|
352
352
|
}
|
|
353
|
-
if (!/^https?:$/.test(parsed.protocol)) throw new Error('
|
|
353
|
+
if (!/^https?:$/.test(parsed.protocol)) throw new Error('Only http/https is supported');
|
|
354
354
|
const controller = new AbortController();
|
|
355
355
|
const timer = setTimeout(() => controller.abort(), 30000);
|
|
356
356
|
try {
|
|
@@ -379,12 +379,12 @@ async function execute(name, args, ctx) {
|
|
|
379
379
|
env: args.env,
|
|
380
380
|
cwd: args.cwd
|
|
381
381
|
});
|
|
382
|
-
return
|
|
382
|
+
return `Installed MCP server "${server.name}" (command: ${server.command} ${(server.args || []).join(
|
|
383
383
|
' '
|
|
384
384
|
)})。请用 dgxay mcp start ${server.id} 或在 /mcp 面板中启动。`;
|
|
385
385
|
}
|
|
386
386
|
default:
|
|
387
|
-
throw new Error(
|
|
387
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
|
package/src/services/input.js
CHANGED
|
@@ -74,7 +74,7 @@ async function confirm(prompt, defaultYes = false) {
|
|
|
74
74
|
if (answer === null) return false;
|
|
75
75
|
const text = answer.trim().toLowerCase();
|
|
76
76
|
if (!text) return defaultYes;
|
|
77
|
-
return text === 'y' || text === 'yes'
|
|
77
|
+
return text === 'y' || text === 'yes';
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** 单行状态提示(覆盖当前行,避免刷新时刷屏) */
|
package/src/services/settings.js
CHANGED
|
@@ -15,24 +15,24 @@ const { listModels } = require('../core/llm');
|
|
|
15
15
|
|
|
16
16
|
const PERMISSIONS = {
|
|
17
17
|
none: {
|
|
18
|
-
label: '
|
|
19
|
-
short: '
|
|
20
|
-
desc: '
|
|
18
|
+
label: 'No permission',
|
|
19
|
+
short: 'read-only',
|
|
20
|
+
desc: 'Read-only access inside the workspace'
|
|
21
21
|
},
|
|
22
22
|
normal: {
|
|
23
|
-
label: '
|
|
24
|
-
short: '
|
|
25
|
-
desc: '
|
|
23
|
+
label: 'Normal',
|
|
24
|
+
short: 'normal',
|
|
25
|
+
desc: 'All capabilities, but every operation asks you'
|
|
26
26
|
},
|
|
27
27
|
auto: {
|
|
28
|
-
label: '
|
|
29
|
-
short: '
|
|
30
|
-
desc: '
|
|
28
|
+
label: 'Auto review',
|
|
29
|
+
short: 'auto',
|
|
30
|
+
desc: 'All capabilities; a reviewer model decides, suspicious ones ask you'
|
|
31
31
|
},
|
|
32
32
|
full: {
|
|
33
|
-
label: '
|
|
34
|
-
short: '
|
|
35
|
-
desc: '
|
|
33
|
+
label: 'Full access',
|
|
34
|
+
short: 'full',
|
|
35
|
+
desc: 'Run commands, touch any file, unrestricted network'
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -42,21 +42,21 @@ const PROTOCOL_META = {
|
|
|
42
42
|
full: 'OpenAI ChatCompletions',
|
|
43
43
|
path: '/v1/chat/completions',
|
|
44
44
|
basePlaceholder: 'https://api.openai.com/v1',
|
|
45
|
-
modelPlaceholder: '
|
|
45
|
+
modelPlaceholder: 'e.g. gpt-4o, deepseek-chat'
|
|
46
46
|
},
|
|
47
47
|
anthropic: {
|
|
48
48
|
short: 'Messages',
|
|
49
49
|
full: 'Anthropic Messages',
|
|
50
50
|
path: '/v1/messages',
|
|
51
51
|
basePlaceholder: 'https://api.anthropic.com',
|
|
52
|
-
modelPlaceholder: '
|
|
52
|
+
modelPlaceholder: 'e.g. claude-sonnet-4-5'
|
|
53
53
|
},
|
|
54
54
|
responses: {
|
|
55
55
|
short: 'Responses',
|
|
56
56
|
full: 'OpenAI Responses API',
|
|
57
57
|
path: '/v1/responses',
|
|
58
58
|
basePlaceholder: 'https://api.openai.com/v1',
|
|
59
|
-
modelPlaceholder: '
|
|
59
|
+
modelPlaceholder: 'e.g. gpt-5, o4-mini'
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
62
|
|
|
@@ -137,28 +137,28 @@ function upsertProvider(input = {}) {
|
|
|
137
137
|
const config = readConfig();
|
|
138
138
|
const list = config.providers || [];
|
|
139
139
|
const existing = input.ref ? findProvider(input.ref) : null;
|
|
140
|
-
if (input.ref && !existing) throw new Error(
|
|
140
|
+
if (input.ref && !existing) throw new Error(`Provider not found: ${input.ref}`);
|
|
141
141
|
|
|
142
142
|
const protocol = PROTOCOLS.includes(input.protocol) ? input.protocol : existing?.protocol || 'openai';
|
|
143
143
|
const baseUrl = String(input.baseUrl || existing?.baseUrl || '').trim().replace(/\/+$/, '');
|
|
144
|
-
if (!/^https?:\/\//i.test(baseUrl)) throw new Error('Base URL
|
|
144
|
+
if (!/^https?:\/\//i.test(baseUrl)) throw new Error('Base URL must start with http:// or https://');
|
|
145
145
|
|
|
146
146
|
const models = normalizeModels(input.models, existing?.models);
|
|
147
|
-
if (!models.length) throw new Error('
|
|
147
|
+
if (!models.length) throw new Error('Provide at least one model id (--model)');
|
|
148
148
|
|
|
149
149
|
const authMode = input.authMode === 'oauth' ? 'oauth' : 'apiKey';
|
|
150
150
|
const oauthProvider = authMode === 'oauth' ? String(input.oauthProvider || '').trim() : '';
|
|
151
151
|
if (authMode === 'oauth') {
|
|
152
|
-
if (!oauthProvider) throw new Error('
|
|
152
|
+
if (!oauthProvider) throw new Error('Pass --oauth-provider (openai/anthropic/xai/google) to use an OAuth account');
|
|
153
153
|
const account = oauth.listProviders().find((item) => item.id === oauthProvider);
|
|
154
154
|
if (!account?.connection || account.connection.expired) {
|
|
155
|
-
throw new Error(`${oauthProvider}
|
|
155
|
+
throw new Error(`${oauthProvider} is not connected or the login expired. Run dgxay oauth connect ${oauthProvider}`);
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
const record = {
|
|
160
160
|
id: existing?.id || newId('prov'),
|
|
161
|
-
name: String(input.name || existing?.name || '
|
|
161
|
+
name: String(input.name || existing?.name || 'My provider').trim(),
|
|
162
162
|
protocol,
|
|
163
163
|
baseUrl,
|
|
164
164
|
apiKey: input.apiKey !== undefined ? String(input.apiKey) : existing?.apiKey || '',
|
|
@@ -205,7 +205,7 @@ function normalizeModels(input, fallback = []) {
|
|
|
205
205
|
function removeProvider(ref) {
|
|
206
206
|
const config = readConfig();
|
|
207
207
|
const target = findProvider(ref);
|
|
208
|
-
if (!target) throw new Error(
|
|
208
|
+
if (!target) throw new Error(`Provider not found: ${ref}`);
|
|
209
209
|
const next = (config.providers || []).filter((p) => p.id !== target.id);
|
|
210
210
|
const patch = { providers: next };
|
|
211
211
|
if (config.activeProviderId === target.id) {
|
|
@@ -219,7 +219,7 @@ function moveProvider(ref, direction) {
|
|
|
219
219
|
const config = readConfig();
|
|
220
220
|
const list = (config.providers || []).slice();
|
|
221
221
|
const target = findProvider(ref);
|
|
222
|
-
if (!target) throw new Error(
|
|
222
|
+
if (!target) throw new Error(`Provider not found: ${ref}`);
|
|
223
223
|
const index = list.findIndex((p) => p.id === target.id);
|
|
224
224
|
const nextIndex = direction < 0 ? index - 1 : index + 1;
|
|
225
225
|
if (nextIndex < 0 || nextIndex >= list.length) return config;
|
|
@@ -231,15 +231,15 @@ function moveProvider(ref, direction) {
|
|
|
231
231
|
function setActiveModel(providerRef, modelRef) {
|
|
232
232
|
const config = readConfig();
|
|
233
233
|
const provider = providerRef ? findProvider(providerRef) : activeProvider(config);
|
|
234
|
-
if (!provider) throw new Error('
|
|
234
|
+
if (!provider) throw new Error('No provider available');
|
|
235
235
|
const models = provider.models || [];
|
|
236
236
|
const model = models.find((m) => m.id === modelRef) || models.find((m) => m.id.includes(String(modelRef)));
|
|
237
|
-
if (!model) throw new Error(
|
|
237
|
+
if (!model) throw new Error(`Provider ${provider.name} has no model ${modelRef}`);
|
|
238
238
|
return saveConfig({ activeProviderId: provider.id, activeModelId: model.id });
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function setPermission(permission) {
|
|
242
|
-
if (!PERMISSIONS[permission]) throw new Error(
|
|
242
|
+
if (!PERMISSIONS[permission]) throw new Error(`Unknown permission: ${permission}`);
|
|
243
243
|
return saveConfig({ ui: { permission } });
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -256,7 +256,7 @@ function setWorkspace(dir) {
|
|
|
256
256
|
const target = dir ? path.resolve(dir) : '';
|
|
257
257
|
if (target) {
|
|
258
258
|
const stat = fs.statSync(target);
|
|
259
|
-
if (!stat.isDirectory()) throw new Error(`${target}
|
|
259
|
+
if (!stat.isDirectory()) throw new Error(`${target} is not a directory`);
|
|
260
260
|
}
|
|
261
261
|
const recents = (config.work?.recentWorkspaces || []).filter((item) => item !== target);
|
|
262
262
|
if (target) recents.unshift(target);
|
|
@@ -274,7 +274,7 @@ async function fetchModels({ providerRef, baseUrl, apiKey, protocol }) {
|
|
|
274
274
|
const provider = providerRef ? findProvider(providerRef) : activeProvider(config);
|
|
275
275
|
const targetBaseUrl = baseUrl || provider?.baseUrl;
|
|
276
276
|
const targetProtocol = protocol || provider?.protocol || 'openai';
|
|
277
|
-
if (!targetBaseUrl) throw new Error('
|
|
277
|
+
if (!targetBaseUrl) throw new Error('Missing Base URL');
|
|
278
278
|
let token = apiKey ?? provider?.apiKey ?? '';
|
|
279
279
|
let authType = 'apiKey';
|
|
280
280
|
if (provider && provider.authMode === 'oauth' && provider.oauthProvider && apiKey === undefined) {
|
package/src/services/skills.js
CHANGED
|
@@ -111,7 +111,7 @@ function readSkill(name) {
|
|
|
111
111
|
const skillName = String(meta.name || fallbackName).trim();
|
|
112
112
|
if (skillName.toLowerCase() !== target) continue;
|
|
113
113
|
const content =
|
|
114
|
-
body.length > MAX_SKILL_CONTENT ? `${body.slice(0, MAX_SKILL_CONTENT)}\n\n
|
|
114
|
+
body.length > MAX_SKILL_CONTENT ? `${body.slice(0, MAX_SKILL_CONTENT)}\n\n… (skill content truncated)` : body;
|
|
115
115
|
return {
|
|
116
116
|
name: skillName,
|
|
117
117
|
description: String(meta.description || describe(body) || '').trim(),
|
package/src/services/terminal.js
CHANGED
|
@@ -83,7 +83,7 @@ function runShellCommand({ command, workspace, onOutput }) {
|
|
|
83
83
|
if (total > MAX_TOTAL) {
|
|
84
84
|
if (!truncated) {
|
|
85
85
|
truncated = true;
|
|
86
|
-
const note = '\n
|
|
86
|
+
const note = '\n… (output too long, truncated)\n';
|
|
87
87
|
stderr += note;
|
|
88
88
|
onOutput?.(note, 'stderr');
|
|
89
89
|
}
|
|
@@ -105,13 +105,13 @@ function runShellCommand({ command, workspace, onOutput }) {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
child.on('error', (err) => {
|
|
108
|
-
onOutput?.(`\n[
|
|
108
|
+
onOutput?.(`\n[failed to start] ${err.message}\n`, 'stderr');
|
|
109
109
|
finish(-1, null);
|
|
110
110
|
});
|
|
111
111
|
child.on('exit', (code, signal) => finish(code, signal));
|
|
112
112
|
|
|
113
113
|
const timer = setTimeout(() => {
|
|
114
|
-
onOutput?.('\n
|
|
114
|
+
onOutput?.('\n… (command ran longer than 5 minutes, terminated)\n', 'stderr');
|
|
115
115
|
killProcessTree(child);
|
|
116
116
|
}, TIMEOUT_MS);
|
|
117
117
|
child.on('exit', () => clearTimeout(timer));
|