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.
@@ -10,80 +10,80 @@ const oauth = require('./oauth');
10
10
  const REVIEW_TIMEOUT = 25000;
11
11
 
12
12
  const REVIEW_SYSTEM = [
13
- '你是 DgxayCode 的本机操作安全审查模型。AI 助手在获得用户授权前,由你判断一次操作是否可能伤害用户的电脑或泄露数据。',
14
- '只输出 JSON,不要代码块:{"decision":"allow","reason":"..."} 或 {"decision":"review","reason":"..."}',
15
- 'allow 适用于:读取与任务直接相关的文件、在工作空间内写入普通文本文件、查看目录、执行只读查询、访问公开文档。',
16
- 'review 适用于:删除或覆盖重要文件、修改工作空间外的系统/用户目录、安装/卸载/提权/服务/批量脚本、访问 .ssh/.env/密钥/浏览器数据、上传本地数据到外部、命令或目标不明确、可能不可逆。',
17
- '不确定时选择 review。理由用简体中文,不超过 60 字。'
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
- '你是 DgxayCode 命令行版内置的 AI 助手。回答使用简体中文(除非用户使用其他语言),结构清晰,尽量简洁,合理使用 Markdown。'
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(`【本对话启用的技能:${skill.name}】\n${skill.content}`);
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(`用户自定义指令:\n${String(customPrompt).trim()}`);
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
- '如果用户明确要求安装 MCP server,可以调用 install_mcp_server 保存启动配置,并提示用户运行 dgxay mcp start 启动。'
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(`当前为「工作模式」。工作空间目录:${workspace || '(尚未选择)'}。`);
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
- '安装 MCP:如果用户明确要求安装 MCP server,调用 install_mcp_server 保存启动配置,完成后提示用户运行 dgxay mcp start 或在 /mcp 面板中启动。不要伪造安装结果。'
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. 需要查看或修改文件时,必须调用工具,不要凭空假设文件内容;修改前先 read_file。',
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('尚未选择工作空间时,请不要调用任何文件工具,提示用户先用 /cd 选择工作目录。');
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)}…(共 ${copy.content.length} 字符)`;
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: '尚未配置自动审查模型,需要你确认', reviewer: null, modelId: null };
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
- `操作类型:${kind === 'terminal' ? '终端命令' : '工具调用'}`,
152
- `操作名称:${name}`,
153
- command ? `命令:${command}` : '',
154
- `工作空间:${workspace || '(未选择)'}`,
155
- args ? `参数:${summarizeArgs(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: `自动审查失败:${err.message}`,
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' ? '用户' : '助手'}:${message.content}`)
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
- '请压缩下面的对话。必须保留:用户目标与偏好、关键事实与数字、已做出的决定、涉及的文件路径与代码位置、未完成事项、重要工具结果。删除寒暄、重复解释和无关内容。用简体中文输出,控制在 800 字以内。\n\n' +
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
 
@@ -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(`会话编号不唯一:「${text}」匹配到 ${prefixMatches.length} 个会话,请输入更长的 id`);
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: `[上下文摘要]\n${String(msg.content || '').replace(/^> 上下文已压缩[^\n]*\n+/, '')}` };
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}[无法读取: ${err.code || err.message}]`);
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... (输出已截断,共 ${text.length} 字符)`;
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(`当前权限模式(${permission})不允许调用工具 ${name}`);
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('该路径是文件夹,请使用 list_directory');
292
- if (stat.size > 2 * 1024 * 1024) throw new Error('文件过大(>2MB),请使用终端命令处理');
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 `文件:${displayPath(target, workspace)}\n\n${truncate(content, MAX_READ)}`;
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 `已${action === 'created' ? '创建' : '更新'}文件 ${displayPath(target, workspace)}(${bytes} 字节)`;
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 `已创建文件夹 ${displayPath(target, workspace)}`;
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 `已删除 ${displayPath(target, workspace)}`;
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('仅支持 http/https 协议');
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 `已安装 MCP server「${server.name}」(启动命令:${server.command} ${(server.args || []).join(
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(`未知工具:${name}`);
387
+ throw new Error(`Unknown tool: ${name}`);
388
388
  }
389
389
  }
390
390
 
@@ -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' || text === '是';
77
+ return text === 'y' || text === 'yes';
78
78
  }
79
79
 
80
80
  /** 单行状态提示(覆盖当前行,避免刷新时刷屏) */
@@ -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: '如 gpt-4o、deepseek-chat'
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: '如 claude-sonnet-4-5'
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: '如 gpt-5、o4-mini'
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(`找不到供应商:${input.ref}`);
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 必须以 http:// 或 https:// 开头');
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('请至少提供一个模型 ID(--model)');
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('使用 OAuth 账户时必须指定 --oauth-provider(openai/anthropic/xai/google)');
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} 账户尚未连接或登录已过期,请先运行 dgxay oauth connect ${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 || '我的供应商').trim(),
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(`找不到供应商:${ref}`);
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(`找不到供应商:${ref}`);
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(`供应商 ${provider.name} 下没有模型 ${modelRef}`);
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(`未知权限:${permission}`);
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('缺少 Base URL');
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) {
@@ -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…(技能内容已截断)` : body;
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(),
@@ -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…(输出过长,已截断)\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[启动失败] ${err.message}\n`, 'stderr');
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…(命令执行超过 5 分钟,已终止)\n', 'stderr');
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));