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,363 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 首次配置向导 / 配置查看。
|
|
5
|
+
*
|
|
6
|
+
* dgxay config init 交互式填写供应商、Base URL、API Key、模型
|
|
7
|
+
* dgxay config init --protocol openai --base-url ... --api-key ... --model gpt-4o
|
|
8
|
+
* dgxay config show 查看当前配置(密钥打码)
|
|
9
|
+
* dgxay config get <a.b.c> 读取单项配置
|
|
10
|
+
* dgxay config set <a.b.c> <value> 写入单项配置
|
|
11
|
+
* dgxay config path 显示数据目录与各文件的路径
|
|
12
|
+
* dgxay config reset 重置配置(不删除 oauth.json / mcp.json / skills)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const print = require('../cli/print');
|
|
18
|
+
const { parseArgs, actionFirst } = require('../cli/args');
|
|
19
|
+
const { COLORS, termWidth } = require('../cli/ansi');
|
|
20
|
+
const settings = require('../services/settings');
|
|
21
|
+
const cfg = require('../services/config');
|
|
22
|
+
const oauth = require('../core/oauth');
|
|
23
|
+
const { ask, askSecret, confirm } = require('../services/input');
|
|
24
|
+
const { VERSION } = require('../services/version');
|
|
25
|
+
|
|
26
|
+
const PRESETS = [
|
|
27
|
+
{ name: 'OpenAI', protocol: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o' },
|
|
28
|
+
{ name: 'DeepSeek', protocol: 'openai', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat' },
|
|
29
|
+
{ name: 'Claude', protocol: 'anthropic', baseUrl: 'https://api.anthropic.com', model: 'claude-sonnet-4-5' },
|
|
30
|
+
{ name: 'OpenAI Responses', protocol: 'responses', baseUrl: 'https://api.openai.com/v1', model: 'gpt-5' },
|
|
31
|
+
{ name: 'Qwen (DashScope)', protocol: 'openai', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' },
|
|
32
|
+
{ name: 'Moonshot', protocol: 'openai', baseUrl: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k' },
|
|
33
|
+
{ name: 'Ollama (local)', protocol: 'openai', baseUrl: 'http://localhost:11434/v1', model: 'llama3.1' }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const options = [
|
|
37
|
+
{ name: 'protocol', type: 'string', valueName: 'p', desc: 'openai / anthropic / responses' },
|
|
38
|
+
{ name: 'name', type: 'string', valueName: 'text', desc: 'Provider name' },
|
|
39
|
+
{ name: 'base-url', type: 'string', valueName: 'url', desc: 'Base URL' },
|
|
40
|
+
{ name: 'api-key', type: 'string', valueName: 'key', desc: 'API Key (DGXAY_API_KEY env var also works)' },
|
|
41
|
+
{ name: 'model', type: 'list', valueName: 'id', desc: 'Model ID; repeatable or comma-separated' },
|
|
42
|
+
{ name: 'oauth-provider', type: 'string', valueName: 'id', desc: 'Use a connected OAuth account instead: openai/anthropic/xai/google' },
|
|
43
|
+
{ name: 'force', alias: 'f', type: 'boolean', desc: 'Overwrite even when config already exists' }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/* ------------------------------ init ------------------------------ */
|
|
47
|
+
|
|
48
|
+
async function promptProtocol(config) {
|
|
49
|
+
const interactive = process.stdin.isTTY;
|
|
50
|
+
if (!interactive) return 'openai';
|
|
51
|
+
print.blank();
|
|
52
|
+
print.heading('API protocol');
|
|
53
|
+
const list = ['openai', 'anthropic', 'responses'];
|
|
54
|
+
list.forEach((key, index) => {
|
|
55
|
+
const meta = settings.PROTOCOL_META[key];
|
|
56
|
+
print.write(` ${COLORS.accent(String(index + 1))}. ${meta.full} ${COLORS.faint(meta.path)}`);
|
|
57
|
+
});
|
|
58
|
+
const answer = await ask(`Choose [1-3, default 1]: `);
|
|
59
|
+
const index = Number(String(answer || '').trim());
|
|
60
|
+
if (Number.isInteger(index) && index >= 1 && index <= 3) return list[index - 1];
|
|
61
|
+
return 'openai';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function promptPreset(protocol) {
|
|
65
|
+
const interactive = process.stdin.isTTY;
|
|
66
|
+
if (!interactive) return null;
|
|
67
|
+
print.blank();
|
|
68
|
+
print.heading('Quick presets');
|
|
69
|
+
const matches = PRESETS.filter((preset) => preset.protocol === protocol);
|
|
70
|
+
matches.forEach((preset, index) => {
|
|
71
|
+
print.write(` ${COLORS.accent(String(index + 1))}. ${preset.name} ${COLORS.faint(preset.baseUrl)}`);
|
|
72
|
+
});
|
|
73
|
+
print.write(` ${COLORS.faint('Press Enter to skip and fill in manually')}`);
|
|
74
|
+
const answer = await ask('Choose a preset: ');
|
|
75
|
+
const index = Number(String(answer || '').trim());
|
|
76
|
+
if (Number.isInteger(index) && index >= 1 && index <= matches.length) return matches[index - 1];
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runInit(opts, runtime) {
|
|
81
|
+
const config = settings.readConfig();
|
|
82
|
+
if (config.providers?.length && !opts.force) {
|
|
83
|
+
print.warn(`${config.providers.length} provider(s) already configured. Use dgxay provider add to append a new provider`);
|
|
84
|
+
print.hint('To re-initialize the first provider, add --force');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const interactive = Boolean(process.stdin.isTTY) && !opts.protocol && !opts.baseUrl;
|
|
89
|
+
let protocol = String(opts.protocol || '').trim();
|
|
90
|
+
let name = String(opts.name || '').trim();
|
|
91
|
+
let baseUrl = String(opts.baseUrl || '').trim();
|
|
92
|
+
let apiKey = opts.apiKey !== undefined ? String(opts.apiKey) : process.env.DGXAY_API_KEY || '';
|
|
93
|
+
let models = (opts.model || []).flatMap((item) => String(item).split(',')).map((item) => item.trim()).filter(Boolean);
|
|
94
|
+
let oauthProvider = String(opts.oauthProvider || '').trim();
|
|
95
|
+
|
|
96
|
+
if (interactive) {
|
|
97
|
+
print.write('');
|
|
98
|
+
print.write(`${COLORS.bold('DgxayCode first-time setup')} ${COLORS.faint(`v${VERSION}`)}`);
|
|
99
|
+
print.write(COLORS.faint('Config is written to the local data directory; the API key stays on this machine and is never uploaded.'));
|
|
100
|
+
print.write(COLORS.faint(`Data directory: ${cfg.DCB_DIR}`));
|
|
101
|
+
|
|
102
|
+
protocol = protocol || (await promptProtocol(config));
|
|
103
|
+
const preset = await promptPreset(protocol);
|
|
104
|
+
if (preset) {
|
|
105
|
+
name = name || preset.name;
|
|
106
|
+
baseUrl = baseUrl || preset.baseUrl;
|
|
107
|
+
models = models.length ? models : [preset.model];
|
|
108
|
+
print.write(COLORS.faint(`Selected ${preset.name}, Base URL pre-filled with ${preset.baseUrl}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!name) {
|
|
112
|
+
const answer = await ask(`Provider name [${preset?.name || 'My provider'}]: `);
|
|
113
|
+
name = String(answer || '').trim() || preset?.name || 'My provider';
|
|
114
|
+
}
|
|
115
|
+
if (!baseUrl) {
|
|
116
|
+
const answer = await ask(`Base URL [${settings.PROTOCOL_META[protocol].basePlaceholder}]: `);
|
|
117
|
+
baseUrl = String(answer || '').trim() || settings.PROTOCOL_META[protocol].basePlaceholder;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const useOauth = await confirm('Use a connected OAuth account (instead of an API Key)?', false);
|
|
121
|
+
if (useOauth) {
|
|
122
|
+
const connected = oauth.listProviders().filter((item) => item.connection && !item.connection.expired);
|
|
123
|
+
if (!connected.length) {
|
|
124
|
+
print.warn('No accounts available yet; continue with an API Key. You can run dgxay oauth connect <platform> later');
|
|
125
|
+
} else {
|
|
126
|
+
connected.forEach((item, index) =>
|
|
127
|
+
print.write(` ${COLORS.accent(String(index + 1))}. ${item.name} ${COLORS.faint(item.connection.account)}`)
|
|
128
|
+
);
|
|
129
|
+
const answer = await ask('Choose an account [1]: ');
|
|
130
|
+
const index = Number(String(answer || '').trim()) || 1;
|
|
131
|
+
oauthProvider = connected[index - 1]?.id || '';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!oauthProvider && !apiKey) {
|
|
136
|
+
const secret = await askSecret('API Key (may be empty for local models): ');
|
|
137
|
+
apiKey = secret !== null ? secret : '';
|
|
138
|
+
}
|
|
139
|
+
if (!models.length) {
|
|
140
|
+
const answer = await ask(`Model ID [${settings.PROTOCOL_META[protocol].modelPlaceholder}]: `);
|
|
141
|
+
models = String(answer || '')
|
|
142
|
+
.split(/[,,\s]+/)
|
|
143
|
+
.map((item) => item.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
if (!models.length) {
|
|
147
|
+
print.fail('Model ID cannot be empty');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!protocol) protocol = 'openai';
|
|
153
|
+
if (!baseUrl) baseUrl = settings.PROTOCOL_META[protocol].basePlaceholder;
|
|
154
|
+
if (!models.length) {
|
|
155
|
+
throw Object.assign(new Error('Missing --model, e.g. --model deepseek-chat'), { userError: true });
|
|
156
|
+
}
|
|
157
|
+
if (!apiKey && !oauthProvider) {
|
|
158
|
+
print.warn('No API Key provided: local models (Ollama, etc.) work without one, cloud endpoints return 401');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = settings.upsertProvider({
|
|
162
|
+
protocol,
|
|
163
|
+
name,
|
|
164
|
+
baseUrl,
|
|
165
|
+
apiKey,
|
|
166
|
+
models,
|
|
167
|
+
authMode: oauthProvider ? 'oauth' : 'apiKey',
|
|
168
|
+
oauthProvider
|
|
169
|
+
});
|
|
170
|
+
settings.saveConfig({ onboarded: true });
|
|
171
|
+
|
|
172
|
+
print.blank();
|
|
173
|
+
print.ok(`Provider saved: '${result.provider.name}'(${settings.PROTOCOL_META[result.provider.protocol].full})`);
|
|
174
|
+
print.keyValue([
|
|
175
|
+
{ label: 'Base URL', value: result.provider.baseUrl },
|
|
176
|
+
{ label: 'Models', value: result.provider.models.map((m) => m.id).join(', ') },
|
|
177
|
+
{ label: 'Review model', value: result.provider.reviewModelId || 'not configured' },
|
|
178
|
+
{ label: 'Auth', value: oauthProvider ? `OAuth · ${oauthProvider}` : apiKey ? 'API Key' : 'none (local model)' },
|
|
179
|
+
{ label: 'Config file', value: settings.homeRelative(cfg.CONFIG_PATH) }
|
|
180
|
+
]);
|
|
181
|
+
print.blank();
|
|
182
|
+
print.hint('Next: run dgxay doctor to check connectivity, then run dgxay to start chatting');
|
|
183
|
+
void interactive;
|
|
184
|
+
void runtime;
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ------------------------------ show / get / set / path / reset ------------------------------ */
|
|
189
|
+
|
|
190
|
+
function runShow() {
|
|
191
|
+
const config = settings.readConfig();
|
|
192
|
+
const provider = settings.activeProvider(config);
|
|
193
|
+
const model = settings.activeModel(config);
|
|
194
|
+
print.heading('Current config');
|
|
195
|
+
print.keyValue(
|
|
196
|
+
[
|
|
197
|
+
{ label: 'Data directory', value: settings.homeRelative(cfg.DCB_DIR) },
|
|
198
|
+
{ label: 'Config file', value: settings.homeRelative(cfg.CONFIG_PATH) },
|
|
199
|
+
{ label: 'Mode', value: config.ui?.mode === 'work' ? 'work mode' : 'chat mode' },
|
|
200
|
+
{
|
|
201
|
+
label: 'Permission',
|
|
202
|
+
value: `${settings.PERMISSIONS[settings.currentPermission(config)].label}${
|
|
203
|
+
config.ui?.permission === 'auto' && !settings.hasReviewModel(config) ? ' (review model missing, fell back to normal permission)' : ''
|
|
204
|
+
}`
|
|
205
|
+
},
|
|
206
|
+
{ label: 'Workspace', value: config.work?.workspace || '(not selected)' },
|
|
207
|
+
{ label: 'Active provider', value: provider ? `${provider.name} (${provider.id})` : 'not configured' },
|
|
208
|
+
{ label: 'Active model', value: model ? `${model.name}${model.id !== model.name ? ` (${model.id})` : ''}` : 'not configured' },
|
|
209
|
+
{ label: 'Streaming', value: config.chat?.stream === false ? 'off' : 'on' },
|
|
210
|
+
{ label: 'Temperature', value: String(config.chat?.temperature ?? 0.7) },
|
|
211
|
+
{ label: 'Max output', value: String(config.chat?.maxTokens ?? 4096) },
|
|
212
|
+
{ label: 'Custom instructions', value: config.chat?.systemPrompt ? 'set' : 'not set' }
|
|
213
|
+
],
|
|
214
|
+
{}
|
|
215
|
+
);
|
|
216
|
+
print.blank();
|
|
217
|
+
print.heading('Providers');
|
|
218
|
+
if (!config.providers?.length) {
|
|
219
|
+
print.hint(' No providers yet, run dgxay config init');
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
const rows = config.providers.map((item, index) => [
|
|
223
|
+
String(index + 1),
|
|
224
|
+
item.name,
|
|
225
|
+
settings.PROTOCOL_META[item.protocol || 'openai'].short,
|
|
226
|
+
item.baseUrl,
|
|
227
|
+
item.authMode === 'oauth' ? `OAuth(${item.oauthProvider || '?'})` : item.apiKey ? cfg.maskKey(item.apiKey) : 'none',
|
|
228
|
+
(item.models || []).length,
|
|
229
|
+
item.reviewModelId || '-'
|
|
230
|
+
]);
|
|
231
|
+
print.table(
|
|
232
|
+
[
|
|
233
|
+
{ title: '#', max: 3 },
|
|
234
|
+
{ title: 'Name', max: 18 },
|
|
235
|
+
{ title: 'Protocol', max: 16 },
|
|
236
|
+
{ title: 'Base URL', max: 40 },
|
|
237
|
+
{ title: 'Auth', max: 16 },
|
|
238
|
+
{ title: 'Models', max: 5 },
|
|
239
|
+
{ title: 'Review model', max: 20 }
|
|
240
|
+
],
|
|
241
|
+
rows
|
|
242
|
+
);
|
|
243
|
+
print.blank();
|
|
244
|
+
print.hint('Add/modify providers: dgxay provider --help');
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getPath(object, keyPath) {
|
|
249
|
+
return String(keyPath)
|
|
250
|
+
.split('.')
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.reduce((acc, key) => (acc == null ? undefined : acc[key]), object);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function runCmd(argv) {
|
|
256
|
+
const { positionals } = parseArgs(argv, []);
|
|
257
|
+
const action = positionals[0] || 'show';
|
|
258
|
+
return { action, rest: positionals.slice(1) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function run({ argv, runtime }) {
|
|
262
|
+
const { action, rest, options: opts } = actionFirst(argv, options);
|
|
263
|
+
|
|
264
|
+
if (action === 'init') return runInit(opts, runtime);
|
|
265
|
+
if (action === 'show' || action === 'list' || !action) return runShow();
|
|
266
|
+
|
|
267
|
+
if (action === 'get') {
|
|
268
|
+
if (!rest[0]) throw Object.assign(new Error('Specify a config key, e.g. dgxay config get ui.permission'), { userError: true });
|
|
269
|
+
const value = getPath(settings.readConfig(), rest[0]);
|
|
270
|
+
if (value === undefined) {
|
|
271
|
+
print.fail(`No such config key: ${rest[0]}`);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write(`${typeof value === 'string' ? value : JSON.stringify(value)}\n`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (action === 'set') {
|
|
279
|
+
const key = rest[0];
|
|
280
|
+
const raw = rest.slice(1).join(' ');
|
|
281
|
+
if (!key || !raw) {
|
|
282
|
+
throw Object.assign(new Error('Usage: dgxay config set <key> <value>, e.g. dgxay config set chat.temperature 0.3'), {
|
|
283
|
+
userError: true
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const allowed = ['ui.permission', 'ui.mode', 'chat.temperature', 'chat.maxTokens', 'chat.stream', 'chat.systemPrompt'];
|
|
287
|
+
if (!allowed.includes(key)) {
|
|
288
|
+
throw Object.assign(new Error(`Cannot modify ${key}. Editable keys: ${allowed.join(', ')}`), { userError: true });
|
|
289
|
+
}
|
|
290
|
+
let value = raw;
|
|
291
|
+
if (key === 'chat.temperature' || key === 'chat.maxTokens') value = Number(raw);
|
|
292
|
+
if (!Number.isFinite(value) && typeof value === 'number') {
|
|
293
|
+
throw Object.assign(new Error('A number is required'), { userError: true });
|
|
294
|
+
}
|
|
295
|
+
if (key === 'chat.stream') value = !/^(false|0|no|off)$/i.test(raw);
|
|
296
|
+
if (key === 'ui.permission' && !settings.PERMISSIONS[raw]) {
|
|
297
|
+
throw Object.assign(new Error('Permission must be one of none / normal / auto / full'), { userError: true });
|
|
298
|
+
}
|
|
299
|
+
const [section, name] = key.split('.');
|
|
300
|
+
settings.saveConfig({ [section]: { [name]: value } });
|
|
301
|
+
print.ok(`${key} = ${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (action === 'path') {
|
|
306
|
+
const paths = settings.paths();
|
|
307
|
+
print.keyValue([
|
|
308
|
+
{ label: 'Data directory', value: paths.dataDir },
|
|
309
|
+
{ label: 'Config file', value: paths.config },
|
|
310
|
+
{ label: 'Conversations', value: paths.chats },
|
|
311
|
+
{ label: 'OAuth', value: paths.oauth },
|
|
312
|
+
{ label: 'MCP', value: paths.mcp },
|
|
313
|
+
{ label: 'Skills directory', value: paths.skills }
|
|
314
|
+
]);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (action === 'reset') {
|
|
319
|
+
const yes =
|
|
320
|
+
argv.includes('--yes') ||
|
|
321
|
+
(await confirm('This resets all config (providers, keys, preferences) and clears conversation history. Continue?', false));
|
|
322
|
+
if (!yes) {
|
|
323
|
+
print.hint('Cancelled');
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
const backup = `${cfg.CONFIG_PATH}.bak-${Date.now()}`;
|
|
327
|
+
try {
|
|
328
|
+
if (fs.existsSync(cfg.CONFIG_PATH)) fs.copyFileSync(cfg.CONFIG_PATH, backup);
|
|
329
|
+
} catch {
|
|
330
|
+
/* ignore */
|
|
331
|
+
}
|
|
332
|
+
cfg.resetConfig();
|
|
333
|
+
cfg.saveChats([]);
|
|
334
|
+
print.ok('Config reset');
|
|
335
|
+
print.hint('oauth.json, mcp.json and skills/ are not deleted; remove the data directory manually for a full clean-up');
|
|
336
|
+
if (fs.existsSync(backup)) print.hint(`Previous config backed up to ${path.basename(backup)}`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
print.fail(`Unknown config subcommand: ${action}`);
|
|
341
|
+
print.hint('Available: init / show / get / set / path / reset');
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
name: 'config',
|
|
347
|
+
aliases: ['cfg'],
|
|
348
|
+
summary: 'Config: initialize, view, modify data directory',
|
|
349
|
+
usage: 'dgxay config <init|show|get|set|path|reset>',
|
|
350
|
+
options,
|
|
351
|
+
subcommands: [
|
|
352
|
+
['init', 'Initial config of provider and model'],
|
|
353
|
+
['show', 'View current config (keys masked)'],
|
|
354
|
+
['get <key>', 'Read a single config key'],
|
|
355
|
+
['set <key> <value>', 'Set a single config key'],
|
|
356
|
+
['path', 'Show data directory and file paths'],
|
|
357
|
+
['reset', 'Reset config (keeps OAuth / MCP / skills)']
|
|
358
|
+
],
|
|
359
|
+
examples: ['dgxay config init', 'dgxay config show', 'dgxay config set chat.temperature 0.3', 'dgxay config path'],
|
|
360
|
+
run,
|
|
361
|
+
runCmd,
|
|
362
|
+
getPath
|
|
363
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* context:查看某段会话的上下文估算用量(对应桌面版的上下文圆环)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const print = require('../cli/print');
|
|
8
|
+
const { actionFirst } = require('../cli/args');
|
|
9
|
+
const sessions = require('../core/sessions');
|
|
10
|
+
const settings = require('../services/settings');
|
|
11
|
+
const { contextInfo, contextReport } = require('../cli/ui');
|
|
12
|
+
|
|
13
|
+
const options = [];
|
|
14
|
+
|
|
15
|
+
async function run({ argv }) {
|
|
16
|
+
const { action, rest } = actionFirst(argv, options);
|
|
17
|
+
const ref = action || rest[0] || '';
|
|
18
|
+
const config = settings.readConfig();
|
|
19
|
+
const model = settings.activeModel(config);
|
|
20
|
+
const chat = ref ? sessions.resolveChat(ref) : sessions.listChats()[0] || null;
|
|
21
|
+
if (ref && !chat) {
|
|
22
|
+
print.fail(`Conversation not found: ${ref}`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
if (!chat) {
|
|
26
|
+
print.hint('No conversation history yet');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
process.stdout.write(`\n${contextReport(contextInfo(chat, model))}\n`);
|
|
30
|
+
print.hint(`Conversation: ${chat.title} (${chat.id})`);
|
|
31
|
+
print.blank();
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
name: 'context',
|
|
37
|
+
aliases: ['ctx'],
|
|
38
|
+
summary: 'Show the estimated context usage',
|
|
39
|
+
usage: 'dgxay context [n|id]',
|
|
40
|
+
options,
|
|
41
|
+
examples: ['dgxay context', 'dgxay context 1'],
|
|
42
|
+
run
|
|
43
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doctor:环境自检。检查 Node 版本、数据目录、权限、供应商、模型连通性、OAuth、MCP、技能。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const print = require('../cli/print');
|
|
11
|
+
const { COLORS, SIGN } = require('../cli/ansi');
|
|
12
|
+
const settings = require('../services/settings');
|
|
13
|
+
const cfg = require('../services/config');
|
|
14
|
+
const mcp = require('../core/mcp');
|
|
15
|
+
const skills = require('../services/skills');
|
|
16
|
+
const oauth = require('../core/oauth');
|
|
17
|
+
const { streamChat } = require('../core/llm');
|
|
18
|
+
const { resolveProviderAuth } = require('../core/prompt');
|
|
19
|
+
const { VERSION } = require('../services/version');
|
|
20
|
+
|
|
21
|
+
const options = [
|
|
22
|
+
{ name: 'offline', type: 'boolean', desc: 'Skip network connectivity test' },
|
|
23
|
+
{ name: 'timeout', type: 'number', valueName: 'ms', desc: 'Connectivity test timeout, default 12000 ms' }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function line(status, label, detail = '') {
|
|
27
|
+
const symbol =
|
|
28
|
+
status === 'ok'
|
|
29
|
+
? COLORS.green(SIGN.ok)
|
|
30
|
+
: status === 'warn'
|
|
31
|
+
? COLORS.yellow('!')
|
|
32
|
+
: status === 'fail'
|
|
33
|
+
? COLORS.red(SIGN.fail)
|
|
34
|
+
: COLORS.faint('-');
|
|
35
|
+
process.stdout.write(` ${symbol} ${label}${detail ? COLORS.faint(` ${detail}`) : ''}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function checkConnectivity(provider, modelId, timeout) {
|
|
39
|
+
const auth = await resolveProviderAuth(provider);
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
try {
|
|
44
|
+
const result = await streamChat({
|
|
45
|
+
protocol: provider.protocol || 'openai',
|
|
46
|
+
baseUrl: provider.baseUrl,
|
|
47
|
+
apiKey: auth.apiKey,
|
|
48
|
+
authType: auth.authType,
|
|
49
|
+
model: modelId,
|
|
50
|
+
messages: [{ role: 'user', content: 'Reply with just OK.' }],
|
|
51
|
+
stream: false,
|
|
52
|
+
temperature: 0,
|
|
53
|
+
maxTokens: 16,
|
|
54
|
+
signal: controller.signal
|
|
55
|
+
});
|
|
56
|
+
return { ok: true, ms: Date.now() - started, text: String(result.content || '').trim().slice(0, 60) };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { ok: false, ms: Date.now() - started, error: err?.message || String(err) };
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function run({ argv }) {
|
|
65
|
+
const { parseArgs } = require('../cli/args');
|
|
66
|
+
const { options: opts } = parseArgs(argv, options);
|
|
67
|
+
const timeout = Number(opts.timeout) > 0 ? Number(opts.timeout) : 12000;
|
|
68
|
+
|
|
69
|
+
print.write('');
|
|
70
|
+
print.heading(`DgxayCode CLI self-check v${VERSION}`);
|
|
71
|
+
print.write('');
|
|
72
|
+
|
|
73
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
74
|
+
line(major >= 20 ? 'ok' : major >= 18 ? 'warn' : 'fail', `Node.js ${process.versions.node}`, major >= 18 ? '' : 'requires 18+');
|
|
75
|
+
line('ok', `Platform ${process.platform} ${process.arch}`, `Home directory ${os.homedir()}`);
|
|
76
|
+
if (typeof fetch !== 'function') line('fail', 'Global fetch unavailable', 'requires Node.js 18+');
|
|
77
|
+
|
|
78
|
+
// 数据目录
|
|
79
|
+
const dir = cfg.DCB_DIR;
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
82
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
83
|
+
line('ok', 'Data directory writable', settings.homeRelative(dir));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
line('fail', 'Data directory not writable', `${dir} · ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (fs.existsSync(cfg.CONFIG_PATH)) line('ok', 'config.json present', settings.homeRelative(cfg.CONFIG_PATH));
|
|
88
|
+
else line('warn', 'No config.json yet', 'run dgxay config init');
|
|
89
|
+
|
|
90
|
+
const config = settings.readConfig();
|
|
91
|
+
const mode = config.ui?.mode === 'work' ? 'work' : 'chat';
|
|
92
|
+
const permission = settings.currentPermission(config);
|
|
93
|
+
line('ok', `Mode / Permission`, `${mode === 'work' ? 'work mode' : 'chat mode'} · ${settings.PERMISSIONS[permission].label}`);
|
|
94
|
+
if (config.ui?.permission === 'auto' && !settings.hasReviewModel(config)) {
|
|
95
|
+
line('warn', 'Auto review fell back', 'the current/default provider has no valid review model');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const workspace = config.work?.workspace || '';
|
|
99
|
+
if (workspace) {
|
|
100
|
+
try {
|
|
101
|
+
const isDir = fs.statSync(workspace).isDirectory();
|
|
102
|
+
line(isDir ? 'ok' : 'fail', 'Workspace', workspace);
|
|
103
|
+
} catch {
|
|
104
|
+
line('warn', 'Workspace no longer exists', workspace);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
line('warn', 'No workspace selected', 'work mode cannot read or write files; set one with /cd or dgxay workspace set');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 供应商
|
|
111
|
+
print.write('');
|
|
112
|
+
print.heading('Providers');
|
|
113
|
+
const providers = config.providers || [];
|
|
114
|
+
if (!providers.length) {
|
|
115
|
+
line('fail', 'No providers', 'run dgxay config init');
|
|
116
|
+
}
|
|
117
|
+
for (const provider of providers) {
|
|
118
|
+
const models = provider.models || [];
|
|
119
|
+
const reviewOk = models.some((m) => m.id === provider.reviewModelId);
|
|
120
|
+
line('ok', provider.name, `${provider.protocol || 'openai'} · ${provider.baseUrl} · ${models.length} models`);
|
|
121
|
+
if (!reviewOk) line('warn', ` ${provider.name} invalid review model`, provider.reviewModelId || 'not configured');
|
|
122
|
+
if (provider.authMode === 'oauth') {
|
|
123
|
+
const account = oauth.listProviders().find((item) => item.id === provider.oauthProvider);
|
|
124
|
+
if (account?.connection && !account.connection.expired) {
|
|
125
|
+
line('ok', ` OAuth account`, `${provider.oauthProvider} · ${account.connection.account}`);
|
|
126
|
+
} else {
|
|
127
|
+
line('fail', ` OAuth account unavailable`, `dgxay oauth connect ${provider.oauthProvider || '<provider>'}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!opts.offline && providers.length) {
|
|
133
|
+
print.write('');
|
|
134
|
+
print.heading('Connectivity test');
|
|
135
|
+
const active = settings.activeProvider(config);
|
|
136
|
+
const model = settings.activeModel(config);
|
|
137
|
+
if (!active || !model) {
|
|
138
|
+
line('warn', 'Skipped: no current model');
|
|
139
|
+
} else {
|
|
140
|
+
process.stdout.write(` ${COLORS.faint(`${SIGN.run} Requesting ${active.name} / ${model.id}…`)}`);
|
|
141
|
+
const result = await checkConnectivity(active, model.id, timeout);
|
|
142
|
+
process.stdout.write('\r\x1b[2K');
|
|
143
|
+
if (result.ok) line('ok', `${active.name} / ${model.id}`, `${result.ms}ms${result.text ? ` · ${result.text}` : ''}`);
|
|
144
|
+
else line('fail', `${active.name} / ${model.id}`, `${result.ms}ms · ${result.error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// OAuth / MCP / 技能
|
|
149
|
+
print.write('');
|
|
150
|
+
print.heading('Accounts and extensions');
|
|
151
|
+
const accounts = oauth.listProviders();
|
|
152
|
+
const connected = accounts.filter((item) => item.connection && !item.connection.expired);
|
|
153
|
+
if (connected.length) {
|
|
154
|
+
for (const account of connected) line('ok', `${account.name} account`, account.connection.account);
|
|
155
|
+
} else {
|
|
156
|
+
line('-', 'No connected OAuth accounts', 'optional: dgxay oauth connect openai');
|
|
157
|
+
}
|
|
158
|
+
const servers = mcp.listServers();
|
|
159
|
+
if (servers.length) {
|
|
160
|
+
for (const server of servers) {
|
|
161
|
+
line('ok', `MCP ${server.name}`, `${server.command} ${(server.args || []).join(' ')}`.trim());
|
|
162
|
+
}
|
|
163
|
+
print.hint(' Note: this version only manages MCP processes; MCP tools are not yet exposed to the model');
|
|
164
|
+
} else {
|
|
165
|
+
line('-', 'No MCP servers');
|
|
166
|
+
}
|
|
167
|
+
const skillList = skills.listSkills();
|
|
168
|
+
line(skillList.length ? 'ok' : '-', `Skills: ${skillList.length}`, settings.homeRelative(cfg.SKILLS_DIR));
|
|
169
|
+
|
|
170
|
+
const chatsPath = path.join(dir, 'chats.json');
|
|
171
|
+
if (fs.existsSync(chatsPath)) {
|
|
172
|
+
try {
|
|
173
|
+
const chats = JSON.parse(fs.readFileSync(chatsPath, 'utf8'));
|
|
174
|
+
line('ok', 'Conversation history', `${Array.isArray(chats) ? chats.length : 0} entries`);
|
|
175
|
+
} catch {
|
|
176
|
+
line('warn', 'Conversation history could not be parsed', chatsPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
print.write('');
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
name: 'doctor',
|
|
186
|
+
aliases: ['check'],
|
|
187
|
+
summary: 'Self-check: environment, config, model connectivity',
|
|
188
|
+
usage: 'dgxay doctor [--offline]',
|
|
189
|
+
options,
|
|
190
|
+
examples: ['dgxay doctor', 'dgxay doctor --offline'],
|
|
191
|
+
run
|
|
192
|
+
};
|