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/agent.js
CHANGED
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
summarizeArgs
|
|
33
33
|
} = require('./prompt');
|
|
34
34
|
const { trimHistory } = require('./sessions');
|
|
35
|
+
const { t } = require('../cli/i18n');
|
|
35
36
|
|
|
36
37
|
const HISTORY_LIMIT = 24;
|
|
37
38
|
const MAX_STEPS_CHAT = 8;
|
|
@@ -43,16 +44,16 @@ const VALID_PERMISSIONS = ['none', 'normal', 'auto', 'full'];
|
|
|
43
44
|
function describeOperation({ kind, name, args, command }) {
|
|
44
45
|
const lines = [];
|
|
45
46
|
if (kind === 'terminal') {
|
|
46
|
-
lines.push(
|
|
47
|
+
lines.push(`Command: ${command || ''}`);
|
|
47
48
|
return lines.join('\n');
|
|
48
49
|
}
|
|
49
|
-
lines.push(
|
|
50
|
-
if (args?.path) lines.push(
|
|
51
|
-
if (args?.url) lines.push(
|
|
52
|
-
if (args?.command) lines.push(
|
|
53
|
-
if (args?.name) lines.push(
|
|
50
|
+
lines.push(`Tool: ${name}`);
|
|
51
|
+
if (args?.path) lines.push(`Path: ${args.path}`);
|
|
52
|
+
if (args?.url) lines.push(`URL: ${args.url}`);
|
|
53
|
+
if (args?.command) lines.push(`Command: ${args.command}`);
|
|
54
|
+
if (args?.name) lines.push(`Name: ${args.name}`);
|
|
54
55
|
const body = summarizeArgs(args);
|
|
55
|
-
if (body && body !== '{}') lines.push(
|
|
56
|
+
if (body && body !== '{}') lines.push(`Arguments: ${body}`);
|
|
56
57
|
return lines.join('\n');
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -82,7 +83,7 @@ function resolvePermissions(config) {
|
|
|
82
83
|
async function requestOperationApproval({ config, permission, kind, name, args, command, workspace, providerId, toolId, askUser, emit }) {
|
|
83
84
|
if (permission === 'full') return { allow: true, source: 'full', review: null };
|
|
84
85
|
if (permission !== 'normal' && permission !== 'auto') {
|
|
85
|
-
return { allow: false, source: 'none', review: null, reason: '
|
|
86
|
+
return { allow: false, source: 'none', review: null, reason: 'Current permission level forbids this operation' };
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
if (permission === 'normal') {
|
|
@@ -90,7 +91,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
|
|
|
90
91
|
const allowed = await askUser({
|
|
91
92
|
kind,
|
|
92
93
|
permission,
|
|
93
|
-
title: kind === 'terminal' ? '
|
|
94
|
+
title: kind === 'terminal' ? t('approval_terminal') : t('approval_tool'),
|
|
94
95
|
operation: name,
|
|
95
96
|
detail: describeOperation({ kind, name, args, command }),
|
|
96
97
|
command: command || '',
|
|
@@ -99,7 +100,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
|
|
|
99
100
|
reviewer: '',
|
|
100
101
|
reviewModel: ''
|
|
101
102
|
});
|
|
102
|
-
return { allow: Boolean(allowed), source: 'user', review: null, reason: allowed ? '' : '
|
|
103
|
+
return { allow: Boolean(allowed), source: 'user', review: null, reason: allowed ? '' : 'The user declined this operation' };
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
emit?.({ type: 'review-start', toolId, name, kind });
|
|
@@ -128,7 +129,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
|
|
|
128
129
|
const allowed = await askUser({
|
|
129
130
|
kind,
|
|
130
131
|
permission,
|
|
131
|
-
title: kind === 'terminal' ? '
|
|
132
|
+
title: kind === 'terminal' ? t('approval_terminal') : t('approval_tool'),
|
|
132
133
|
operation: name,
|
|
133
134
|
detail: describeOperation({ kind, name, args, command }),
|
|
134
135
|
command: command || '',
|
|
@@ -137,7 +138,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
|
|
|
137
138
|
reviewer: review.reviewer,
|
|
138
139
|
reviewModel: review.modelId
|
|
139
140
|
});
|
|
140
|
-
return { allow: Boolean(allowed), source: 'user', review, reason: allowed ? '' : '
|
|
141
|
+
return { allow: Boolean(allowed), source: 'user', review, reason: allowed ? '' : 'The user declined this operation' };
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
function providerOf(config, providerId) {
|
|
@@ -197,12 +198,12 @@ async function runAgent({
|
|
|
197
198
|
}) {
|
|
198
199
|
const resolvedMode = mode === 'work' ? 'work' : 'chat';
|
|
199
200
|
if (!Array.isArray(messages) || !messages.length) {
|
|
200
|
-
throw Object.assign(new Error('
|
|
201
|
+
throw Object.assign(new Error('No messages: at least one user message is required'), { userError: true });
|
|
201
202
|
}
|
|
202
203
|
const provider = providerOf(config, providerId);
|
|
203
|
-
if (!provider) throw new Error('
|
|
204
|
+
if (!provider) throw new Error('Provider not found. Run dgxay provider add first');
|
|
204
205
|
const targetModel = modelOf(provider, modelId, config);
|
|
205
|
-
if (!targetModel) throw new Error('
|
|
206
|
+
if (!targetModel) throw new Error('This provider has no models. Add one first');
|
|
206
207
|
|
|
207
208
|
const effectivePermission = VALID_PERMISSIONS.includes(permission) ? permission : resolvePermissions(config);
|
|
208
209
|
const activeWorkspace = normalizeWorkspace(workspace || config.work?.workspace || '');
|
|
@@ -251,7 +252,7 @@ async function runAgent({
|
|
|
251
252
|
let continuationCount = 0;
|
|
252
253
|
|
|
253
254
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
254
|
-
emit({ type: 'status', text: step === 0 ? '
|
|
255
|
+
emit({ type: 'status', text: step === 0 ? 'Working' : 'Continuing' });
|
|
255
256
|
const result = await streamChat({
|
|
256
257
|
protocol: provider.protocol || 'openai',
|
|
257
258
|
baseUrl: provider.baseUrl,
|
|
@@ -372,13 +373,13 @@ async function runAgent({
|
|
|
372
373
|
apiMessages.push({
|
|
373
374
|
role: 'user',
|
|
374
375
|
content:
|
|
375
|
-
'
|
|
376
|
+
'Continue until the task is fully done. Do not describe a plan or promise a next step — call the tools to finish the remaining work, verify the result, then give a short summary.'
|
|
376
377
|
});
|
|
377
378
|
if (finalText) {
|
|
378
379
|
finalText += '\n\n';
|
|
379
380
|
emit({ type: 'delta', text: '\n\n' });
|
|
380
381
|
}
|
|
381
|
-
emit({ type: 'status', text: '
|
|
382
|
+
emit({ type: 'status', text: 'Continuing the task' });
|
|
382
383
|
continue;
|
|
383
384
|
}
|
|
384
385
|
|
package/src/core/llm.js
CHANGED
|
@@ -20,7 +20,7 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
20
20
|
|
|
21
21
|
function endpointFor(baseUrl, path) {
|
|
22
22
|
const base = normalizeBaseUrl(baseUrl);
|
|
23
|
-
if (!base) throw new Error('Base URL
|
|
23
|
+
if (!base) throw new Error('Base URL must not be empty');
|
|
24
24
|
if (base.toLowerCase().endsWith(path.toLowerCase())) return base;
|
|
25
25
|
// 已经带版本前缀的根地址(/v1、/v1beta/openai、/v1beta1 等)直接拼端点
|
|
26
26
|
if (/\/v\d+[a-z0-9]*(?:\/.*)?$/i.test(base)) return `${base}${path}`;
|
|
@@ -95,7 +95,7 @@ function splitSystem(messages) {
|
|
|
95
95
|
|
|
96
96
|
/** 通用 SSE 读取:每条 data 交给 onData;忽略注释与 [DONE] */
|
|
97
97
|
async function consumeSSE(res, onData) {
|
|
98
|
-
if (!res.body) throw new Error('
|
|
98
|
+
if (!res.body) throw new Error('This network response does not support streaming');
|
|
99
99
|
const reader = res.body.getReader();
|
|
100
100
|
const decoder = new TextDecoder('utf-8');
|
|
101
101
|
let buffer = '';
|
|
@@ -219,7 +219,7 @@ async function streamOpenAI(options) {
|
|
|
219
219
|
} catch {
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
|
-
if (json.error) throw new Error(json.error.message || '
|
|
222
|
+
if (json.error) throw new Error(json.error.message || 'The model returned an error');
|
|
223
223
|
const choice = json.choices?.[0];
|
|
224
224
|
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
225
225
|
const delta = choice?.delta || choice?.message || {};
|
|
@@ -365,7 +365,7 @@ async function streamAnthropic(options) {
|
|
|
365
365
|
|
|
366
366
|
if (!stream) {
|
|
367
367
|
const json = await res.json();
|
|
368
|
-
if (json.error) throw new Error(json.error.message || 'Anthropic
|
|
368
|
+
if (json.error) throw new Error(json.error.message || 'Anthropic returned an error');
|
|
369
369
|
let content = '';
|
|
370
370
|
let reasoning = '';
|
|
371
371
|
const toolCalls = [];
|
|
@@ -452,7 +452,7 @@ async function streamAnthropic(options) {
|
|
|
452
452
|
break;
|
|
453
453
|
}
|
|
454
454
|
case 'error':
|
|
455
|
-
throw new Error(event.error?.message || 'Anthropic
|
|
455
|
+
throw new Error(event.error?.message || 'Anthropic returned an error');
|
|
456
456
|
default:
|
|
457
457
|
break;
|
|
458
458
|
}
|
|
@@ -573,7 +573,7 @@ async function streamResponses(options) {
|
|
|
573
573
|
|
|
574
574
|
if (!stream) {
|
|
575
575
|
const json = await res.json();
|
|
576
|
-
if (json.error) throw new Error(json.error.message || 'Responses
|
|
576
|
+
if (json.error) throw new Error(json.error.message || 'Responses returned an error');
|
|
577
577
|
const { text, thought, calls } = collectOutput(json);
|
|
578
578
|
if (text) onDelta?.(text);
|
|
579
579
|
if (thought) onReasoning?.(thought);
|
|
@@ -655,9 +655,9 @@ async function streamResponses(options) {
|
|
|
655
655
|
finishReason = event.response?.status || 'completed';
|
|
656
656
|
break;
|
|
657
657
|
case 'response.failed':
|
|
658
|
-
throw new Error(event.response?.error?.message || 'Responses
|
|
658
|
+
throw new Error(event.response?.error?.message || 'Responses request failed');
|
|
659
659
|
case 'error':
|
|
660
|
-
throw new Error(event.error?.message || 'Responses
|
|
660
|
+
throw new Error(event.error?.message || 'Responses returned an error');
|
|
661
661
|
default:
|
|
662
662
|
break;
|
|
663
663
|
}
|
|
@@ -676,8 +676,8 @@ async function streamResponses(options) {
|
|
|
676
676
|
|
|
677
677
|
async function streamChat(options) {
|
|
678
678
|
const protocol = PROTOCOLS.includes(options.protocol) ? options.protocol : 'openai';
|
|
679
|
-
if (!options.model) throw new Error('
|
|
680
|
-
if (!Array.isArray(options.messages) || !options.messages.length) throw new Error('
|
|
679
|
+
if (!options.model) throw new Error('No model selected');
|
|
680
|
+
if (!Array.isArray(options.messages) || !options.messages.length) throw new Error('Messages cannot be empty');
|
|
681
681
|
if (protocol === 'anthropic') return streamAnthropic(options);
|
|
682
682
|
if (protocol === 'responses') return streamResponses(options);
|
|
683
683
|
return streamOpenAI(options);
|
package/src/core/mcp.js
CHANGED
|
@@ -92,8 +92,8 @@ function normalizeEnv(raw) {
|
|
|
92
92
|
function addServer(input) {
|
|
93
93
|
const name = String(input?.name || '').trim();
|
|
94
94
|
const command = String(input?.command || '').trim();
|
|
95
|
-
if (!name) throw new Error('MCP
|
|
96
|
-
if (!command) throw new Error('MCP
|
|
95
|
+
if (!name) throw new Error('MCP name cannot be empty');
|
|
96
|
+
if (!command) throw new Error('MCP command cannot be empty');
|
|
97
97
|
|
|
98
98
|
const data = readConfig();
|
|
99
99
|
const record = {
|
|
@@ -167,7 +167,7 @@ function pushLog(entry, stream, text) {
|
|
|
167
167
|
function startServer(id) {
|
|
168
168
|
const data = readConfig();
|
|
169
169
|
const server = data.servers.find((item) => item.id === id) || data.servers.find((item) => item.name === id);
|
|
170
|
-
if (!server) throw new Error('
|
|
170
|
+
if (!server) throw new Error('MCP server not found');
|
|
171
171
|
|
|
172
172
|
const existing = processes.get(server.id);
|
|
173
173
|
if (existing?.status === 'running' && existing.child && !existing.child.killed) {
|
|
@@ -185,7 +185,7 @@ function startServer(id) {
|
|
|
185
185
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
186
186
|
});
|
|
187
187
|
} catch (err) {
|
|
188
|
-
throw new Error(
|
|
188
|
+
throw new Error(`Cannot start MCP server: ${err.message}`);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
const entry = {
|
|
@@ -210,7 +210,7 @@ function startServer(id) {
|
|
|
210
210
|
entry.status = wasStopping || code === 0 ? 'stopped' : 'error';
|
|
211
211
|
entry.exitCode = code;
|
|
212
212
|
if (!wasStopping && code !== 0 && code !== null) {
|
|
213
|
-
entry.error =
|
|
213
|
+
entry.error = `process exited, code=${code}${signal ? `, signal=${signal}` : ''}`;
|
|
214
214
|
}
|
|
215
215
|
notify({ type: 'status', id: server.id, status: entry.status, code, signal });
|
|
216
216
|
});
|
package/src/core/oauth.js
CHANGED
|
@@ -30,7 +30,7 @@ const PROVIDERS = {
|
|
|
30
30
|
openai: {
|
|
31
31
|
id: 'openai',
|
|
32
32
|
name: 'OpenAI',
|
|
33
|
-
subtitle: 'ChatGPT / Codex
|
|
33
|
+
subtitle: 'ChatGPT / Codex account',
|
|
34
34
|
authorizeUrl: 'https://auth.openai.com/oauth/authorize',
|
|
35
35
|
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
36
36
|
clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
@@ -47,13 +47,13 @@ const PROVIDERS = {
|
|
|
47
47
|
},
|
|
48
48
|
baseUrl: 'https://api.openai.com/v1',
|
|
49
49
|
protocol: 'openai',
|
|
50
|
-
hint: '
|
|
50
|
+
hint: 'Uses your ChatGPT subscription account. If the inference endpoint rejects subscription tokens, use an API key for this provider instead.',
|
|
51
51
|
publicClient: true
|
|
52
52
|
},
|
|
53
53
|
anthropic: {
|
|
54
54
|
id: 'anthropic',
|
|
55
55
|
name: 'Anthropic',
|
|
56
|
-
subtitle: 'Claude
|
|
56
|
+
subtitle: 'Claude account',
|
|
57
57
|
authorizeUrl: 'https://claude.ai/oauth/authorize',
|
|
58
58
|
tokenUrl: 'https://console.anthropic.com/v1/oauth/token',
|
|
59
59
|
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
@@ -65,7 +65,7 @@ const PROVIDERS = {
|
|
|
65
65
|
extraAuth: { code: 'true' },
|
|
66
66
|
baseUrl: 'https://api.anthropic.com',
|
|
67
67
|
protocol: 'anthropic',
|
|
68
|
-
hint: '
|
|
68
|
+
hint: 'After approving in the browser you get an authorization code; paste it back into the terminal to finish connecting.',
|
|
69
69
|
publicClient: true
|
|
70
70
|
},
|
|
71
71
|
xai: {
|
|
@@ -84,13 +84,13 @@ const PROVIDERS = {
|
|
|
84
84
|
extraAuth: {},
|
|
85
85
|
baseUrl: 'https://api.x.ai/v1',
|
|
86
86
|
protocol: 'openai',
|
|
87
|
-
hint: '
|
|
87
|
+
hint: 'Requires SuperGrok or X Premium+. xAI may restrict OAuth inference by plan; use an API key if you get a 403.',
|
|
88
88
|
publicClient: true
|
|
89
89
|
},
|
|
90
90
|
google: {
|
|
91
91
|
id: 'google',
|
|
92
92
|
name: 'Google',
|
|
93
|
-
subtitle: 'Gemini API
|
|
93
|
+
subtitle: 'Gemini API account',
|
|
94
94
|
authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
95
95
|
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
96
96
|
clientId: '',
|
|
@@ -104,7 +104,7 @@ const PROVIDERS = {
|
|
|
104
104
|
userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
105
105
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
106
106
|
protocol: 'openai',
|
|
107
|
-
hint: '
|
|
107
|
+
hint: 'First create a "Desktop app" OAuth client in Google Cloud Console and enable the Generative Language API.',
|
|
108
108
|
publicClient: false
|
|
109
109
|
}
|
|
110
110
|
};
|
|
@@ -168,7 +168,7 @@ function decodeConnection(raw) {
|
|
|
168
168
|
try {
|
|
169
169
|
if (raw.enc === true) {
|
|
170
170
|
// 桌面版 Electron safeStorage 加密的数据,CLI 无法解密
|
|
171
|
-
const error = new Error('
|
|
171
|
+
const error = new Error('This token was encrypted by the desktop app and cannot be read by the CLI. Reconnect in the desktop app, or re-authorize in this tool.');
|
|
172
172
|
error.code = 'DESKTOP_ENCRYPTED';
|
|
173
173
|
throw error;
|
|
174
174
|
}
|
|
@@ -176,7 +176,7 @@ function decodeConnection(raw) {
|
|
|
176
176
|
const key = tokenKey();
|
|
177
177
|
if (!key) {
|
|
178
178
|
const error = new Error(
|
|
179
|
-
'
|
|
179
|
+
'The token is encrypted with DGXAY_TOKEN_KEY, but that variable is missing in this environment, so it cannot be decrypted. Set the same DGXAY_TOKEN_KEY and try again.'
|
|
180
180
|
);
|
|
181
181
|
error.code = 'MISSING_TOKEN_KEY';
|
|
182
182
|
throw error;
|
|
@@ -210,7 +210,7 @@ function encodeConnection(connection) {
|
|
|
210
210
|
|
|
211
211
|
function clientConfig(id, store = readStore()) {
|
|
212
212
|
const def = PROVIDERS[id];
|
|
213
|
-
if (!def) throw new Error('
|
|
213
|
+
if (!def) throw new Error('Unsupported OAuth provider');
|
|
214
214
|
const saved = store.clients[id] || {};
|
|
215
215
|
const pick = (key) => {
|
|
216
216
|
const value = saved[key];
|
|
@@ -239,7 +239,7 @@ function connectionSummary(id, connection) {
|
|
|
239
239
|
if (connection.__error) {
|
|
240
240
|
return {
|
|
241
241
|
provider: id,
|
|
242
|
-
account: '
|
|
242
|
+
account: '(cannot decrypt)',
|
|
243
243
|
broken: true,
|
|
244
244
|
error: connection.__error.message,
|
|
245
245
|
expired: true,
|
|
@@ -267,7 +267,7 @@ function listProviders() {
|
|
|
267
267
|
subtitle: def.subtitle,
|
|
268
268
|
hint: def.hint,
|
|
269
269
|
redirectMode: def.redirectMode,
|
|
270
|
-
redirectUri: config.redirectUri || (def.redirectMode === 'loopback' ? '
|
|
270
|
+
redirectUri: config.redirectUri || (def.redirectMode === 'loopback' ? 'Loopback port assigned by the system' : def.redirectUri),
|
|
271
271
|
scopes: config.scopes,
|
|
272
272
|
authorizeUrl: config.authorizeUrl,
|
|
273
273
|
tokenUrl: config.tokenUrl,
|
|
@@ -283,7 +283,7 @@ function listProviders() {
|
|
|
283
283
|
|
|
284
284
|
function saveClient(id, patch = {}) {
|
|
285
285
|
const def = PROVIDERS[id];
|
|
286
|
-
if (!def) throw new Error('
|
|
286
|
+
if (!def) throw new Error('Unsupported OAuth provider');
|
|
287
287
|
const store = readStore();
|
|
288
288
|
const current = store.clients[id] || {};
|
|
289
289
|
const next = { ...current };
|
|
@@ -301,10 +301,10 @@ function saveClient(id, patch = {}) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
if (next.authorizeUrl && !/^https?:\/\//i.test(next.authorizeUrl)) {
|
|
304
|
-
throw new Error('
|
|
304
|
+
throw new Error('The authorization endpoint must start with http:// or https://');
|
|
305
305
|
}
|
|
306
306
|
if (next.tokenUrl && !/^https?:\/\//i.test(next.tokenUrl)) {
|
|
307
|
-
throw new Error('
|
|
307
|
+
throw new Error('The token endpoint must start with http:// or https://');
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
store.clients[id] = next;
|
|
@@ -338,7 +338,7 @@ function accountFromTokens(tokens) {
|
|
|
338
338
|
const email = claims.email || accessClaims.email || '';
|
|
339
339
|
const name = claims.name || claims.preferred_username || accessClaims.name || '';
|
|
340
340
|
const subject = claims.sub || accessClaims.sub || '';
|
|
341
|
-
return email || name || (subject ?
|
|
341
|
+
return email || name || (subject ? `Account ${String(subject).slice(0, 10)}` : 'Connected account');
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
async function fetchAccount(id, tokens) {
|
|
@@ -384,7 +384,7 @@ async function requestToken(config, params) {
|
|
|
384
384
|
}
|
|
385
385
|
if (!res.ok || !data.access_token) {
|
|
386
386
|
const detail = data.error_description || data.error?.message || data.error || text.slice(0, 300);
|
|
387
|
-
throw new Error(`OAuth
|
|
387
|
+
throw new Error(`OAuth token exchange failed: HTTP ${res.status}${detail ? ` · ${detail}` : ''}`);
|
|
388
388
|
}
|
|
389
389
|
return data;
|
|
390
390
|
} finally {
|
|
@@ -433,11 +433,11 @@ async function getAccessToken(id) {
|
|
|
433
433
|
const connection = decodeConnection(store.connections[id]);
|
|
434
434
|
if (connection?.__error) throw connection.__error;
|
|
435
435
|
if (!connection?.accessToken) {
|
|
436
|
-
throw new Error(
|
|
436
|
+
throw new Error(`${PROVIDERS[id]?.name || id} account is not connected. Run dgxay oauth connect ${id} first`);
|
|
437
437
|
}
|
|
438
438
|
if (connection.expiresAt && Date.now() > connection.expiresAt - 60_000) {
|
|
439
439
|
if (!connection.refreshToken) {
|
|
440
|
-
throw new Error(
|
|
440
|
+
throw new Error(`The ${PROVIDERS[id].name} login has expired. Run dgxay oauth connect ${id} again`);
|
|
441
441
|
}
|
|
442
442
|
const refreshed = await refreshConnection(id, connection);
|
|
443
443
|
return refreshed.accessToken;
|
|
@@ -512,13 +512,13 @@ function startLoopback(flow) {
|
|
|
512
512
|
`<!doctype html><meta charset="utf-8"><title>DgxayCode</title>` +
|
|
513
513
|
`<body style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;display:grid;place-items:center;height:100vh;margin:0;background:#f7f7f5;color:#1f2023">` +
|
|
514
514
|
`<div style="text-align:center"><div style="width:44px;height:44px;border-radius:12px;background:#2563eb;display:inline-grid;place-items:center;color:#fff;font-size:22px">✓</div>` +
|
|
515
|
-
`<h1 style="font-size:18px;margin:16px 0 6px">${ok ? '
|
|
516
|
-
`<p style="font-size:13px;color:#6b6d72;margin:0">${ok ? '
|
|
515
|
+
`<h1 style="font-size:18px;margin:16px 0 6px">${ok ? 'Authorization complete' : 'Authorization incomplete'}</h1>` +
|
|
516
|
+
`<p style="font-size:13px;color:#6b6d72;margin:0">${ok ? 'You can close this page and return to the terminal.' : 'Please return to the terminal and try again.'}</p></div></body>`
|
|
517
517
|
);
|
|
518
518
|
|
|
519
519
|
if (!ok) {
|
|
520
520
|
const err = new Error(
|
|
521
|
-
error ?
|
|
521
|
+
error ? `Authorization was denied: ${error}` : state !== flow.state ? 'Authorization callback state mismatch' : 'Authorization callback is missing the code'
|
|
522
522
|
);
|
|
523
523
|
failFlow(flow, err);
|
|
524
524
|
return;
|
|
@@ -533,9 +533,9 @@ function startLoopback(flow) {
|
|
|
533
533
|
server.once('error', (err) => {
|
|
534
534
|
const hint =
|
|
535
535
|
err.code === 'EADDRINUSE'
|
|
536
|
-
?
|
|
536
|
+
? `Port ${flow.fixedPort || ''} is already in use (often Codex CLI or another auth instance is running). Close it and try again.`
|
|
537
537
|
: err.message;
|
|
538
|
-
reject(new Error(
|
|
538
|
+
reject(new Error(`Cannot start the local callback server: ${hint}`));
|
|
539
539
|
});
|
|
540
540
|
const port = flow.fixedPort || 0;
|
|
541
541
|
const host = flow.fixedPort ? 'localhost' : '127.0.0.1';
|
|
@@ -554,17 +554,17 @@ function startLoopback(flow) {
|
|
|
554
554
|
async function begin(input = {}) {
|
|
555
555
|
const id = String(input.provider || '');
|
|
556
556
|
const def = PROVIDERS[id];
|
|
557
|
-
if (!def) throw new Error('
|
|
557
|
+
if (!def) throw new Error('Unsupported OAuth provider');
|
|
558
558
|
|
|
559
559
|
const store = readStore();
|
|
560
560
|
const config = clientConfig(id, store);
|
|
561
|
-
if (!config.clientId) throw new Error(
|
|
561
|
+
if (!config.clientId) throw new Error(`Set this platform's OAuth Client ID first: dgxay oauth client ${id} --client-id <ID>`);
|
|
562
562
|
|
|
563
563
|
for (const [label, url] of [
|
|
564
|
-
['
|
|
565
|
-
['
|
|
564
|
+
['Authorization endpoint', config.authorizeUrl],
|
|
565
|
+
['Token endpoint', config.tokenUrl]
|
|
566
566
|
]) {
|
|
567
|
-
if (!/^https?:\/\//i.test(url || '')) throw new Error(`${label}
|
|
567
|
+
if (!/^https?:\/\//i.test(url || '')) throw new Error(`${label} must be an http:// or https:// URL`);
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
const verifier = base64url(crypto.randomBytes(32));
|
|
@@ -621,7 +621,7 @@ async function begin(input = {}) {
|
|
|
621
621
|
|
|
622
622
|
flows.set(flowId, flow);
|
|
623
623
|
flow.timer = setTimeout(() => {
|
|
624
|
-
failFlow(flow, new Error('
|
|
624
|
+
failFlow(flow, new Error('Authorization timed out, please start the connection again'));
|
|
625
625
|
}, FLOW_TIMEOUT);
|
|
626
626
|
|
|
627
627
|
const opened = await openBrowser(authUrl.toString());
|
|
@@ -639,9 +639,9 @@ async function begin(input = {}) {
|
|
|
639
639
|
|
|
640
640
|
async function finalizeFlow(flow, rawCode, state) {
|
|
641
641
|
if (!flow || flow.settled) return null;
|
|
642
|
-
if (state && flow.state && state !== flow.state) throw new Error('
|
|
642
|
+
if (state && flow.state && state !== flow.state) throw new Error('Authorization callback state mismatch, aborted');
|
|
643
643
|
const code = String(rawCode || '').trim();
|
|
644
|
-
if (!code) throw new Error('
|
|
644
|
+
if (!code) throw new Error('The authorization code is empty');
|
|
645
645
|
|
|
646
646
|
const params = {
|
|
647
647
|
grant_type: 'authorization_code',
|
|
@@ -689,13 +689,13 @@ function failFlow(flow, error) {
|
|
|
689
689
|
|
|
690
690
|
function wait(id) {
|
|
691
691
|
const flow = flows.get(String(id));
|
|
692
|
-
if (!flow) throw new Error('
|
|
692
|
+
if (!flow) throw new Error('The authorization session does not exist or has ended. Start the connection again');
|
|
693
693
|
return flow.promise;
|
|
694
694
|
}
|
|
695
695
|
|
|
696
696
|
async function complete(id, rawInput) {
|
|
697
697
|
const flow = flows.get(String(id));
|
|
698
|
-
if (!flow) throw new Error('
|
|
698
|
+
if (!flow) throw new Error('The authorization session does not exist or has ended. Start the connection again');
|
|
699
699
|
let code = String(rawInput || '').trim();
|
|
700
700
|
let state = '';
|
|
701
701
|
if (code.includes('#')) {
|
|
@@ -711,14 +711,14 @@ async function complete(id, rawInput) {
|
|
|
711
711
|
/* keep raw */
|
|
712
712
|
}
|
|
713
713
|
}
|
|
714
|
-
if (!code) throw new Error('
|
|
714
|
+
if (!code) throw new Error('No authorization code recognized. Copy it again');
|
|
715
715
|
return finalizeFlow(flow, code, state);
|
|
716
716
|
}
|
|
717
717
|
|
|
718
718
|
function cancel(id) {
|
|
719
719
|
const flow = flows.get(String(id));
|
|
720
720
|
if (!flow) return false;
|
|
721
|
-
failFlow(flow, new Error('
|
|
721
|
+
failFlow(flow, new Error('Authorization cancelled'));
|
|
722
722
|
return true;
|
|
723
723
|
}
|
|
724
724
|
|