dgxay-code-cli 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README-zh.md +242 -0
- package/README.md +223 -2
- package/bin/dgxay.js +42 -0
- package/package.json +47 -4
- package/src/cli/ansi.js +539 -0
- package/src/cli/approval.js +186 -0
- package/src/cli/args.js +162 -0
- package/src/cli/command-table.js +52 -0
- package/src/cli/editor.js +828 -0
- package/src/cli/i18n.js +276 -0
- package/src/cli/index.js +228 -0
- package/src/cli/print.js +148 -0
- package/src/cli/render.js +395 -0
- package/src/cli/repl-state.js +85 -0
- package/src/cli/repl.js +1231 -0
- package/src/cli/slash.js +75 -0
- package/src/cli/ui.js +52 -0
- package/src/commands/_shared.js +20 -0
- package/src/commands/chat.js +212 -0
- package/src/commands/config.js +363 -0
- package/src/commands/context.js +43 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/export.js +102 -0
- package/src/commands/mcp.js +257 -0
- package/src/commands/model.js +223 -0
- package/src/commands/oauth.js +246 -0
- package/src/commands/permission.js +108 -0
- package/src/commands/provider.js +332 -0
- package/src/commands/run.js +172 -0
- package/src/commands/skill.js +94 -0
- package/src/commands/workspace.js +100 -0
- package/src/core/agent.js +420 -0
- package/src/core/llm.js +709 -0
- package/src/core/mcp.js +271 -0
- package/src/core/oauth.js +737 -0
- package/src/core/prompt.js +269 -0
- package/src/core/sessions.js +208 -0
- package/src/core/tools.js +391 -0
- package/src/services/config.js +182 -0
- package/src/services/input.js +91 -0
- package/src/services/session-bus.js +104 -0
- package/src/services/settings.js +340 -0
- package/src/services/skills.js +125 -0
- package/src/services/terminal.js +124 -0
- package/src/services/version.js +25 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* oauth:账户连接(对应桌面版「设置 → 账户连接」)。
|
|
5
|
+
*
|
|
6
|
+
* dgxay oauth list
|
|
7
|
+
* dgxay oauth connect <openai|anthropic|xai|google>
|
|
8
|
+
* dgxay oauth disconnect <平台>
|
|
9
|
+
* dgxay oauth client <平台> [--client-id X] [--client-secret Y] [--authorize-url U] [--token-url U] [--scopes S]
|
|
10
|
+
*
|
|
11
|
+
* 说明:CLI 用系统浏览器发起授权;Anthropic 需要把浏览器显示的 code#state 粘贴回来。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const print = require('../cli/print');
|
|
15
|
+
const { parseArgs, actionFirst } = require('../cli/args');
|
|
16
|
+
const { COLORS } = require('../cli/ansi');
|
|
17
|
+
const oauth = require('../core/oauth');
|
|
18
|
+
const { ask } = require('../services/input');
|
|
19
|
+
|
|
20
|
+
const options = [
|
|
21
|
+
{ name: 'client-id', type: 'string', valueName: 'id', desc: 'OAuth Client ID' },
|
|
22
|
+
{ name: 'client-secret', type: 'string', valueName: 'secret', desc: 'OAuth Client Secret (optional)' },
|
|
23
|
+
{ name: 'authorize-url', type: 'string', valueName: 'url', desc: 'Authorize endpoint' },
|
|
24
|
+
{ name: 'token-url', type: 'string', valueName: 'url', desc: 'Token endpoint' },
|
|
25
|
+
{ name: 'scopes', type: 'string', valueName: 'text', desc: 'Permission scope' },
|
|
26
|
+
{ name: 'code', type: 'string', valueName: 'code', desc: 'Manual flow: submit the auth code directly (code#state)' },
|
|
27
|
+
{ name: 'no-browser', type: 'boolean', desc: 'Do not open the browser, only print the authorize link' },
|
|
28
|
+
{ name: 'yes', alias: 'y', type: 'boolean', desc: 'Skip confirmation' }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function formatExpiry(ts) {
|
|
32
|
+
if (!ts) return 'never expires';
|
|
33
|
+
const date = new Date(ts);
|
|
34
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
35
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(
|
|
36
|
+
date.getMinutes()
|
|
37
|
+
)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function list() {
|
|
41
|
+
const providers = oauth.listProviders();
|
|
42
|
+
print.blank();
|
|
43
|
+
print.heading('Account connections');
|
|
44
|
+
const rows = providers.map((item) => {
|
|
45
|
+
const connection = item.connection;
|
|
46
|
+
const status = !connection ? 'not connected' : connection.expired ? 'expired' : 'connected';
|
|
47
|
+
const tone = !connection ? (value) => value : connection.expired ? COLORS.yellow : COLORS.green;
|
|
48
|
+
return [
|
|
49
|
+
item.id,
|
|
50
|
+
item.name,
|
|
51
|
+
item.subtitle,
|
|
52
|
+
tone(status),
|
|
53
|
+
connection?.account || '',
|
|
54
|
+
connection ? formatExpiry(connection.expiresAt) : '',
|
|
55
|
+
item.redirectMode === 'manual' ? 'manual auth code' : 'system browser'
|
|
56
|
+
];
|
|
57
|
+
});
|
|
58
|
+
print.table(
|
|
59
|
+
[
|
|
60
|
+
{ title: 'ID', max: 10 },
|
|
61
|
+
{ title: 'Platform', max: 10 },
|
|
62
|
+
{ title: 'Account type', max: 24 },
|
|
63
|
+
{ title: 'Status', max: 8 },
|
|
64
|
+
{ title: 'Account', max: 28 },
|
|
65
|
+
{ title: 'Expires', max: 18 },
|
|
66
|
+
{ title: 'Auth method', max: 12 }
|
|
67
|
+
],
|
|
68
|
+
rows
|
|
69
|
+
);
|
|
70
|
+
print.blank();
|
|
71
|
+
print.hint('dgxay oauth connect <platform> to connect · dgxay oauth disconnect <platform> to disconnect');
|
|
72
|
+
print.hint('After connecting, enable OAuth on a provider: dgxay provider edit <name> --oauth-provider <platform>');
|
|
73
|
+
print.blank();
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function connect(id, opts) {
|
|
78
|
+
const def = oauth.PROVIDERS[id];
|
|
79
|
+
if (!def) {
|
|
80
|
+
print.fail(`Unsupported platform: ${id} (choose openai / anthropic / xai / google)`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const current = oauth.listProviders().find((item) => item.id === id);
|
|
84
|
+
if (current.connection && !current.connection.expired && !opts.yes && process.stdin.isTTY) {
|
|
85
|
+
const answer = await ask(`${def.name} is already connected (${current.connection.account}). Re-authorizing overwrites the existing token. Continue? [y/N] `);
|
|
86
|
+
if (!/^y/i.test(String(answer || '').trim())) {
|
|
87
|
+
print.hint('Cancelled');
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (opts.noBrowser) process.env.DGXAY_OAUTH_NO_BROWSER = '1';
|
|
93
|
+
|
|
94
|
+
let flow;
|
|
95
|
+
try {
|
|
96
|
+
flow = await oauth.begin({ provider: id });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
print.fail(err.message);
|
|
99
|
+
if (/Client ID/i.test(err.message)) {
|
|
100
|
+
print.hint(`Google requires a desktop-app OAuth client first: dgxay oauth client google --client-id <ID>`);
|
|
101
|
+
}
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
print.blank();
|
|
106
|
+
print.heading(`Connect ${def.name}`);
|
|
107
|
+
print.hint(def.hint);
|
|
108
|
+
print.blank();
|
|
109
|
+
if (flow.browserOpened) print.ok('Opened the system browser; complete authorization in the browser');
|
|
110
|
+
else {
|
|
111
|
+
print.warn('Could not open the browser automatically; open the link below manually:');
|
|
112
|
+
}
|
|
113
|
+
print.blank();
|
|
114
|
+
process.stdout.write(`${COLORS.cyan(flow.authUrl)}\n`);
|
|
115
|
+
print.blank();
|
|
116
|
+
|
|
117
|
+
if (flow.mode === 'manual') {
|
|
118
|
+
print.write('After authorizing in the browser, copy the auth code (like code#state) and paste it here:');
|
|
119
|
+
let code = opts.code;
|
|
120
|
+
if (!code) {
|
|
121
|
+
if (!process.stdin.isTTY) {
|
|
122
|
+
print.fail('In non-interactive environments use --code <auth code>');
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
code = await ask('Auth code: ');
|
|
126
|
+
}
|
|
127
|
+
if (!code) {
|
|
128
|
+
print.fail('Auth code is empty');
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const connection = await oauth.complete(flow.id, code);
|
|
133
|
+
print.ok(`${def.name} account connected: ${connection.account}`);
|
|
134
|
+
return 0;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
print.fail(err.message || 'Authorization failed');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
process.stdout.write(`${COLORS.faint('Waiting for the browser callback to finish authorization... (Ctrl+C to cancel)')}\n`);
|
|
142
|
+
try {
|
|
143
|
+
const connection = await oauth.wait(flow.id);
|
|
144
|
+
print.blank();
|
|
145
|
+
print.ok(`${def.name} account connected: ${connection.account}`);
|
|
146
|
+
print.hint('Enable it on a provider: dgxay provider edit <name> --oauth-provider ' + id);
|
|
147
|
+
return 0;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
print.fail(err.message || 'Authorization failed');
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function disconnect(id, opts) {
|
|
155
|
+
const def = oauth.PROVIDERS[id];
|
|
156
|
+
if (!def) {
|
|
157
|
+
print.fail(`Unsupported platform: ${id}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
const current = oauth.listProviders().find((item) => item.id === id);
|
|
161
|
+
if (!current?.connection) {
|
|
162
|
+
print.warn(`${def.name} is not connected`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
oauth.disconnect(id);
|
|
166
|
+
print.ok(`Disconnected ${def.name} account (${current.connection.account})`);
|
|
167
|
+
print.hint('Providers using this account cannot make requests until you reconnect or switch to an API Key');
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function saveClient(id, opts) {
|
|
172
|
+
const def = oauth.PROVIDERS[id];
|
|
173
|
+
if (!def) {
|
|
174
|
+
print.fail(`Unsupported platform: ${id}`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
const patch = {};
|
|
178
|
+
for (const key of ['clientId', 'clientSecret', 'authorizeUrl', 'tokenUrl', 'scopes']) {
|
|
179
|
+
if (opts[key] !== undefined) patch[key] = opts[key];
|
|
180
|
+
}
|
|
181
|
+
if (!Object.keys(patch).length) {
|
|
182
|
+
const current = oauth.listProviders().find((item) => item.id === id);
|
|
183
|
+
print.blank();
|
|
184
|
+
print.heading(`${def.name} OAuth client`);
|
|
185
|
+
print.keyValue([
|
|
186
|
+
{ label: 'Client ID', value: current.clientId ? `${current.clientId.slice(0, 8)}…` : '(empty)' },
|
|
187
|
+
{ label: 'Client Secret', value: current.hasSecret ? 'set' : 'not set' },
|
|
188
|
+
{ label: 'Authorize endpoint', value: current.authorizeUrl },
|
|
189
|
+
{ label: 'Token endpoint', value: current.tokenUrl },
|
|
190
|
+
{ label: 'Permission scope', value: current.scopes },
|
|
191
|
+
{ label: 'Redirect URI', value: current.redirectUri }
|
|
192
|
+
]);
|
|
193
|
+
print.blank();
|
|
194
|
+
print.hint(`Modify: dgxay oauth client ${id} --client-id <ID> [--client-secret <Secret>]`);
|
|
195
|
+
print.blank();
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const saved = oauth.saveClient(id, patch);
|
|
200
|
+
print.ok(`Saved OAuth client config for ${def.name}`);
|
|
201
|
+
print.hint(`You can connect now: dgxay oauth connect ${saved.id}`);
|
|
202
|
+
return 0;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
print.fail(err.message);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function run({ argv }) {
|
|
210
|
+
const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
|
|
211
|
+
const action = rawAction || 'list';
|
|
212
|
+
const target = rest[0];
|
|
213
|
+
|
|
214
|
+
if (action === 'list' || action === 'ls') return list();
|
|
215
|
+
if (action === 'connect' || action === 'login') {
|
|
216
|
+
if (!target) throw Object.assign(new Error('Usage: dgxay oauth connect <openai|anthropic|xai|google>'), { userError: true });
|
|
217
|
+
return connect(target, opts);
|
|
218
|
+
}
|
|
219
|
+
if (action === 'disconnect' || action === 'logout') {
|
|
220
|
+
if (!target) throw Object.assign(new Error('Usage: dgxay oauth disconnect <platform>'), { userError: true });
|
|
221
|
+
return disconnect(target, opts);
|
|
222
|
+
}
|
|
223
|
+
if (action === 'client') {
|
|
224
|
+
if (!target) throw Object.assign(new Error('Usage: dgxay oauth client <platform> [--client-id ...]'), { userError: true });
|
|
225
|
+
return saveClient(target, opts);
|
|
226
|
+
}
|
|
227
|
+
print.fail(`Unknown subcommand: ${action}`);
|
|
228
|
+
print.hint('Available: list / connect / disconnect / client');
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
name: 'oauth',
|
|
234
|
+
aliases: ['account'],
|
|
235
|
+
summary: 'OAuth account connections',
|
|
236
|
+
usage: 'dgxay oauth <list|connect|disconnect|client>',
|
|
237
|
+
options,
|
|
238
|
+
subcommands: [
|
|
239
|
+
['list', 'View connection status per platform'],
|
|
240
|
+
['connect <platform>', 'Connect (system browser + PKCE)'],
|
|
241
|
+
['disconnect <platform>', 'Disconnect and delete the local token'],
|
|
242
|
+
['client <platform>', 'View / modify OAuth client config']
|
|
243
|
+
],
|
|
244
|
+
examples: ['dgxay oauth list', 'dgxay oauth connect anthropic', 'dgxay oauth client google --client-id xxx.apps.googleusercontent.com'],
|
|
245
|
+
run
|
|
246
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* permission:四档权限(对应桌面版权限列表)。
|
|
5
|
+
*
|
|
6
|
+
* dgxay permission 查看当前权限与可用性
|
|
7
|
+
* dgxay permission list 同上(带说明)
|
|
8
|
+
* dgxay permission set auto 切换权限并写入配置
|
|
9
|
+
*
|
|
10
|
+
* 自动审查只有在「当前供应商或默认供应商」配置了有效审查模型时才可用;
|
|
11
|
+
* 移除审查模型后自动回落到普通权限。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const print = require('../cli/print');
|
|
15
|
+
const { actionFirst } = require('../cli/args');
|
|
16
|
+
const { COLORS } = require('../cli/ansi');
|
|
17
|
+
const settings = require('../services/settings');
|
|
18
|
+
const { resolveReviewer } = require('../core/prompt');
|
|
19
|
+
|
|
20
|
+
const options = [];
|
|
21
|
+
|
|
22
|
+
function overview() {
|
|
23
|
+
const config = settings.readConfig();
|
|
24
|
+
const current = settings.currentPermission(config);
|
|
25
|
+
const available = settings.availablePermissions(config);
|
|
26
|
+
const reviewer = resolveReviewer(config, config.activeProviderId);
|
|
27
|
+
print.blank();
|
|
28
|
+
print.heading('Permission');
|
|
29
|
+
for (const [key, meta] of Object.entries(settings.PERMISSIONS)) {
|
|
30
|
+
const usable = available.includes(key);
|
|
31
|
+
const active = current === key;
|
|
32
|
+
print.write(
|
|
33
|
+
` ${active ? COLORS.accent('❯') : ' '} ${COLORS.accent(key.padEnd(8))} ${usable ? meta.label : COLORS.faint(`${meta.label} (unavailable)`)}`
|
|
34
|
+
);
|
|
35
|
+
print.write(` ${COLORS.faint(meta.desc)}`);
|
|
36
|
+
}
|
|
37
|
+
print.blank();
|
|
38
|
+
if (reviewer) {
|
|
39
|
+
print.hint(`Review model: ${reviewer.provider.name} / ${reviewer.modelId} (from: ${reviewer.source === 'own' ? 'current provider' : 'default provider'})`);
|
|
40
|
+
} else {
|
|
41
|
+
print.hint('No review model configured, so auto review is unavailable. Set one with dgxay provider review <provider> --model <model-id>');
|
|
42
|
+
}
|
|
43
|
+
if (config.ui?.permission === 'auto' && !available.includes('auto')) {
|
|
44
|
+
print.warn('Auto review from config is no longer valid; running with normal permission');
|
|
45
|
+
}
|
|
46
|
+
print.blank();
|
|
47
|
+
print.hint('dgxay permission set <none|normal|auto|full>');
|
|
48
|
+
print.blank();
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function run({ argv, runtime }) {
|
|
53
|
+
const { action: rawAction, rest } = actionFirst(argv, options);
|
|
54
|
+
const action = rawAction || 'list';
|
|
55
|
+
const config = settings.readConfig();
|
|
56
|
+
|
|
57
|
+
if (action === 'list' || action === 'show') return overview();
|
|
58
|
+
|
|
59
|
+
if (action === 'set' || action === 'use') {
|
|
60
|
+
const level = rest[0];
|
|
61
|
+
if (!level) throw Object.assign(new Error('Usage: dgxay permission set <none|normal|auto|full>'), { userError: true });
|
|
62
|
+
if (!settings.PERMISSIONS[level]) {
|
|
63
|
+
print.fail('Permission must be one of none / normal / auto / full');
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
if (level === 'auto' && !settings.hasReviewModel(config)) {
|
|
67
|
+
print.fail('Auto review unavailable: neither the current nor the default provider has a valid review model');
|
|
68
|
+
print.hint('Run first: dgxay provider review <provider> --model <model-id>');
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
settings.setPermission(level);
|
|
72
|
+
print.ok(`Permission set to: ${settings.PERMISSIONS[level].label}`);
|
|
73
|
+
if (level === 'full') {
|
|
74
|
+
print.warn('With full access the AI can run any command and read or write any file without asking');
|
|
75
|
+
}
|
|
76
|
+
void runtime;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (settings.PERMISSIONS[action]) {
|
|
81
|
+
const level = action;
|
|
82
|
+
if (level === 'auto' && !settings.hasReviewModel(config)) {
|
|
83
|
+
print.fail('Auto review unavailable: configure a review model first');
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
settings.setPermission(level);
|
|
87
|
+
print.ok(`Permission set to: ${settings.PERMISSIONS[level].label}`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
print.fail(`Unknown usage: ${action}`);
|
|
92
|
+
print.hint('Available: list / set <level>');
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
name: 'permission',
|
|
98
|
+
aliases: ['perm'],
|
|
99
|
+
summary: 'Four permission levels: show and switch',
|
|
100
|
+
usage: 'dgxay permission [list|show|set <level>]',
|
|
101
|
+
options,
|
|
102
|
+
subcommands: [
|
|
103
|
+
['list', 'Show permissions and availability'],
|
|
104
|
+
['set <level>', 'Switch permission (writes config)']
|
|
105
|
+
],
|
|
106
|
+
examples: ['dgxay permission', 'dgxay permission set normal', 'dgxay permission set auto'],
|
|
107
|
+
run
|
|
108
|
+
};
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* provider:供应商管理(对应桌面版「设置 → 供应商与模型」)。
|
|
5
|
+
*
|
|
6
|
+
* dgxay provider list
|
|
7
|
+
* dgxay provider add --name DeepSeek --base-url https://api.deepseek.com/v1 --model deepseek-chat [--api-key sk-...]
|
|
8
|
+
* dgxay provider edit <名称> [--base-url ...] [--api-key ...] [--model ...]
|
|
9
|
+
* dgxay provider rm <名称>
|
|
10
|
+
* dgxay provider use <名称> 设为当前供应商
|
|
11
|
+
* dgxay provider mv <名称> up|down 调整顺序(第一个是默认供应商,也是审查模型回退来源)
|
|
12
|
+
* dgxay provider review <名称> [--model <id>] 设置自动审查模型;不带 --model 显示当前值
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const print = require('../cli/print');
|
|
16
|
+
const { parseArgs, actionFirst } = require('../cli/args');
|
|
17
|
+
const { COLORS } = require('../cli/ansi');
|
|
18
|
+
const settings = require('../services/settings');
|
|
19
|
+
const cfg = require('../services/config');
|
|
20
|
+
const oauth = require('../core/oauth');
|
|
21
|
+
const { ask, askSecret, confirm } = require('../services/input');
|
|
22
|
+
|
|
23
|
+
const options = [
|
|
24
|
+
{ name: 'name', type: 'string', valueName: 'text', desc: 'Provider name' },
|
|
25
|
+
{ name: 'protocol', type: 'string', valueName: 'p', desc: 'openai / anthropic / responses' },
|
|
26
|
+
{ name: 'base-url', type: 'string', valueName: 'url', desc: 'Base URL' },
|
|
27
|
+
{ name: 'api-key', type: 'string', valueName: 'key', desc: 'API Key' },
|
|
28
|
+
{ name: 'model', type: 'list', valueName: 'id', desc: 'Model ID (repeatable or comma-separated)' },
|
|
29
|
+
{ name: 'oauth-provider', type: 'string', valueName: 'id', desc: 'Use a connected OAuth account' },
|
|
30
|
+
{ name: 'review-model', type: 'string', valueName: 'id', desc: 'Auto review model ID' },
|
|
31
|
+
{ name: 'context-window', type: 'number', valueName: 'n', desc: 'Context window (optional)' },
|
|
32
|
+
{ name: 'yes', alias: 'y', type: 'boolean', desc: 'Skip confirmation' }
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function list() {
|
|
36
|
+
const config = settings.readConfig();
|
|
37
|
+
const providers = config.providers || [];
|
|
38
|
+
print.blank();
|
|
39
|
+
if (!providers.length) {
|
|
40
|
+
print.hint('No providers yet. Run dgxay provider add or dgxay config init');
|
|
41
|
+
print.blank();
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
print.heading(`Providers (${providers.length})`);
|
|
45
|
+
const rows = providers.map((provider, index) => [
|
|
46
|
+
String(index + 1),
|
|
47
|
+
index === 0 ? COLORS.accent('default') : '',
|
|
48
|
+
provider.id === config.activeProviderId ? COLORS.green('active') : '',
|
|
49
|
+
provider.name,
|
|
50
|
+
settings.PROTOCOL_META[provider.protocol || 'openai'].short,
|
|
51
|
+
provider.baseUrl,
|
|
52
|
+
(provider.models || []).map((m) => m.id || m.name).join(', '),
|
|
53
|
+
provider.reviewModelId || '-',
|
|
54
|
+
provider.authMode === 'oauth' ? `OAuth(${provider.oauthProvider || '?'})` : provider.apiKey ? cfg.maskKey(provider.apiKey) : '-'
|
|
55
|
+
]);
|
|
56
|
+
print.table(
|
|
57
|
+
[
|
|
58
|
+
{ title: '#', max: 3 },
|
|
59
|
+
{ title: '', max: 4 },
|
|
60
|
+
{ title: '', max: 4 },
|
|
61
|
+
{ title: 'Name', max: 16 },
|
|
62
|
+
{ title: 'Protocol', max: 16 },
|
|
63
|
+
{ title: 'Base URL', max: 40 },
|
|
64
|
+
{ title: 'Models', max: 30 },
|
|
65
|
+
{ title: 'Review model', max: 18 },
|
|
66
|
+
{ title: 'Auth', max: 16 }
|
|
67
|
+
],
|
|
68
|
+
rows
|
|
69
|
+
);
|
|
70
|
+
print.blank();
|
|
71
|
+
print.hint('Provider order matters: the first one is the default provider and the fallback source when no review model is configured');
|
|
72
|
+
print.blank();
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function add(opts) {
|
|
77
|
+
const interactive = process.stdin.isTTY && !opts.baseUrl;
|
|
78
|
+
let protocol = opts.protocol || 'openai';
|
|
79
|
+
let baseUrl = opts.baseUrl || '';
|
|
80
|
+
let name = opts.name || '';
|
|
81
|
+
let apiKey = opts.apiKey;
|
|
82
|
+
let models = opts.model || [];
|
|
83
|
+
let oauthProvider = opts.oauthProvider || '';
|
|
84
|
+
|
|
85
|
+
if (interactive) {
|
|
86
|
+
if (!opts.protocol) {
|
|
87
|
+
const answer = await ask('Protocol [1] OpenAI ChatCompletions [2] Anthropic Messages [3] OpenAI Responses : ');
|
|
88
|
+
const index = Number(String(answer || '').trim()) || 1;
|
|
89
|
+
protocol = ['openai', 'anthropic', 'responses'][index - 1] || 'openai';
|
|
90
|
+
}
|
|
91
|
+
if (!baseUrl) {
|
|
92
|
+
const answer = await ask(`Base URL [${settings.PROTOCOL_META[protocol].basePlaceholder}]: `);
|
|
93
|
+
baseUrl = String(answer || '').trim() || settings.PROTOCOL_META[protocol].basePlaceholder;
|
|
94
|
+
}
|
|
95
|
+
if (!name) {
|
|
96
|
+
const answer = await ask('Provider name: ');
|
|
97
|
+
name = String(answer || '').trim();
|
|
98
|
+
}
|
|
99
|
+
if (!oauthProvider && apiKey === undefined) {
|
|
100
|
+
const secret = await askSecret('API Key (may be empty for local models): ');
|
|
101
|
+
apiKey = secret === null ? undefined : secret;
|
|
102
|
+
}
|
|
103
|
+
if (!models.length) {
|
|
104
|
+
const answer = await ask('Model ID (separate multiple with commas): ');
|
|
105
|
+
models = String(answer || '')
|
|
106
|
+
.split(/[,,\s]+/)
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = settings.upsertProvider({
|
|
112
|
+
name: name || undefined,
|
|
113
|
+
protocol,
|
|
114
|
+
baseUrl,
|
|
115
|
+
apiKey,
|
|
116
|
+
models,
|
|
117
|
+
authMode: oauthProvider ? 'oauth' : 'apiKey',
|
|
118
|
+
oauthProvider,
|
|
119
|
+
reviewModelId: opts.reviewModelId
|
|
120
|
+
});
|
|
121
|
+
print.ok(`Provider added: ${result.provider.name} (${result.provider.id})`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function edit(ref, opts) {
|
|
126
|
+
const provider = settings.findProvider(ref);
|
|
127
|
+
if (!provider) {
|
|
128
|
+
print.fail(`Provider not found: ${ref}`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const result = settings.upsertProvider({
|
|
132
|
+
ref: provider.id,
|
|
133
|
+
name: opts.name,
|
|
134
|
+
protocol: opts.protocol,
|
|
135
|
+
baseUrl: opts.baseUrl,
|
|
136
|
+
apiKey: opts.apiKey,
|
|
137
|
+
models: opts.model?.length ? opts.model : undefined,
|
|
138
|
+
authMode: opts.oauthProvider ? 'oauth' : provider.authMode,
|
|
139
|
+
oauthProvider: opts.oauthProvider,
|
|
140
|
+
reviewModelId: opts.reviewModelId
|
|
141
|
+
});
|
|
142
|
+
print.ok(`Provider updated: ${result.provider.name}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function remove(ref, opts) {
|
|
147
|
+
const provider = settings.findProvider(ref);
|
|
148
|
+
if (!provider) {
|
|
149
|
+
print.fail(`Provider not found: ${ref}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
if (!opts.yes && process.stdin.isTTY) {
|
|
153
|
+
const ok = await confirm(`Delete provider "${provider.name}" and its API key?`, false);
|
|
154
|
+
if (!ok) {
|
|
155
|
+
print.hint('Cancelled');
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const config = settings.removeProvider(provider.id);
|
|
160
|
+
print.ok(`Deleted "${provider.name}"`);
|
|
161
|
+
if (!config.providers?.length) print.hint('No providers left; run dgxay config init to reconfigure');
|
|
162
|
+
else if (config.activeProviderId !== settings.readConfig().activeProviderId) {
|
|
163
|
+
print.hint(`Active provider switched to "${(config.providers || []).find((p) => p.id === config.activeProviderId)?.name || 'unknown'}"`);
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function use(ref) {
|
|
169
|
+
const provider = settings.findProvider(ref);
|
|
170
|
+
if (!provider) {
|
|
171
|
+
print.fail(`Provider not found: ${ref}`);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
const model = provider.models?.[0];
|
|
175
|
+
if (!model) {
|
|
176
|
+
print.fail(`Provider ${provider.name} has no models`);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
settings.saveConfig({ activeProviderId: provider.id, activeModelId: model.id });
|
|
180
|
+
print.ok(`Active provider: ${provider.name} / ${model.name || model.id}`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function review(ref, opts) {
|
|
185
|
+
const provider = settings.findProvider(ref);
|
|
186
|
+
if (!provider) {
|
|
187
|
+
print.fail(`Provider not found: ${ref}`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
if (opts.reviewModel === undefined) {
|
|
191
|
+
const model = (provider.models || []).find((m) => m.id === provider.reviewModelId);
|
|
192
|
+
if (model) print.write(`Review model for ${provider.name}: ${model.name || model.id} (${model.id})`);
|
|
193
|
+
else print.write(`${provider.name} has no review model configured; auto review falls back to the default provider`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
const models = provider.models || [];
|
|
197
|
+
const target = models.find((m) => m.id === opts.reviewModel);
|
|
198
|
+
if (opts.reviewModel && !target) {
|
|
199
|
+
print.fail(`Provider ${provider.name} has no model ${opts.reviewModel}`);
|
|
200
|
+
print.hint(`Available: ${models.map((m) => m.id).join(', ')}`);
|
|
201
|
+
return 1;
|
|
202
|
+
}
|
|
203
|
+
const next = (settings.readConfig().providers || []).map((p) =>
|
|
204
|
+
p.id === provider.id ? { ...p, reviewModelId: target ? target.id : '' } : p
|
|
205
|
+
);
|
|
206
|
+
settings.saveConfig({ providers: next });
|
|
207
|
+
if (target) {
|
|
208
|
+
print.ok(`Auto review model for ${provider.name}: ${target.name || target.id}`);
|
|
209
|
+
print.hint('You can now select "Auto review" in /permission');
|
|
210
|
+
} else {
|
|
211
|
+
print.ok(`Cleared the review model for ${provider.name}`);
|
|
212
|
+
}
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function move(ref, direction) {
|
|
217
|
+
const dir = direction === 'up' || direction === 'u' || direction === '-1' ? -1 : 1;
|
|
218
|
+
settings.moveProvider(ref, dir);
|
|
219
|
+
print.ok(`${dir < 0 ? 'Moved up' : 'Moved down'} "${settings.findProvider(ref)?.name || ref}"`);
|
|
220
|
+
return list();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function oauthAccounts() {
|
|
224
|
+
const list = oauth.listProviders();
|
|
225
|
+
print.blank();
|
|
226
|
+
print.heading('OAuth accounts');
|
|
227
|
+
const rows = list.map((item) => [
|
|
228
|
+
item.name,
|
|
229
|
+
item.subtitle,
|
|
230
|
+
item.connection && !item.connection.expired
|
|
231
|
+
? COLORS.green('connected')
|
|
232
|
+
: item.connection?.expired
|
|
233
|
+
? COLORS.yellow('expired')
|
|
234
|
+
: 'not connected',
|
|
235
|
+
item.connection?.account || '',
|
|
236
|
+
item.redirectMode === 'manual' ? 'manual auth code' : 'system browser'
|
|
237
|
+
]);
|
|
238
|
+
print.table(
|
|
239
|
+
[
|
|
240
|
+
{ title: 'Platform', max: 12 },
|
|
241
|
+
{ title: 'Account type', max: 24 },
|
|
242
|
+
{ title: 'Status', max: 8 },
|
|
243
|
+
{ title: 'Account', max: 30 },
|
|
244
|
+
{ title: 'Auth method', max: 12 }
|
|
245
|
+
],
|
|
246
|
+
rows
|
|
247
|
+
);
|
|
248
|
+
print.blank();
|
|
249
|
+
print.hint('dgxay oauth connect <platform> to connect an account; then use dgxay provider add --oauth-provider <platform>');
|
|
250
|
+
print.blank();
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function run({ argv }) {
|
|
255
|
+
const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
|
|
256
|
+
const action = rawAction || 'list';
|
|
257
|
+
|
|
258
|
+
switch (action) {
|
|
259
|
+
case 'list':
|
|
260
|
+
case 'ls':
|
|
261
|
+
return list();
|
|
262
|
+
case 'add':
|
|
263
|
+
return add(opts);
|
|
264
|
+
case 'edit':
|
|
265
|
+
case 'set': {
|
|
266
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay provider edit <name> [options]'), { userError: true });
|
|
267
|
+
return edit(rest[0], opts);
|
|
268
|
+
}
|
|
269
|
+
case 'rm':
|
|
270
|
+
case 'remove':
|
|
271
|
+
case 'delete': {
|
|
272
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay provider rm <name>'), { userError: true });
|
|
273
|
+
return remove(rest[0], opts);
|
|
274
|
+
}
|
|
275
|
+
case 'use': {
|
|
276
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay provider use <name>'), { userError: true });
|
|
277
|
+
return use(rest[0]);
|
|
278
|
+
}
|
|
279
|
+
case 'mv':
|
|
280
|
+
case 'move': {
|
|
281
|
+
if (!rest[0]) throw Object.assign(new Error('Usage: dgxay provider mv <name> up|down'), { userError: true });
|
|
282
|
+
return move(rest[0], rest[1] || 'down');
|
|
283
|
+
}
|
|
284
|
+
case 'review': {
|
|
285
|
+
if (!rest[0]) {
|
|
286
|
+
const config = settings.readConfig();
|
|
287
|
+
const rows = (config.providers || []).map((p) => [p.name, p.reviewModelId || '(not configured)']);
|
|
288
|
+
print.blank();
|
|
289
|
+
print.table(
|
|
290
|
+
[
|
|
291
|
+
{ title: 'Provider', max: 20 },
|
|
292
|
+
{ title: 'Review model', max: 30 }
|
|
293
|
+
],
|
|
294
|
+
rows
|
|
295
|
+
);
|
|
296
|
+
print.blank();
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
return review(rest[0], opts);
|
|
300
|
+
}
|
|
301
|
+
case 'oauth':
|
|
302
|
+
return oauthAccounts();
|
|
303
|
+
default:
|
|
304
|
+
print.fail(`Unknown subcommand: ${action}`);
|
|
305
|
+
print.hint('Available: list / add / edit / rm / use / mv / review / oauth');
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
name: 'provider',
|
|
312
|
+
aliases: ['prov'],
|
|
313
|
+
summary: 'Provider and review model management',
|
|
314
|
+
usage: 'dgxay provider <list|add|edit|rm|use|mv|review>',
|
|
315
|
+
options,
|
|
316
|
+
subcommands: [
|
|
317
|
+
['list', 'List all providers'],
|
|
318
|
+
['add', 'Add a provider'],
|
|
319
|
+
['edit <name>', 'Edit a provider'],
|
|
320
|
+
['rm <name>', 'Delete a provider'],
|
|
321
|
+
['use <name>', 'Set as the active provider'],
|
|
322
|
+
['mv <name> up|down', 'Reorder providers'],
|
|
323
|
+
['review [name] [--model id]', 'View / set the auto review model']
|
|
324
|
+
],
|
|
325
|
+
examples: [
|
|
326
|
+
'dgxay provider list',
|
|
327
|
+
'dgxay provider add --name DeepSeek --base-url https://api.deepseek.com/v1 --model deepseek-chat',
|
|
328
|
+
'dgxay provider add --oauth-provider openai --base-url https://api.openai.com/v1 --model gpt-5',
|
|
329
|
+
'dgxay provider review DeepSeek --model deepseek-chat'
|
|
330
|
+
],
|
|
331
|
+
run
|
|
332
|
+
};
|