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.
Files changed (46) hide show
  1. package/LICENSE +674 -0
  2. package/README-zh.md +242 -0
  3. package/README.md +223 -2
  4. package/bin/dgxay.js +42 -0
  5. package/package.json +47 -4
  6. package/src/cli/ansi.js +539 -0
  7. package/src/cli/approval.js +186 -0
  8. package/src/cli/args.js +162 -0
  9. package/src/cli/command-table.js +52 -0
  10. package/src/cli/editor.js +828 -0
  11. package/src/cli/i18n.js +276 -0
  12. package/src/cli/index.js +228 -0
  13. package/src/cli/print.js +148 -0
  14. package/src/cli/render.js +395 -0
  15. package/src/cli/repl-state.js +85 -0
  16. package/src/cli/repl.js +1231 -0
  17. package/src/cli/slash.js +75 -0
  18. package/src/cli/ui.js +52 -0
  19. package/src/commands/_shared.js +20 -0
  20. package/src/commands/chat.js +212 -0
  21. package/src/commands/config.js +363 -0
  22. package/src/commands/context.js +43 -0
  23. package/src/commands/doctor.js +192 -0
  24. package/src/commands/export.js +102 -0
  25. package/src/commands/mcp.js +257 -0
  26. package/src/commands/model.js +223 -0
  27. package/src/commands/oauth.js +246 -0
  28. package/src/commands/permission.js +108 -0
  29. package/src/commands/provider.js +332 -0
  30. package/src/commands/run.js +172 -0
  31. package/src/commands/skill.js +94 -0
  32. package/src/commands/workspace.js +100 -0
  33. package/src/core/agent.js +420 -0
  34. package/src/core/llm.js +709 -0
  35. package/src/core/mcp.js +271 -0
  36. package/src/core/oauth.js +737 -0
  37. package/src/core/prompt.js +269 -0
  38. package/src/core/sessions.js +208 -0
  39. package/src/core/tools.js +391 -0
  40. package/src/services/config.js +182 -0
  41. package/src/services/input.js +91 -0
  42. package/src/services/session-bus.js +104 -0
  43. package/src/services/settings.js +340 -0
  44. package/src/services/skills.js +125 -0
  45. package/src/services/terminal.js +124 -0
  46. package/src/services/version.js +25 -0
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * export:导出会话(桌面版没有这个能力,CLI 补上)。
5
+ *
6
+ * dgxay export 把最近一个会话输出到 stdout(Markdown)
7
+ * dgxay export -o chat.md 写入文件
8
+ * dgxay export --format json 导出 JSON
9
+ * dgxay export 2 指定会话序号
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const print = require('../cli/print');
15
+ const { actionFirst } = require('../cli/args');
16
+ const sessions = require('../core/sessions');
17
+
18
+ const options = [
19
+ { name: 'format', type: 'string', valueName: 'md|json', desc: 'Output format: markdown (default) / json' },
20
+ { name: 'output', alias: 'o', type: 'string', valueName: 'file', desc: 'Write to a file instead of printing' }
21
+ ];
22
+
23
+ function toMarkdown(chat) {
24
+ const lines = [];
25
+ const date = new Date(chat.createdAt || Date.now());
26
+ lines.push(`# ${chat.title || 'New conversation'}`);
27
+ lines.push('');
28
+ lines.push(`> ${date.toLocaleString()} · ${chat.mode === 'work' ? 'Work mode' : 'Chat mode'}${chat.skill ? ` · Skill ${chat.skill}` : ''}`);
29
+ lines.push('');
30
+ for (const message of chat.messages) {
31
+ if (message.role === 'user') {
32
+ lines.push('## User');
33
+ lines.push('');
34
+ lines.push(message.content || '');
35
+ lines.push('');
36
+ } else if (message.role === 'assistant') {
37
+ lines.push('## DgxayCode');
38
+ lines.push('');
39
+ lines.push(message.content || '');
40
+ lines.push('');
41
+ if (message.toolLog?.length) {
42
+ lines.push('<details><summary>Tool calls</summary>');
43
+ lines.push('');
44
+ for (const tool of message.toolLog) {
45
+ lines.push(`- \`${tool.name}\` ${tool.ok ? 'ok' : 'failed'}${tool.review?.reason ? ` · ${tool.review.reason}` : ''}`);
46
+ }
47
+ lines.push('');
48
+ lines.push('</details>');
49
+ lines.push('');
50
+ }
51
+ } else if (message.role === 'terminal') {
52
+ lines.push('## Terminal');
53
+ lines.push('');
54
+ lines.push('```');
55
+ lines.push(`$ ${message.command}`);
56
+ lines.push(`${message.output || ''}${message.stderr || ''}`.trimEnd());
57
+ lines.push('```');
58
+ lines.push('');
59
+ }
60
+ }
61
+ return lines.join('\n');
62
+ }
63
+
64
+ async function run({ argv }) {
65
+ const { action, rest, options: opts } = actionFirst(argv, options);
66
+ const ref = action || rest[0] || '';
67
+ const chats = sessions.listChats();
68
+ if (!chats.length) {
69
+ print.warn('No conversation to export yet');
70
+ return 1;
71
+ }
72
+ const chat = ref ? sessions.resolveChat(ref, chats) : chats[0];
73
+ if (!chat) {
74
+ print.fail(`Conversation not found: ${ref}`);
75
+ return 1;
76
+ }
77
+ const format = String(opts.format || 'md').toLowerCase();
78
+ const content =
79
+ format === 'json'
80
+ ? `${JSON.stringify(chat, null, 2)}\n`
81
+ : toMarkdown(chat);
82
+
83
+ if (opts.output) {
84
+ const target = path.resolve(opts.output);
85
+ fs.mkdirSync(path.dirname(target), { recursive: true });
86
+ fs.writeFileSync(target, content, 'utf8');
87
+ print.ok(`Exported "${chat.title}" to ${target}`);
88
+ return 0;
89
+ }
90
+ process.stdout.write(content.endsWith('\n') ? content : `${content}\n`);
91
+ return 0;
92
+ }
93
+
94
+ module.exports = {
95
+ name: 'export',
96
+ aliases: ['dump'],
97
+ summary: 'Export a conversation as Markdown / JSON',
98
+ usage: 'dgxay export [n|id] [--format md|json] [-o file]',
99
+ options,
100
+ examples: ['dgxay export', 'dgxay export 2 -o chat.md', 'dgxay export -o chats.json --format json'],
101
+ run
102
+ };
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * mcp:MCP Server 管理(对应桌面版「设置 → MCP」)。
5
+ *
6
+ * dgxay mcp list
7
+ * dgxay mcp add --name filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem ."
8
+ * dgxay mcp start <名称|ID>
9
+ * dgxay mcp stop <名称|ID>
10
+ * dgxay mcp logs <名称|ID>
11
+ * dgxay mcp rm <名称|ID>
12
+ * dgxay mcp env <名称|ID> --env "KEY=value" 追加环境变量
13
+ *
14
+ * 注意:与桌面版一致,当前版本只管理进程,不把 MCP 工具暴露给模型。
15
+ */
16
+
17
+ const print = require('../cli/print');
18
+ const { parseArgs, actionFirst } = require('../cli/args');
19
+ const { COLORS } = require('../cli/ansi');
20
+ const mcp = require('../core/mcp');
21
+ const cfg = require('../services/config');
22
+ const { ask } = require('../services/input');
23
+
24
+ const options = [
25
+ { name: 'name', type: 'string', valueName: 'text', desc: 'Display name' },
26
+ { name: 'command', type: 'string', valueName: 'cmd', desc: 'Start command (e.g. npx / uvx)' },
27
+ { name: 'args', type: 'string', valueName: 'text', desc: 'Command arguments' },
28
+ { name: 'env', type: 'list', valueName: 'K=V', desc: 'Environment variables, repeatable' },
29
+ { name: 'cwd', type: 'string', valueName: 'dir', desc: 'Working directory' },
30
+ { name: 'follow', alias: 'f', type: 'boolean', desc: 'logs: keep streaming output' },
31
+ { name: 'yes', alias: 'y', type: 'boolean', desc: 'Skip confirmation' }
32
+ ];
33
+
34
+ function statusText(status) {
35
+ if (status === 'running') return COLORS.green('running');
36
+ if (status === 'error') return COLORS.red('error');
37
+ if (status === 'stopping') return COLORS.yellow('stopping');
38
+ return COLORS.faint('stopped');
39
+ }
40
+
41
+ function list() {
42
+ const servers = mcp.listServers();
43
+ print.blank();
44
+ if (!servers.length) {
45
+ print.hint(`No MCP servers yet (config is stored in ${cfg.MCP_PATH})`);
46
+ print.hint('Add: dgxay mcp add --name filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem ."');
47
+ print.blank();
48
+ return 0;
49
+ }
50
+ print.heading(`MCP servers (${servers.length})`);
51
+ const rows = servers.map((server, index) => [
52
+ String(index + 1),
53
+ server.name,
54
+ statusText(server.status),
55
+ server.pid ? String(server.pid) : '',
56
+ `${server.command} ${(server.args || []).join(' ')}`.trim(),
57
+ server.cwd || ''
58
+ ]);
59
+ print.table(
60
+ [
61
+ { title: '#', max: 3 },
62
+ { title: 'Name', max: 20 },
63
+ { title: 'Status', max: 8 },
64
+ { title: 'PID', max: 7 },
65
+ { title: 'Command', max: 50 },
66
+ { title: 'Working dir', max: 30 }
67
+ ],
68
+ rows
69
+ );
70
+ print.blank();
71
+ print.hint('MCP servers do not start with the app; run start manually each time');
72
+ print.hint('This version only manages processes; MCP tools are not yet exposed to the model');
73
+ print.blank();
74
+ return 0;
75
+ }
76
+
77
+ function resolve(ref) {
78
+ const server = mcp.findServer(ref);
79
+ if (!server) {
80
+ print.fail(`MCP not found: ${ref}`);
81
+ return null;
82
+ }
83
+ return server;
84
+ }
85
+
86
+ async function add(opts) {
87
+ let name = opts.name;
88
+ let command = opts.command;
89
+ if (!name && process.stdin.isTTY) name = String((await ask('Name: ')) || '').trim();
90
+ if (!command && process.stdin.isTTY) command = String((await ask('Start command (e.g. npx): ')) || '').trim();
91
+ if (!name || !command) {
92
+ throw Object.assign(new Error('Usage: dgxay mcp add --name <name> --command <command> [--args "..."]'), {
93
+ userError: true
94
+ });
95
+ }
96
+ const server = mcp.addServer({
97
+ name,
98
+ command,
99
+ args: opts.args || '',
100
+ env: (opts.env || []).join('\n'),
101
+ cwd: opts.cwd || ''
102
+ });
103
+ print.ok(`Saved MCP server "${server.name}" (${server.id})`);
104
+ print.hint(`Start: dgxay mcp start ${server.name}`);
105
+ return 0;
106
+ }
107
+
108
+ function start(ref) {
109
+ const server = resolve(ref);
110
+ if (!server) return 1;
111
+ try {
112
+ const result = mcp.startServer(server.id);
113
+ if (result.status === 'running' && result.pid) print.ok(`Started ${server.name} (pid ${result.pid})`);
114
+ else print.ok(`${server.name} is already running`);
115
+ print.hint(`View logs: dgxay mcp logs ${server.name}`);
116
+ return 0;
117
+ } catch (err) {
118
+ print.fail(err.message);
119
+ return 1;
120
+ }
121
+ }
122
+
123
+ function stop(ref) {
124
+ const server = resolve(ref);
125
+ if (!server) return 1;
126
+ const ok = mcp.stopServer(server.id);
127
+ if (ok) print.ok(`Stopped ${server.name}`);
128
+ else print.warn(`${server.name} is not running`);
129
+ return 0;
130
+ }
131
+
132
+ async function logs(ref, opts) {
133
+ const server = resolve(ref);
134
+ if (!server) return 1;
135
+ const shown = new Set();
136
+ const printLogs = () => {
137
+ const entries = mcp.statusOf(server.id).logs || [];
138
+ for (const entry of entries) {
139
+ const key = `${entry.at}:${entry.stream}:${entry.text}`;
140
+ if (shown.has(key)) continue;
141
+ shown.add(key);
142
+ print.write(entry.stream === 'stderr' ? COLORS.red(entry.text) : entry.text);
143
+ }
144
+ };
145
+ printLogs();
146
+ if (!opts.follow) {
147
+ if (!shown.size) print.hint('No logs yet');
148
+ return 0;
149
+ }
150
+ print.hint('Streaming; press Ctrl+C to exit');
151
+ mcp.setNotifier((event) => {
152
+ if (event.type === 'log' && event.id === server.id) {
153
+ print.write(event.stream === 'stderr' ? COLORS.red(event.text) : event.text);
154
+ }
155
+ });
156
+ await new Promise((resolve) => {
157
+ process.once('SIGINT', () => resolve());
158
+ });
159
+ return 0;
160
+ }
161
+
162
+ async function remove(ref, opts) {
163
+ const server = resolve(ref);
164
+ if (!server) return 1;
165
+ if (!opts.yes && process.stdin.isTTY) {
166
+ const answer = await ask(`Delete MCP server "${server.name}" and stop its process? [y/N] `);
167
+ if (!/^y/i.test(String(answer || '').trim())) {
168
+ print.hint('Cancelled');
169
+ return 1;
170
+ }
171
+ }
172
+ mcp.removeServer(server.id);
173
+ print.ok(`Deleted ${server.name}`);
174
+ return 0;
175
+ }
176
+
177
+ function updateEnv(ref, opts) {
178
+ const server = resolve(ref);
179
+ if (!server) return 1;
180
+ const env = { ...(server.env || {}) };
181
+ for (const pair of opts.env || []) {
182
+ const index = String(pair).indexOf('=');
183
+ if (index > 0) env[String(pair).slice(0, index).trim()] = String(pair).slice(index + 1).trim();
184
+ }
185
+ mcp.addServer({
186
+ name: server.name,
187
+ command: server.command,
188
+ args: server.args,
189
+ env,
190
+ cwd: server.cwd
191
+ });
192
+ print.ok(`Updated environment variables for ${server.name} (${Object.keys(env).length} entries)`);
193
+ print.warn('Takes effect after restart: dgxay mcp stop ' + server.name + ' && dgxay mcp start ' + server.name);
194
+ return 0;
195
+ }
196
+
197
+ async function run({ argv }) {
198
+ const { action: rawAction, rest: restArgs, options: opts } = actionFirst(argv, options);
199
+ const action = rawAction || 'list';
200
+ const ref = restArgs.join(' ');
201
+
202
+ // 兼容:dgxay mcp <名称> 视为 start
203
+ if (!['list', 'ls', 'add', 'start', 'run', 'stop', 'logs', 'log', 'rm', 'remove', 'delete', 'env'].includes(action)) {
204
+ return start(restArgs.join(' '));
205
+ }
206
+
207
+ switch (action) {
208
+ case 'list':
209
+ case 'ls':
210
+ return list();
211
+ case 'add':
212
+ return add(opts);
213
+ case 'start':
214
+ case 'run':
215
+ if (!ref) throw Object.assign(new Error('Usage: dgxay mcp start <name|id>'), { userError: true });
216
+ return start(ref);
217
+ case 'stop':
218
+ if (!ref) throw Object.assign(new Error('Usage: dgxay mcp stop <name|id>'), { userError: true });
219
+ return stop(ref);
220
+ case 'logs':
221
+ case 'log':
222
+ if (!ref) throw Object.assign(new Error('Usage: dgxay mcp logs <name|id> [-f]'), { userError: true });
223
+ return logs(ref, opts);
224
+ case 'rm':
225
+ case 'remove':
226
+ case 'delete':
227
+ if (!ref) throw Object.assign(new Error('Usage: dgxay mcp rm <name|id>'), { userError: true });
228
+ return remove(ref, opts);
229
+ case 'env':
230
+ if (!ref) throw Object.assign(new Error('Usage: dgxay mcp env <name|id> --env "KEY=value"'), { userError: true });
231
+ return updateEnv(ref, opts);
232
+ default:
233
+ return 1;
234
+ }
235
+ }
236
+
237
+ module.exports = {
238
+ name: 'mcp',
239
+ summary: 'Manage MCP servers',
240
+ usage: 'dgxay mcp <list|add|start|stop|logs|rm|env>',
241
+ options,
242
+ subcommands: [
243
+ ['list', 'List MCP servers'],
244
+ ['add', 'Add a server'],
245
+ ['start <name>', 'Start the process'],
246
+ ['stop <name>', 'Stop the process'],
247
+ ['logs <name> [-f]', 'View logs'],
248
+ ['rm <name>', 'Delete'],
249
+ ['env <name> --env K=V', 'Update environment variables']
250
+ ],
251
+ examples: [
252
+ 'dgxay mcp add --name filesystem --command npx --args "-y @modelcontextprotocol/server-filesystem ."',
253
+ 'dgxay mcp start filesystem',
254
+ 'dgxay mcp logs filesystem -f'
255
+ ],
256
+ run
257
+ };
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * model:模型管理。
5
+ *
6
+ * dgxay model list 列出当前供应商的模型与上下文窗口
7
+ * dgxay model use <id|序号> 设为当前模型
8
+ * dgxay model add <id> [--name X] [--context-window N] [--provider P]
9
+ * dgxay model rm <id> [--provider P]
10
+ * dgxay model fetch [--provider P] [--base-url ...] [--api-key ...]
11
+ */
12
+
13
+ const print = require('../cli/print');
14
+ const { parseArgs, actionFirst } = require('../cli/args');
15
+ const { COLORS } = require('../cli/ansi');
16
+ const settings = require('../services/settings');
17
+ const sessions = require('../core/sessions');
18
+ const { askSecret } = require('../services/input');
19
+
20
+ const options = [
21
+ { name: 'provider', alias: 'p', type: 'string', valueName: 'name', desc: 'Target provider (default: active provider)' },
22
+ { name: 'name', type: 'string', valueName: 'text', desc: 'Display name' },
23
+ { name: 'context-window', type: 'number', valueName: 'n', desc: 'Context window' },
24
+ { name: 'base-url', type: 'string', valueName: 'url', desc: 'Use this base URL when fetching the list' },
25
+ { name: 'api-key', type: 'string', valueName: 'key', desc: 'Use this key when fetching the list' },
26
+ { name: 'add', type: 'list', valueName: 'id', desc: 'Add the given models to the provider after fetching' }
27
+ ];
28
+
29
+ function list(providerRef) {
30
+ const config = settings.readConfig();
31
+ const provider = providerRef ? settings.findProvider(providerRef) : settings.activeProvider(config);
32
+ if (!provider) {
33
+ print.warn('No providers yet, run dgxay config init');
34
+ return 1;
35
+ }
36
+ const models = provider.models || [];
37
+ print.blank();
38
+ print.heading(`Models for ${provider.name} (${models.length})`);
39
+ if (!models.length) {
40
+ print.hint(' No models yet, run dgxay model add <id>');
41
+ print.blank();
42
+ return 0;
43
+ }
44
+ const active = provider.id === config.activeProviderId ? config.activeModelId : '';
45
+ const rows = models.map((model, index) => [
46
+ String(index + 1),
47
+ model.id === active ? COLORS.accent('❯') : ' ',
48
+ model.name || model.id,
49
+ model.id,
50
+ model.contextWindow ? sessions.formatTokens(model.contextWindow) : COLORS.faint('protocol default'),
51
+ provider.reviewModelId === model.id ? 'review' : ''
52
+ ]);
53
+ print.table(
54
+ [
55
+ { title: '#', max: 3 },
56
+ { title: '', max: 1 },
57
+ { title: 'Name', max: 26 },
58
+ { title: 'ID', max: 32 },
59
+ { title: 'Context', max: 10 },
60
+ { title: '', max: 4 }
61
+ ],
62
+ rows
63
+ );
64
+ print.blank();
65
+ return 0;
66
+ }
67
+
68
+ async function fetchList(opts) {
69
+ const config = settings.readConfig();
70
+ const provider = opts.provider ? settings.findProvider(opts.provider) : settings.activeProvider(config);
71
+ let apiKey = opts.apiKey;
72
+ if (apiKey === undefined && provider && !(provider.authMode === 'oauth') && !provider.apiKey) {
73
+ const secret = await askSecret('API Key: ');
74
+ apiKey = secret || '';
75
+ }
76
+ process.stdout.write(`${COLORS.faint('Fetching model list...')}`);
77
+ try {
78
+ let models;
79
+ if (opts.baseUrl) {
80
+ models = await settings.fetchModels({
81
+ baseUrl: opts.baseUrl,
82
+ apiKey: apiKey ?? '',
83
+ protocol: opts.protocol || provider?.protocol || 'openai'
84
+ });
85
+ } else {
86
+ if (!provider) throw new Error('No provider available');
87
+ models = await settings.fetchModels({ providerRef: provider.id, apiKey });
88
+ }
89
+ process.stdout.write('\r\x1b[2K');
90
+ print.ok(`Fetched ${models.length} model(s)`);
91
+ const existing = new Set((provider?.models || []).map((m) => m.id));
92
+ for (const id of models) {
93
+ print.write(` ${existing.has(id) ? COLORS.green('✓') : ' '} ${id}`);
94
+ }
95
+ print.blank();
96
+ print.hint('Use dgxay model add <id> to add to the provider; if fetching fails you can type the model ID manually');
97
+ return 0;
98
+ } catch (err) {
99
+ process.stdout.write('\r\x1b[2K');
100
+ print.fail(`Fetch failed: ${err.message}`);
101
+ print.hint('Some gateways do not support the /models endpoint; add models manually with dgxay model add <id>');
102
+ return 1;
103
+ }
104
+ }
105
+
106
+ function addModel(id, opts) {
107
+ const config = settings.readConfig();
108
+ const provider = opts.provider ? settings.findProvider(opts.provider) : settings.activeProvider(config);
109
+ if (!provider) {
110
+ print.warn('No providers yet, run dgxay config init');
111
+ return 1;
112
+ }
113
+ const models = (provider.models || []).slice();
114
+ if (models.some((m) => m.id === id)) {
115
+ print.warn(`Model ${id} already exists`);
116
+ return 1;
117
+ }
118
+ models.push({
119
+ id,
120
+ name: opts.name || id,
121
+ contextWindow: Number(opts.contextWindow) || 0
122
+ });
123
+ const next = (config.providers || []).map((p) => (p.id === provider.id ? { ...p, models } : p));
124
+ const patch = { providers: next };
125
+ if (provider.id === config.activeProviderId && !config.activeModelId) patch.activeModelId = id;
126
+ settings.saveConfig(patch);
127
+ print.ok(`Model added: ${id}`);
128
+ return 0;
129
+ }
130
+
131
+ function removeModel(id, opts) {
132
+ const config = settings.readConfig();
133
+ const provider = opts.provider ? settings.findProvider(opts.provider) : settings.activeProvider(config);
134
+ if (!provider) {
135
+ print.warn('No providers yet');
136
+ return 1;
137
+ }
138
+ const models = (provider.models || []).filter((m) => m.id !== id);
139
+ if (models.length === (provider.models || []).length) {
140
+ print.fail(`Provider ${provider.name} has no model ${id}`);
141
+ return 1;
142
+ }
143
+ if (!models.length) {
144
+ print.fail('Cannot delete the last model; add another model first');
145
+ return 1;
146
+ }
147
+ const reviewModelId = models.some((m) => m.id === provider.reviewModelId)
148
+ ? provider.reviewModelId
149
+ : models[0].id;
150
+ const next = (config.providers || []).map((p) =>
151
+ p.id === provider.id ? { ...p, models, reviewModelId } : p
152
+ );
153
+ const patch = { providers: next };
154
+ if (config.activeModelId === id) patch.activeModelId = models[0].id;
155
+ settings.saveConfig(patch);
156
+ print.ok(`Model deleted: ${id}`);
157
+ return 0;
158
+ }
159
+
160
+ function useModel(ref, opts) {
161
+ const config = settings.readConfig();
162
+ const provider = opts.provider ? settings.findProvider(opts.provider) : settings.activeProvider(config);
163
+ if (!provider) {
164
+ print.warn('No providers yet');
165
+ return 1;
166
+ }
167
+ const models = provider.models || [];
168
+ const index = Number(ref);
169
+ const target =
170
+ (Number.isInteger(index) && index >= 1 && index <= models.length ? models[index - 1] : null) ||
171
+ models.find((m) => m.id === ref) ||
172
+ models.find((m) => m.id.includes(ref));
173
+ if (!target) {
174
+ print.fail(`Model not found: ${ref}`);
175
+ return 1;
176
+ }
177
+ settings.setActiveModel(provider.id, target.id);
178
+ print.ok(`Active model: ${target.name || target.id} (${provider.name})`);
179
+ return 0;
180
+ }
181
+
182
+ async function run({ argv }) {
183
+ const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
184
+ const action = rawAction || 'list';
185
+
186
+ // model add deepseek-chat gpt-4o 支持多个
187
+ if (action === 'add') {
188
+ if (!rest.length) throw Object.assign(new Error('Usage: dgxay model add <id> [<id>...]'), { userError: true });
189
+ let code = 0;
190
+ for (const id of rest) code = Math.max(code, addModel(id, opts));
191
+ return code;
192
+ }
193
+ if (action === 'rm' || action === 'remove' || action === 'delete') {
194
+ if (!rest[0]) throw Object.assign(new Error('Usage: dgxay model rm <id>'), { userError: true });
195
+ return removeModel(rest[0], opts);
196
+ }
197
+ if (action === 'use' || action === 'set') {
198
+ if (!rest[0]) throw Object.assign(new Error('Usage: dgxay model use <id|index>'), { userError: true });
199
+ return useModel(rest[0], opts);
200
+ }
201
+ if (action === 'fetch' || action === 'ls-remote') return fetchList(opts);
202
+ if (action === 'list' || action === 'ls') return list(opts.provider);
203
+
204
+ // 不是已知子命令:当作模型 ID 使用
205
+ return useModel(action, opts);
206
+ }
207
+
208
+ module.exports = {
209
+ name: 'model',
210
+ aliases: ['models'],
211
+ summary: 'Model management: list, switch, add/remove, fetch',
212
+ usage: 'dgxay model <list|use|add|rm|fetch>',
213
+ options,
214
+ subcommands: [
215
+ ['list', 'List models'],
216
+ ['use <id|index>', 'Switch the active model'],
217
+ ['add <id>', 'Add a model'],
218
+ ['rm <id>', 'Delete a model'],
219
+ ['fetch', 'Fetch the model list from the provider API']
220
+ ],
221
+ examples: ['dgxay model list', 'dgxay model use 2', 'dgxay model add gpt-4o --name GPT-4o', 'dgxay model fetch'],
222
+ run
223
+ };