dsh-agy-cli 0.0.0-stage → 0.12.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 +30 -0
- package/README.en.md +27 -0
- package/README.md +94 -2
- package/agents/dsh-agy-image-view.md +17 -0
- package/agents/dsh-agy-mcp-executor.md +32 -0
- package/agents/dsh-agy-read-only.md +20 -0
- package/agents/dsh-agy-tool-free.md +16 -0
- package/agents/dsh-agy-workspace-write.md +23 -0
- package/bin/dsh-agy-cli.js +9 -0
- package/bin/win32-x64/agy-launcher.exe +0 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +4259 -0
- package/lib/agent-installer.d.ts +66 -0
- package/lib/agent-installer.js +147 -0
- package/lib/agent-presets.d.ts +42 -0
- package/lib/agent-presets.js +130 -0
- package/lib/agents-cli.d.ts +3 -0
- package/lib/agents-cli.js +110 -0
- package/lib/agy/diagnostics.d.ts +29 -0
- package/lib/agy/diagnostics.js +109 -0
- package/lib/agy/errors.d.ts +20 -0
- package/lib/agy/errors.js +77 -0
- package/lib/agy/experimental-transport.d.ts +79 -0
- package/lib/agy/experimental-transport.js +612 -0
- package/lib/agy/keepalive.d.ts +140 -0
- package/lib/agy/keepalive.js +424 -0
- package/lib/agy/limiter.d.ts +32 -0
- package/lib/agy/limiter.js +111 -0
- package/lib/agy/log.d.ts +214 -0
- package/lib/agy/log.js +503 -0
- package/lib/agy/models.d.ts +49 -0
- package/lib/agy/models.js +203 -0
- package/lib/agy/overrides.d.ts +17 -0
- package/lib/agy/overrides.js +73 -0
- package/lib/agy/parser.d.ts +102 -0
- package/lib/agy/parser.js +492 -0
- package/lib/agy/persistent-transport.d.ts +101 -0
- package/lib/agy/persistent-transport.js +753 -0
- package/lib/agy/process.d.ts +144 -0
- package/lib/agy/process.js +452 -0
- package/lib/agy/redact.d.ts +3 -0
- package/lib/agy/redact.js +24 -0
- package/lib/agy/stream-protocol.d.ts +50 -0
- package/lib/agy/stream-protocol.js +156 -0
- package/lib/agy/windows-launcher.d.ts +21 -0
- package/lib/agy/windows-launcher.js +76 -0
- package/lib/diagnostics.d.ts +78 -0
- package/lib/diagnostics.js +162 -0
- package/lib/doctor.d.ts +138 -0
- package/lib/doctor.js +923 -0
- package/lib/dsh/context.d.ts +133 -0
- package/lib/dsh/context.js +352 -0
- package/lib/dsh-home.d.ts +6 -0
- package/lib/dsh-home.js +11 -0
- package/lib/index.d.ts +43 -0
- package/lib/index.js +256 -0
- package/lib/mcp/agy-registration.d.ts +70 -0
- package/lib/mcp/agy-registration.js +245 -0
- package/lib/mcp/bridge.d.ts +73 -0
- package/lib/mcp/bridge.js +295 -0
- package/lib/mcp/endpoint.d.ts +41 -0
- package/lib/mcp/endpoint.js +142 -0
- package/lib/mcp/protocol.d.ts +107 -0
- package/lib/mcp/protocol.js +197 -0
- package/lib/mcp/tool-projection.d.ts +64 -0
- package/lib/mcp/tool-projection.js +168 -0
- package/lib/provider/agy.d.ts +153 -0
- package/lib/provider/agy.js +2159 -0
- package/lib/provider/config.d.ts +226 -0
- package/lib/provider/config.js +294 -0
- package/lib/provider/error-codes.d.ts +22 -0
- package/lib/provider/error-codes.js +29 -0
- package/lib/provider/image-bridge.d.ts +60 -0
- package/lib/provider/image-bridge.js +290 -0
- package/lib/provider/mock.d.ts +15 -0
- package/lib/provider/mock.js +78 -0
- package/lib/provider/prompt-budget.d.ts +45 -0
- package/lib/provider/prompt-budget.js +596 -0
- package/lib/provider/serialize.d.ts +15 -0
- package/lib/provider/serialize.js +82 -0
- package/lib/provider/tool-protocol.d.ts +80 -0
- package/lib/provider/tool-protocol.js +848 -0
- package/lib/provider/tool-schema-file.d.ts +11 -0
- package/lib/provider/tool-schema-file.js +27 -0
- package/lib/session/file-store.d.ts +56 -0
- package/lib/session/file-store.js +186 -0
- package/lib/session/store.d.ts +49 -0
- package/lib/session/store.js +62 -0
- package/lib/web/router.d.ts +19 -0
- package/lib/web/router.js +906 -0
- package/package.json +112 -4
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { redactText } from './agy/redact.js';
|
|
7
|
+
import { getAgentPreset, listAgentPresets, readAgentPresetTemplate, } from './agent-presets.js';
|
|
8
|
+
import { diagnoseProvider, } from './diagnostics.js';
|
|
9
|
+
import { Config } from './provider/config.js';
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
function loadPackageMetadata() {
|
|
12
|
+
try {
|
|
13
|
+
return require('../package.json');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const packageMetadata = loadPackageMetadata();
|
|
20
|
+
const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
21
|
+
const AGENT_WRITE_TOOLS = new Set([
|
|
22
|
+
'multi_replace_file_content',
|
|
23
|
+
'replace_file_content',
|
|
24
|
+
'write_to_file',
|
|
25
|
+
]);
|
|
26
|
+
function isSafeProfileName(value) {
|
|
27
|
+
return PROFILE_NAME_PATTERN.test(value);
|
|
28
|
+
}
|
|
29
|
+
function resolveDshInvocation(value) {
|
|
30
|
+
if (process.platform !== 'win32' || !/\.cmd$/i.test(value)) {
|
|
31
|
+
if (/\.(?:c|m)?js$/i.test(value))
|
|
32
|
+
return { executable: process.execPath, prefixArgs: [value] };
|
|
33
|
+
return { executable: value, prefixArgs: [] };
|
|
34
|
+
}
|
|
35
|
+
if (/[\\/]/.test(value)) {
|
|
36
|
+
const entry = join(dirname(value), '..', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
|
|
37
|
+
if (existsSync(entry))
|
|
38
|
+
return { executable: process.execPath, prefixArgs: [entry] };
|
|
39
|
+
}
|
|
40
|
+
// npm's Windows .cmd shims cannot be spawned directly with shell=false.
|
|
41
|
+
// Keep shell=false and invoke the shim through the Windows command
|
|
42
|
+
// interpreter; profile names and DSH_HOME are passed as regular argv/env.
|
|
43
|
+
return {
|
|
44
|
+
executable: process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe',
|
|
45
|
+
prefixArgs: ['/d', '/s', '/c', 'call', value],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function killProcessTree(child) {
|
|
49
|
+
if (child.pid === undefined)
|
|
50
|
+
return;
|
|
51
|
+
if (process.platform === 'win32') {
|
|
52
|
+
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
|
|
53
|
+
shell: false,
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
stdio: 'ignore',
|
|
56
|
+
});
|
|
57
|
+
killer.once('error', () => child.kill());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
child.kill('SIGKILL');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function defaultRunCommand(executable, args, options = {}) {
|
|
68
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
69
|
+
const maxOutputBytes = 1024 * 1024;
|
|
70
|
+
return new Promise(resolve => {
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
let totalBytes = 0;
|
|
74
|
+
let timedOut = false;
|
|
75
|
+
let settled = false;
|
|
76
|
+
const child = spawn(executable, args, {
|
|
77
|
+
cwd: options.cwd,
|
|
78
|
+
env: options.env ?? process.env,
|
|
79
|
+
shell: false,
|
|
80
|
+
windowsHide: true,
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
});
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
timedOut = true;
|
|
85
|
+
killProcessTree(child);
|
|
86
|
+
}, timeoutMs);
|
|
87
|
+
const finish = (exitCode) => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve({ exitCode, stdout, stderr, timedOut });
|
|
93
|
+
};
|
|
94
|
+
child.stdout?.setEncoding('utf8');
|
|
95
|
+
child.stderr?.setEncoding('utf8');
|
|
96
|
+
let stderrBytes = 0;
|
|
97
|
+
child.stdout?.on('data', chunk => {
|
|
98
|
+
totalBytes += Buffer.byteLength(chunk, 'utf8');
|
|
99
|
+
if (totalBytes <= maxOutputBytes)
|
|
100
|
+
stdout += chunk;
|
|
101
|
+
});
|
|
102
|
+
child.stderr?.on('data', chunk => {
|
|
103
|
+
// Counted incrementally like stdout: re-measuring the whole accumulated
|
|
104
|
+
// string per chunk is quadratic on a chatty child.
|
|
105
|
+
stderrBytes += Buffer.byteLength(chunk, 'utf8');
|
|
106
|
+
if (stderrBytes <= maxOutputBytes)
|
|
107
|
+
stderr += chunk;
|
|
108
|
+
});
|
|
109
|
+
child.once('error', () => finish(null));
|
|
110
|
+
child.once('close', exitCode => finish(exitCode));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function resolveDshHome(customHome) {
|
|
114
|
+
const envHome = process.env.DSH_HOME?.trim();
|
|
115
|
+
if (customHome !== undefined && customHome.trim().length > 0)
|
|
116
|
+
return customHome.trim();
|
|
117
|
+
if (envHome !== undefined && envHome.length > 0)
|
|
118
|
+
return envHome;
|
|
119
|
+
return join(homedir(), '.dsh');
|
|
120
|
+
}
|
|
121
|
+
function stripDumpValue(value) {
|
|
122
|
+
const withoutComment = value.replace(/\s+#.*$/, '').trim();
|
|
123
|
+
if (withoutComment.length >= 2) {
|
|
124
|
+
const first = withoutComment[0];
|
|
125
|
+
const last = withoutComment.at(-1);
|
|
126
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
127
|
+
return withoutComment.slice(1, -1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return withoutComment;
|
|
131
|
+
}
|
|
132
|
+
/** Parse only the provider bundle section; this is intentionally not a general YAML parser. */
|
|
133
|
+
function parseDumpConfig(stdout) {
|
|
134
|
+
const values = {};
|
|
135
|
+
let recognized = false;
|
|
136
|
+
let malformed = false;
|
|
137
|
+
let inBundleSection = false;
|
|
138
|
+
const stack = [];
|
|
139
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
140
|
+
if (/^\s*#\s*==\s*dsh-agy-cli\b/i.test(line)
|
|
141
|
+
|| /^\s*-\s*id:\s*dsh-agy-cli\b/i.test(line)
|
|
142
|
+
|| /^\s*id:\s*dsh-agy-cli\b/i.test(line)) {
|
|
143
|
+
recognized = true;
|
|
144
|
+
inBundleSection = true;
|
|
145
|
+
stack.length = 0;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (inBundleSection && /^\s*#\s*==\s+/.test(line)) {
|
|
149
|
+
inBundleSection = false;
|
|
150
|
+
stack.length = 0;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!inBundleSection || line.trim().length === 0 || line.trim().startsWith('#'))
|
|
154
|
+
continue;
|
|
155
|
+
const match = /^(\s*)([A-Za-z][A-Za-z0-9_-]*):(?:\s*(.*))?$/.exec(line);
|
|
156
|
+
if (/^\s*-\s+/.test(line))
|
|
157
|
+
continue;
|
|
158
|
+
if (match === null || match[1] === undefined || match[2] === undefined) {
|
|
159
|
+
malformed = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const indent = match[1].replace(/\t/g, ' ').length;
|
|
163
|
+
while (stack.length > 0 && (stack.at(-1)?.indent ?? -1) >= indent)
|
|
164
|
+
stack.pop();
|
|
165
|
+
const path = [...stack.map(entry => entry.key), match[2]].join('.');
|
|
166
|
+
const rawValue = match[3]?.trim() ?? '';
|
|
167
|
+
values[path] = stripDumpValue(rawValue);
|
|
168
|
+
if (rawValue.length === 0)
|
|
169
|
+
stack.push({ indent, key: match[2] });
|
|
170
|
+
}
|
|
171
|
+
return { recognized, malformed, values };
|
|
172
|
+
}
|
|
173
|
+
function normalizeConfigValues(values) {
|
|
174
|
+
const normalized = { ...values };
|
|
175
|
+
for (const [key, value] of Object.entries(values)) {
|
|
176
|
+
if (key.startsWith('config.')) {
|
|
177
|
+
const shortKey = key.slice('config.'.length);
|
|
178
|
+
if (normalized[shortKey] === undefined)
|
|
179
|
+
normalized[shortKey] = value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
184
|
+
function valueOf(values, key) {
|
|
185
|
+
const value = values[key];
|
|
186
|
+
return value === undefined || value.length === 0 ? null : value;
|
|
187
|
+
}
|
|
188
|
+
function booleanValue(value) {
|
|
189
|
+
if (value === null)
|
|
190
|
+
return null;
|
|
191
|
+
if (value.toLowerCase() === 'true')
|
|
192
|
+
return true;
|
|
193
|
+
if (value.toLowerCase() === 'false')
|
|
194
|
+
return false;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function numberValue(value) {
|
|
198
|
+
if (value === null || !/^\d+$/.test(value))
|
|
199
|
+
return null;
|
|
200
|
+
const parsed = Number(value);
|
|
201
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
202
|
+
}
|
|
203
|
+
function listValue(value) {
|
|
204
|
+
if (value === null)
|
|
205
|
+
return [];
|
|
206
|
+
const normalized = value.trim().replace(/^\[/, '').replace(/\]$/, '').trim();
|
|
207
|
+
if (normalized.length === 0)
|
|
208
|
+
return [];
|
|
209
|
+
return normalized.split(',').map(item => stripDumpValue(item.trim())).filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
function workspaceStatus(value) {
|
|
212
|
+
if (value === null || value.trim().length === 0)
|
|
213
|
+
return value === null ? 'unknown' : 'missing';
|
|
214
|
+
try {
|
|
215
|
+
return statSync(value).isDirectory() ? 'configured' : 'not-directory';
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return 'missing';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function routeFromDump(values, purpose) {
|
|
222
|
+
const prefix = `purposeRoutes.${purpose}.`;
|
|
223
|
+
const configured = Object.keys(values).some(key => key.startsWith(prefix));
|
|
224
|
+
if (!configured)
|
|
225
|
+
return null;
|
|
226
|
+
const model = valueOf(values, `${prefix}model`);
|
|
227
|
+
const agent = valueOf(values, `${prefix}agent`);
|
|
228
|
+
const reasoningEffort = valueOf(values, `${prefix}reasoningEffort`);
|
|
229
|
+
return {
|
|
230
|
+
configured: true,
|
|
231
|
+
complete: model !== null && agent !== null && reasoningEffort !== null,
|
|
232
|
+
model,
|
|
233
|
+
agent,
|
|
234
|
+
reasoningEffort,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function emptyEffective(dumpStatus) {
|
|
238
|
+
return {
|
|
239
|
+
dumpStatus,
|
|
240
|
+
provider: null,
|
|
241
|
+
model: null,
|
|
242
|
+
agent: null,
|
|
243
|
+
agentPreset: null,
|
|
244
|
+
sessionMode: null,
|
|
245
|
+
retryPolicy: null,
|
|
246
|
+
purposeRoutes: { compaction: null, sessionTitle: null },
|
|
247
|
+
workspaceRootStatus: 'unknown',
|
|
248
|
+
workspaceSource: 'unknown',
|
|
249
|
+
visibleModels: { raw: null, count: null, filtered: false },
|
|
250
|
+
modelEffortSplit: { baseModel: null, suffixDetected: false, normalized: false },
|
|
251
|
+
imageInput: null,
|
|
252
|
+
modelCapability: { inputModalities: ['text'], imageBridge: 'unknown' },
|
|
253
|
+
agentCapability: {
|
|
254
|
+
knownPreset: null,
|
|
255
|
+
frontmatterValid: null,
|
|
256
|
+
tools: [],
|
|
257
|
+
viewFile: null,
|
|
258
|
+
writeTools: null,
|
|
259
|
+
commandExecutionPolicy: null,
|
|
260
|
+
mcpServersEmpty: null,
|
|
261
|
+
subagent: null,
|
|
262
|
+
},
|
|
263
|
+
dshContext: {
|
|
264
|
+
source: 'profile-dump',
|
|
265
|
+
session: { state: 'not-probed', required: false },
|
|
266
|
+
workspace: { state: 'not-probed', configured: false },
|
|
267
|
+
permission: { state: 'not-probed', preset: null },
|
|
268
|
+
approval: { state: 'not-probed', policy: null },
|
|
269
|
+
services: {
|
|
270
|
+
sessions: 'not-probed',
|
|
271
|
+
workspaceRegistry: 'not-probed',
|
|
272
|
+
sandboxPolicy: 'not-probed',
|
|
273
|
+
permissionPresets: 'not-probed',
|
|
274
|
+
approval: 'not-probed',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
bridgeCapability: {
|
|
278
|
+
owner: 'unknown',
|
|
279
|
+
enabled: null,
|
|
280
|
+
agent: null,
|
|
281
|
+
internalTools: 'unknown',
|
|
282
|
+
schemaCapability: 'unknown',
|
|
283
|
+
legacyConfig: { agyOwned: false, workspaceRootConfigured: false },
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function bridgeCapabilityFromConfig(toolPolicy, bundleEnabled, configuredAgent, agentCapability, workspaceRootStatus) {
|
|
288
|
+
const workspaceRootConfigured = workspaceRootStatus === 'configured';
|
|
289
|
+
if (toolPolicy === 'dsh-owned') {
|
|
290
|
+
return {
|
|
291
|
+
owner: 'dsh',
|
|
292
|
+
enabled: bundleEnabled,
|
|
293
|
+
agent: 'dsh-agy-tool-free',
|
|
294
|
+
internalTools: 'empty',
|
|
295
|
+
schemaCapability: 'prompt-contract-v1',
|
|
296
|
+
legacyConfig: { agyOwned: false, workspaceRootConfigured },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (toolPolicy === 'agy-owned') {
|
|
300
|
+
const internalTools = agentCapability.frontmatterValid === true
|
|
301
|
+
&& agentCapability.commandExecutionPolicy === 'off'
|
|
302
|
+
&& agentCapability.mcpServersEmpty === true
|
|
303
|
+
&& agentCapability.subagent === false
|
|
304
|
+
? 'empty'
|
|
305
|
+
: agentCapability.frontmatterValid === false ? 'present' : 'unknown';
|
|
306
|
+
return {
|
|
307
|
+
owner: 'agy',
|
|
308
|
+
enabled: bundleEnabled,
|
|
309
|
+
agent: configuredAgent,
|
|
310
|
+
internalTools,
|
|
311
|
+
schemaCapability: 'agy-owned',
|
|
312
|
+
legacyConfig: { agyOwned: true, workspaceRootConfigured },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (toolPolicy === 'reject') {
|
|
316
|
+
return {
|
|
317
|
+
owner: 'none',
|
|
318
|
+
enabled: bundleEnabled,
|
|
319
|
+
agent: configuredAgent,
|
|
320
|
+
internalTools: 'unknown',
|
|
321
|
+
schemaCapability: 'unsupported',
|
|
322
|
+
legacyConfig: { agyOwned: false, workspaceRootConfigured },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
owner: 'unknown',
|
|
327
|
+
enabled: bundleEnabled,
|
|
328
|
+
agent: configuredAgent,
|
|
329
|
+
internalTools: 'unknown',
|
|
330
|
+
schemaCapability: 'unknown',
|
|
331
|
+
legacyConfig: { agyOwned: false, workspaceRootConfigured },
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function effectiveFromDump(parsed, dumpStatus) {
|
|
335
|
+
const values = normalizeConfigValues(parsed.values);
|
|
336
|
+
const imageInput = valueOf(values, 'imageInput');
|
|
337
|
+
const retryMax = valueOf(values, 'retryPolicy.maxRetries');
|
|
338
|
+
const retryCodes = valueOf(values, 'retryPolicy.retryableCodes');
|
|
339
|
+
const toolPolicy = valueOf(values, 'toolPolicy');
|
|
340
|
+
const bundleEnabled = booleanValue(valueOf(values, 'enabled'));
|
|
341
|
+
const workspaceRootStatus = workspaceStatus(valueOf(values, 'workspaceRoot'));
|
|
342
|
+
const workspaceSource = toolPolicy === 'dsh-owned' ? 'dsh-session-cwd' : workspaceRootStatus === 'configured' ? 'config-legacy' : 'unknown';
|
|
343
|
+
const rawVisible = valueOf(values, 'visibleModels');
|
|
344
|
+
const visibleCount = rawVisible === null ? null : (() => { try {
|
|
345
|
+
const v = JSON.parse(rawVisible);
|
|
346
|
+
return Array.isArray(v) ? v.length : null;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return rawVisible.length > 0 ? 1 : 0;
|
|
350
|
+
} })();
|
|
351
|
+
const rawModel = valueOf(values, 'model');
|
|
352
|
+
const suffixDetected = rawModel !== null && /-(?:low|medium|high)$/i.test(rawModel);
|
|
353
|
+
const baseModel = rawModel !== null ? rawModel.replace(/-(?:low|medium|high)$/i, '') : null;
|
|
354
|
+
const configuredAgent = valueOf(values, 'agent');
|
|
355
|
+
return {
|
|
356
|
+
dumpStatus,
|
|
357
|
+
provider: valueOf(values, 'provider'),
|
|
358
|
+
model: valueOf(values, 'model'),
|
|
359
|
+
agent: valueOf(values, 'agent'),
|
|
360
|
+
agentPreset: valueOf(values, 'agentPreset'),
|
|
361
|
+
sessionMode: valueOf(values, 'sessionMode'),
|
|
362
|
+
retryPolicy: retryMax === null && retryCodes === null
|
|
363
|
+
? null
|
|
364
|
+
: { maxRetries: numberValue(retryMax), retryableCodes: listValue(retryCodes) },
|
|
365
|
+
purposeRoutes: {
|
|
366
|
+
compaction: routeFromDump(values, 'compaction'),
|
|
367
|
+
sessionTitle: routeFromDump(values, 'sessionTitle'),
|
|
368
|
+
},
|
|
369
|
+
workspaceRootStatus,
|
|
370
|
+
workspaceSource,
|
|
371
|
+
visibleModels: { raw: rawVisible, count: visibleCount, filtered: visibleCount !== null && visibleCount > 0 },
|
|
372
|
+
modelEffortSplit: { baseModel, suffixDetected, normalized: suffixDetected ? baseModel !== rawModel : false },
|
|
373
|
+
imageInput,
|
|
374
|
+
modelCapability: {
|
|
375
|
+
inputModalities: imageInput === 'experimental' ? ['text', 'image'] : ['text'],
|
|
376
|
+
imageBridge: imageInput === 'off' ? 'off' : imageInput === 'experimental' ? 'experimental' : 'unknown',
|
|
377
|
+
},
|
|
378
|
+
agentCapability: emptyEffective(dumpStatus).agentCapability,
|
|
379
|
+
dshContext: {
|
|
380
|
+
source: 'profile-dump',
|
|
381
|
+
session: { state: 'not-probed', required: toolPolicy === 'dsh-owned' },
|
|
382
|
+
workspace: { state: 'not-probed', configured: workspaceRootStatus === 'configured' },
|
|
383
|
+
permission: { state: 'not-probed', preset: null },
|
|
384
|
+
approval: { state: 'not-probed', policy: null },
|
|
385
|
+
services: {
|
|
386
|
+
sessions: 'not-probed',
|
|
387
|
+
workspaceRegistry: 'not-probed',
|
|
388
|
+
sandboxPolicy: 'not-probed',
|
|
389
|
+
permissionPresets: 'not-probed',
|
|
390
|
+
approval: 'not-probed',
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
bridgeCapability: bridgeCapabilityFromConfig(toolPolicy, bundleEnabled, configuredAgent, emptyEffective(dumpStatus).agentCapability, workspaceRootStatus),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function parseAgentFrontmatter(source) {
|
|
397
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source);
|
|
398
|
+
if (match === null || match[1] === undefined) {
|
|
399
|
+
return { name: null, tools: null, commandExecutionPolicy: null, mcpServersEmpty: null, subagent: null };
|
|
400
|
+
}
|
|
401
|
+
let name = null;
|
|
402
|
+
let tools = null;
|
|
403
|
+
let commandExecutionPolicy = null;
|
|
404
|
+
let mcpServersEmpty = null;
|
|
405
|
+
let subagent = null;
|
|
406
|
+
let collectingTools = false;
|
|
407
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
408
|
+
const listItem = /^\s*-\s*(.+?)\s*$/.exec(line);
|
|
409
|
+
if (collectingTools && listItem?.[1] !== undefined) {
|
|
410
|
+
tools ??= [];
|
|
411
|
+
tools.push(stripDumpValue(listItem[1]));
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const field = /^([A-Za-z][A-Za-z0-9_-]*):(?:\s*(.*))?$/.exec(line);
|
|
415
|
+
if (field === null || field[1] === undefined)
|
|
416
|
+
continue;
|
|
417
|
+
collectingTools = field[1] === 'tools' && (field[2] ?? '').trim().length === 0;
|
|
418
|
+
const rawValue = stripDumpValue(field[2]?.trim() ?? '');
|
|
419
|
+
if (field[1] === 'name')
|
|
420
|
+
name = rawValue || null;
|
|
421
|
+
if (field[1] === 'tools' && !collectingTools)
|
|
422
|
+
tools = [...listValue(rawValue)];
|
|
423
|
+
if (field[1] === 'commandExecutionPolicy')
|
|
424
|
+
commandExecutionPolicy = rawValue || null;
|
|
425
|
+
if (field[1] === 'mcpServers')
|
|
426
|
+
mcpServersEmpty = rawValue === '[]';
|
|
427
|
+
if (field[1] === 'subagent')
|
|
428
|
+
subagent = booleanValue(rawValue);
|
|
429
|
+
}
|
|
430
|
+
return { name, tools, commandExecutionPolicy, mcpServersEmpty, subagent };
|
|
431
|
+
}
|
|
432
|
+
function presetForAgent(agent, agentPreset) {
|
|
433
|
+
return getAgentPreset(agentPreset) ?? listAgentPresets().find(preset => preset.agentName === agent);
|
|
434
|
+
}
|
|
435
|
+
async function inspectAgent(agent, agentPreset) {
|
|
436
|
+
const preset = presetForAgent(agent, agentPreset);
|
|
437
|
+
if (preset === undefined) {
|
|
438
|
+
return {
|
|
439
|
+
capability: {
|
|
440
|
+
knownPreset: agent === null ? null : false,
|
|
441
|
+
frontmatterValid: agent === null ? null : null,
|
|
442
|
+
tools: [],
|
|
443
|
+
viewFile: agent === null ? null : false,
|
|
444
|
+
writeTools: agent === null ? null : false,
|
|
445
|
+
commandExecutionPolicy: null,
|
|
446
|
+
mcpServersEmpty: null,
|
|
447
|
+
subagent: null,
|
|
448
|
+
},
|
|
449
|
+
...(agent === null ? {} : {
|
|
450
|
+
suggestion: `Install or inspect the selected Agent explicitly; bundled options: ${listAgentPresets().map(item => item.id).join(', ')}`,
|
|
451
|
+
}),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const frontmatter = parseAgentFrontmatter(await readAgentPresetTemplate(preset));
|
|
456
|
+
const actualTools = frontmatter.tools ?? [];
|
|
457
|
+
const toolsMatch = actualTools.length === preset.tools.length
|
|
458
|
+
&& actualTools.every((tool, index) => tool === preset.tools[index]);
|
|
459
|
+
const valid = frontmatter.name === preset.agentName
|
|
460
|
+
&& toolsMatch
|
|
461
|
+
&& frontmatter.commandExecutionPolicy === 'off'
|
|
462
|
+
&& frontmatter.mcpServersEmpty === true
|
|
463
|
+
&& frontmatter.subagent === false;
|
|
464
|
+
const capability = {
|
|
465
|
+
knownPreset: true,
|
|
466
|
+
frontmatterValid: valid,
|
|
467
|
+
tools: actualTools,
|
|
468
|
+
viewFile: actualTools.includes('view_file'),
|
|
469
|
+
writeTools: actualTools.some(tool => AGENT_WRITE_TOOLS.has(tool)),
|
|
470
|
+
commandExecutionPolicy: frontmatter.commandExecutionPolicy,
|
|
471
|
+
mcpServersEmpty: frontmatter.mcpServersEmpty,
|
|
472
|
+
subagent: frontmatter.subagent,
|
|
473
|
+
};
|
|
474
|
+
return valid
|
|
475
|
+
? { capability }
|
|
476
|
+
: {
|
|
477
|
+
capability,
|
|
478
|
+
issue: {
|
|
479
|
+
component: 'profile',
|
|
480
|
+
code: 'PROFILE_AGENT_FRONTMATTER_INVALID',
|
|
481
|
+
message: `Bundled Agent ${preset.agentName} does not match its declared tool and execution policy`,
|
|
482
|
+
},
|
|
483
|
+
suggestion: `Reinstall the bundled Agent with: npx dsh-agy-cli agents install ${preset.id} --apply`,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return {
|
|
488
|
+
capability: {
|
|
489
|
+
knownPreset: true,
|
|
490
|
+
frontmatterValid: false,
|
|
491
|
+
tools: [],
|
|
492
|
+
viewFile: preset.tools.includes('view_file'),
|
|
493
|
+
writeTools: preset.tools.some(tool => AGENT_WRITE_TOOLS.has(tool)),
|
|
494
|
+
commandExecutionPolicy: null,
|
|
495
|
+
mcpServersEmpty: null,
|
|
496
|
+
subagent: null,
|
|
497
|
+
},
|
|
498
|
+
issue: {
|
|
499
|
+
component: 'profile',
|
|
500
|
+
code: 'PROFILE_AGENT_FRONTMATTER_UNREADABLE',
|
|
501
|
+
message: `Could not read the bundled Agent template for ${preset.agentName}`,
|
|
502
|
+
},
|
|
503
|
+
suggestion: `Reinstall the bundled Agent with: npx dsh-agy-cli agents install ${preset.id} --apply`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
export async function diagnoseProfile(options) {
|
|
508
|
+
const profileName = options.profileName.trim();
|
|
509
|
+
const dshHome = resolveDshHome(options.dshHome);
|
|
510
|
+
const dshHomePresent = existsSync(dshHome);
|
|
511
|
+
const validProfileName = isSafeProfileName(profileName);
|
|
512
|
+
const profileDir = validProfileName ? join(dshHome, 'profiles', profileName) : '';
|
|
513
|
+
const profilePresent = dshHomePresent && existsSync(profileDir);
|
|
514
|
+
const issues = [];
|
|
515
|
+
if (!validProfileName) {
|
|
516
|
+
return {
|
|
517
|
+
profileSchemaVersion: 4,
|
|
518
|
+
name: '<invalid>',
|
|
519
|
+
dshHomePresent,
|
|
520
|
+
profilePresent: false,
|
|
521
|
+
packageInstalled: false,
|
|
522
|
+
bundleDeclared: false,
|
|
523
|
+
bundleEnabled: null,
|
|
524
|
+
toolPolicy: null,
|
|
525
|
+
effectiveProvider: null,
|
|
526
|
+
effectiveModel: null,
|
|
527
|
+
effective: emptyEffective('not-run'),
|
|
528
|
+
repairSuggestions: [],
|
|
529
|
+
issues: [{
|
|
530
|
+
component: 'profile',
|
|
531
|
+
code: 'PROFILE_NAME_INVALID',
|
|
532
|
+
message: 'Profile name must contain 1-64 letters, numbers, dots, underscores, or hyphens and must not contain path separators',
|
|
533
|
+
}],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
let packageInstalled = false;
|
|
537
|
+
let bundleDeclared = false;
|
|
538
|
+
let bundleEnabled = null;
|
|
539
|
+
let toolPolicy = null;
|
|
540
|
+
let effectiveProvider = null;
|
|
541
|
+
let effectiveModel = null;
|
|
542
|
+
let effective = emptyEffective('not-run');
|
|
543
|
+
const repairSuggestions = [];
|
|
544
|
+
const addSuggestion = (suggestion) => {
|
|
545
|
+
if (!repairSuggestions.includes(suggestion))
|
|
546
|
+
repairSuggestions.push(suggestion);
|
|
547
|
+
};
|
|
548
|
+
if (!dshHomePresent) {
|
|
549
|
+
issues.push({
|
|
550
|
+
component: 'profile',
|
|
551
|
+
code: 'DSH_HOME_NOT_FOUND',
|
|
552
|
+
message: `DSH home directory not found: run DSH once or set DSH_HOME`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
else if (!profilePresent) {
|
|
556
|
+
issues.push({
|
|
557
|
+
component: 'profile',
|
|
558
|
+
code: 'PROFILE_NOT_FOUND',
|
|
559
|
+
message: `Profile "${profileName}" not found. Create with: npx @deepseek-ai/dsh plugin --profile ${profileName} add dsh-agy-cli`,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
const profilePackageJsonPath = join(profileDir, 'package.json');
|
|
564
|
+
if (existsSync(profilePackageJsonPath)) {
|
|
565
|
+
try {
|
|
566
|
+
const pkg = JSON.parse(readFileSync(profilePackageJsonPath, 'utf8'));
|
|
567
|
+
const deps = pkg.dependencies ?? {};
|
|
568
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
569
|
+
packageInstalled = Boolean(deps['dsh-agy-cli'] || devDeps['dsh-agy-cli']);
|
|
570
|
+
const bundles = pkg.dsh?.profile?.bundles;
|
|
571
|
+
bundleDeclared = Array.isArray(bundles) && bundles.includes('dsh-agy-cli');
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
issues.push({
|
|
575
|
+
component: 'profile',
|
|
576
|
+
code: 'PROFILE_PACKAGE_JSON_INVALID',
|
|
577
|
+
message: `Could not parse package.json for profile "${profileName}"`,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (!packageInstalled) {
|
|
582
|
+
issues.push({
|
|
583
|
+
component: 'profile',
|
|
584
|
+
code: 'PROFILE_PACKAGE_MISSING',
|
|
585
|
+
message: `Package dsh-agy-cli is not installed in profile "${profileName}". Run: npx @deepseek-ai/dsh plugin --profile ${profileName} add dsh-agy-cli`,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
else if (!bundleDeclared) {
|
|
589
|
+
issues.push({
|
|
590
|
+
component: 'profile',
|
|
591
|
+
code: 'PROFILE_BUNDLE_MISSING',
|
|
592
|
+
message: `Bundle dsh-agy-cli is not declared in dsh.profile.bundles for profile "${profileName}". Re-run: npx @deepseek-ai/dsh plugin --profile ${profileName} add dsh-agy-cli`,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
// Inspect effective dump-config. Failure is part of the diagnostic result;
|
|
596
|
+
// it must not silently turn into a false "unknown but healthy" profile.
|
|
597
|
+
const runCmd = options.runCommand ?? defaultRunCommand;
|
|
598
|
+
const customBin = options.dshBin?.trim() || process.env.DSH_BIN?.trim();
|
|
599
|
+
const dshCandidates = [
|
|
600
|
+
...(customBin ? [customBin] : []),
|
|
601
|
+
join(profileDir, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'),
|
|
602
|
+
join(dirname(profileDir), '..', 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'),
|
|
603
|
+
];
|
|
604
|
+
const pathDshCommand = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
|
|
605
|
+
const dshEntry = options.runCommand !== undefined
|
|
606
|
+
? (customBin ?? pathDshCommand)
|
|
607
|
+
: dshCandidates.find(p => existsSync(p))
|
|
608
|
+
?? pathDshCommand;
|
|
609
|
+
// dshEntry always resolves (the PATH fallback is unconditional), so there
|
|
610
|
+
// is no "missing" branch to guard: a dump is always attempted.
|
|
611
|
+
let dumpStatus = 'not-run';
|
|
612
|
+
{
|
|
613
|
+
try {
|
|
614
|
+
const invocation = resolveDshInvocation(dshEntry);
|
|
615
|
+
const args = [...invocation.prefixArgs, '--profile', profileName, '--dump-config'];
|
|
616
|
+
const dumpResult = await runCmd(invocation.executable, args, {
|
|
617
|
+
timeoutMs: options.timeoutMs ?? 10_000,
|
|
618
|
+
cwd: profileDir,
|
|
619
|
+
env: { ...process.env, DSH_HOME: dshHome },
|
|
620
|
+
});
|
|
621
|
+
if (dumpResult.timedOut) {
|
|
622
|
+
dumpStatus = 'timeout';
|
|
623
|
+
issues.push({
|
|
624
|
+
component: 'profile',
|
|
625
|
+
code: 'PROFILE_DUMP_TIMEOUT',
|
|
626
|
+
message: `DSH --dump-config timed out for profile "${profileName}"`,
|
|
627
|
+
});
|
|
628
|
+
addSuggestion(`Retry the read-only check: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
|
|
629
|
+
}
|
|
630
|
+
else if (dumpResult.exitCode !== 0) {
|
|
631
|
+
dumpStatus = 'nonzero';
|
|
632
|
+
issues.push({
|
|
633
|
+
component: 'profile',
|
|
634
|
+
code: 'PROFILE_DUMP_NONZERO',
|
|
635
|
+
message: `DSH --dump-config exited unsuccessfully for profile "${profileName}"`,
|
|
636
|
+
});
|
|
637
|
+
addSuggestion(`Run the read-only check directly: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
const parsed = parseDumpConfig(dumpResult.stdout);
|
|
641
|
+
if (!parsed.recognized || parsed.malformed) {
|
|
642
|
+
dumpStatus = 'parse-error';
|
|
643
|
+
issues.push({
|
|
644
|
+
component: 'profile',
|
|
645
|
+
code: 'PROFILE_DUMP_PARSE_FAILED',
|
|
646
|
+
message: `DSH --dump-config did not contain a valid dsh-agy-cli section for profile "${profileName}"`,
|
|
647
|
+
});
|
|
648
|
+
addSuggestion(`Inspect the read-only dump format: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
dumpStatus = 'ok';
|
|
652
|
+
effective = effectiveFromDump(parsed, dumpStatus);
|
|
653
|
+
const configValues = normalizeConfigValues(parsed.values);
|
|
654
|
+
bundleEnabled = booleanValue(valueOf(configValues, 'enabled'));
|
|
655
|
+
toolPolicy = valueOf(configValues, 'toolPolicy');
|
|
656
|
+
effectiveProvider = effective.provider;
|
|
657
|
+
effectiveModel = effective.model;
|
|
658
|
+
const agentInspection = await inspectAgent(effective.agent, effective.agentPreset);
|
|
659
|
+
effective = {
|
|
660
|
+
...effective,
|
|
661
|
+
agentCapability: agentInspection.capability,
|
|
662
|
+
bridgeCapability: bridgeCapabilityFromConfig(toolPolicy, bundleEnabled, effective.agent, agentInspection.capability, effective.workspaceRootStatus),
|
|
663
|
+
};
|
|
664
|
+
if (agentInspection.issue !== undefined)
|
|
665
|
+
issues.push(agentInspection.issue);
|
|
666
|
+
if (agentInspection.suggestion !== undefined)
|
|
667
|
+
addSuggestion(agentInspection.suggestion);
|
|
668
|
+
if (toolPolicy === 'agy-owned') {
|
|
669
|
+
issues.push({
|
|
670
|
+
component: 'profile',
|
|
671
|
+
code: 'PROFILE_TOOL_POLICY_DEPRECATED',
|
|
672
|
+
message: 'toolPolicy "agy-owned" is a legacy compatibility mode; DSH-owned execution is recommended and the profile was not changed',
|
|
673
|
+
});
|
|
674
|
+
addSuggestion('Set toolPolicy: dsh-owned after confirming the DSH permission and approval services are available');
|
|
675
|
+
}
|
|
676
|
+
if (toolPolicy === 'dsh-owned' && effective.bridgeCapability.internalTools === 'present') {
|
|
677
|
+
issues.push({
|
|
678
|
+
component: 'profile',
|
|
679
|
+
code: 'DSH_BRIDGE_AGENT_INTERNAL_TOOLS',
|
|
680
|
+
message: 'The DSH-owned bridge Agent exposes internal AGY tools; the bridge requires the bundled tool-free Agent',
|
|
681
|
+
});
|
|
682
|
+
addSuggestion('Use the bundled dsh-agy-tool-free Agent for toolPolicy: dsh-owned');
|
|
683
|
+
}
|
|
684
|
+
if (toolPolicy === 'reject' && bundleEnabled === true) {
|
|
685
|
+
issues.push({
|
|
686
|
+
component: 'profile',
|
|
687
|
+
code: 'DSH_SCHEMA_UNSUPPORTED',
|
|
688
|
+
message: 'The active profile rejects DSH tool schemas; tool bridge capability is unavailable until toolPolicy is explicitly selected',
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
// 0.9.0: dsh-owned uses DSH session cwd, no workspaceRoot required; only warn for legacy agy-owned
|
|
692
|
+
if (toolPolicy === 'dsh-owned' && effective.workspaceRootStatus === 'configured') {
|
|
693
|
+
issues.push({
|
|
694
|
+
component: 'profile',
|
|
695
|
+
code: 'DEPRECATED_WORKSPACE_ROOT',
|
|
696
|
+
message: 'workspaceRoot is deprecated for toolPolicy dsh-owned; DSH session workspace is used automatically',
|
|
697
|
+
});
|
|
698
|
+
addSuggestion('Remove workspaceRoot from profile; open the project folder in DSH and rely on session workspace');
|
|
699
|
+
}
|
|
700
|
+
if (effective.modelEffortSplit.suffixDetected) {
|
|
701
|
+
issues.push({
|
|
702
|
+
component: 'profile',
|
|
703
|
+
code: 'DEPRECATED_MODEL_EFFORT_SUFFIX',
|
|
704
|
+
message: `Model id "${effective.model}" contains -low/-medium/-high suffix; use base model "${effective.modelEffortSplit.baseModel}" and select reasoningEffort separately`,
|
|
705
|
+
});
|
|
706
|
+
addSuggestion('Set model to ' + effective.modelEffortSplit.baseModel + ' and choose reasoningEffort low/medium/high via DSH');
|
|
707
|
+
}
|
|
708
|
+
if (effective.agentCapability.writeTools === true
|
|
709
|
+
&& effective.workspaceRootStatus !== 'configured'
|
|
710
|
+
&& toolPolicy !== 'dsh-owned') {
|
|
711
|
+
issues.push({
|
|
712
|
+
component: 'profile',
|
|
713
|
+
code: 'PROFILE_WORKSPACE_REQUIRED',
|
|
714
|
+
message: 'The selected Agent can write files but no existing workspace directory is configured',
|
|
715
|
+
});
|
|
716
|
+
addSuggestion('Set workspaceRoot to an existing project directory before enabling workspace-write');
|
|
717
|
+
}
|
|
718
|
+
for (const purpose of ['compaction', 'sessionTitle']) {
|
|
719
|
+
const route = effective.purposeRoutes[purpose];
|
|
720
|
+
if (route?.complete === false) {
|
|
721
|
+
issues.push({
|
|
722
|
+
component: 'profile',
|
|
723
|
+
code: 'PROFILE_PURPOSE_ROUTE_INCOMPLETE',
|
|
724
|
+
message: `Purpose route "${purpose}" is missing model, agent, or reasoningEffort`,
|
|
725
|
+
});
|
|
726
|
+
addSuggestion(`Complete or remove purposeRoutes.${purpose} in the profile configuration`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (effective.retryPolicy !== null
|
|
730
|
+
&& (effective.retryPolicy.maxRetries === null || effective.retryPolicy.maxRetries > 5)) {
|
|
731
|
+
issues.push({
|
|
732
|
+
component: 'profile',
|
|
733
|
+
code: 'PROFILE_RETRY_POLICY_INVALID',
|
|
734
|
+
message: 'retryPolicy.maxRetries must be an integer from 0 through 5',
|
|
735
|
+
});
|
|
736
|
+
addSuggestion('Set retryPolicy.maxRetries to an integer from 0 through 5 in the profile configuration');
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
dumpStatus = 'unavailable';
|
|
743
|
+
issues.push({
|
|
744
|
+
component: 'profile',
|
|
745
|
+
code: 'PROFILE_DUMP_UNAVAILABLE',
|
|
746
|
+
message: `Could not execute DSH --dump-config for profile "${profileName}"`,
|
|
747
|
+
});
|
|
748
|
+
addSuggestion(`Verify the DSH executable and retry: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (dumpStatus !== 'ok')
|
|
752
|
+
effective = emptyEffective(dumpStatus);
|
|
753
|
+
// Check bundleEnabled and toolPolicy warnings
|
|
754
|
+
if (bundleEnabled === false) {
|
|
755
|
+
issues.push({
|
|
756
|
+
component: 'profile',
|
|
757
|
+
code: 'PROFILE_BUNDLE_DISABLED',
|
|
758
|
+
message: `Bundle dsh-agy-cli is disabled in profile "${profileName}". Enable in cordis.patch.yml or profile config`,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (toolPolicy === 'reject' && profileName === 'web') {
|
|
762
|
+
issues.push({
|
|
763
|
+
component: 'profile',
|
|
764
|
+
code: 'PROFILE_TOOL_POLICY_REJECT',
|
|
765
|
+
message: `Profile "${profileName}" carries tool schemas by default, but toolPolicy is "reject". Set toolPolicy: dsh-owned to let DSH execute tools, or agy-owned for legacy AGY-owned execution`,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
profileSchemaVersion: 4,
|
|
771
|
+
name: profileName,
|
|
772
|
+
dshHomePresent,
|
|
773
|
+
profilePresent,
|
|
774
|
+
packageInstalled,
|
|
775
|
+
bundleDeclared,
|
|
776
|
+
bundleEnabled,
|
|
777
|
+
toolPolicy,
|
|
778
|
+
effectiveProvider,
|
|
779
|
+
effectiveModel,
|
|
780
|
+
effective,
|
|
781
|
+
repairSuggestions,
|
|
782
|
+
issues,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
export async function runDoctor(options = {}) {
|
|
786
|
+
const providerResult = await diagnoseProvider(options);
|
|
787
|
+
let profileResult;
|
|
788
|
+
const profileName = options.profile?.trim();
|
|
789
|
+
if (profileName !== undefined && profileName.length > 0) {
|
|
790
|
+
profileResult = await diagnoseProfile({
|
|
791
|
+
profileName,
|
|
792
|
+
...(options.dshHome === undefined ? {} : { dshHome: options.dshHome }),
|
|
793
|
+
...(options.dshBin === undefined ? {} : { dshBin: options.dshBin }),
|
|
794
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
795
|
+
...(options.runProfileCommand === undefined ? {} : { runCommand: options.runProfileCommand }),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
const allErrors = [...providerResult.errors];
|
|
799
|
+
if (profileResult !== undefined) {
|
|
800
|
+
allErrors.push(...profileResult.issues);
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
...providerResult,
|
|
804
|
+
ok: allErrors.length === 0,
|
|
805
|
+
...(profileResult === undefined ? {} : { profile: profileResult }),
|
|
806
|
+
errors: allErrors,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
export function formatDoctorHuman(result) {
|
|
810
|
+
const lines = [];
|
|
811
|
+
lines.push(`dsh-agy-cli doctor (schema v${result.schemaVersion})`);
|
|
812
|
+
lines.push(`status: ${result.ok ? 'PASS' : 'FAIL'}; quotaUsed: ${result.quotaUsed}`);
|
|
813
|
+
lines.push(`plugin: ${result.plugin.name}@${result.plugin.version ?? 'unknown'}; enabled: ${result.plugin.enabled}`);
|
|
814
|
+
lines.push(`node: ${result.node.version} (${result.node.supported ? 'supported' : 'unsupported'})`);
|
|
815
|
+
lines.push(`dsh: cli=${result.dsh.cliVersion ?? 'unknown'}; llm=${result.dsh.llmContractVersion ?? 'unknown'}; bundle=${result.dsh.bundlePatchPresent ? 'present' : 'missing'}`);
|
|
816
|
+
if (result.profile !== undefined) {
|
|
817
|
+
const p = result.profile;
|
|
818
|
+
lines.push(`profile [${p.name}]: exists=${p.profilePresent}; packageInstalled=${p.packageInstalled}; bundleDeclared=${p.bundleDeclared}; enabled=${p.bundleEnabled ?? 'unknown'}; toolPolicy=${p.toolPolicy ?? 'unknown'}`);
|
|
819
|
+
lines.push(`effective: dump=${p.effective.dumpStatus}; provider=${p.effective.provider ?? 'unknown'}; model=${p.effective.model ?? 'unknown'}; agent=${p.effective.agent ?? 'unknown'}; sessionMode=${p.effective.sessionMode ?? 'unknown'}; imageInput=${p.effective.imageInput ?? 'unknown'}; inputModalities=${p.effective.modelCapability.inputModalities.join(',')}`);
|
|
820
|
+
lines.push(`agentCapability: preset=${p.effective.agentCapability.knownPreset ?? 'unknown'}; frontmatter=${p.effective.agentCapability.frontmatterValid ?? 'unknown'}; view_file=${p.effective.agentCapability.viewFile ?? 'unknown'}; writeTools=${p.effective.agentCapability.writeTools ?? 'unknown'}; workspace=${p.effective.workspaceRootStatus}`);
|
|
821
|
+
lines.push(`dshContext: session=${p.effective.dshContext.session.state}; workspace=${p.effective.dshContext.workspace.configured ? 'configured' : 'not-configured'}; permission=${p.effective.dshContext.permission.state}; approval=${p.effective.dshContext.approval.state}`);
|
|
822
|
+
lines.push(`bridge: owner=${p.effective.bridgeCapability.owner}; agent=${p.effective.bridgeCapability.agent ?? 'unknown'}; schema=${p.effective.bridgeCapability.schemaCapability}; internalTools=${p.effective.bridgeCapability.internalTools}`);
|
|
823
|
+
if (p.repairSuggestions.length > 0) {
|
|
824
|
+
lines.push('repair suggestions:');
|
|
825
|
+
for (const suggestion of p.repairSuggestions)
|
|
826
|
+
lines.push(`- ${suggestion}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
lines.push(`provider: ${result.configuration.provider}; agent=${result.configuration.agent}; defaultModel=${result.configuration.defaultModel}; toolPolicy=${result.configuration.toolPolicy}; transport=${result.configuration.transport ?? 'one-shot'}`);
|
|
830
|
+
lines.push(`transport: persistentIdleTtlMs=${result.configuration.persistentIdleTtlMs ?? 30000}; readyTimeout=${result.configuration.persistentReadyTimeoutMs ?? 10000}; fallback=${result.configuration.persistentFallback ?? 'before-accept'}`);
|
|
831
|
+
lines.push(`models: ${result.models.map(m => `${m.id} (${m.name})`).join(', ') || 'none'}`);
|
|
832
|
+
lines.push(`modelCatalog: source=${result.modelCatalog.source}; stale=${result.modelCatalog.stale}; warningCode=${result.modelCatalog.warningCode ?? 'none'}`);
|
|
833
|
+
lines.push(`agy: version=${result.agy.version ?? 'unknown'}; agents=${result.agy.agents.join(', ') || 'none'}; executableSource=${result.agy.executableSource}`);
|
|
834
|
+
if (result.errors.length > 0) {
|
|
835
|
+
lines.push('issues:');
|
|
836
|
+
for (const issue of result.errors) {
|
|
837
|
+
lines.push(`- [${issue.component}/${issue.code}] ${issue.message}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return lines.join('\n');
|
|
841
|
+
}
|
|
842
|
+
export function formatDoctorHelp() {
|
|
843
|
+
return `Usage: dsh-agy-cli [doctor] [options]
|
|
844
|
+
|
|
845
|
+
Quota-free diagnostic CLI v3 for dsh-agy-cli and DSH profiles.
|
|
846
|
+
|
|
847
|
+
Commands:
|
|
848
|
+
doctor Run diagnostic checks (default)
|
|
849
|
+
|
|
850
|
+
Options:
|
|
851
|
+
--profile <name> DSH profile to inspect (e.g. web, headless)
|
|
852
|
+
--dsh-home <path> Custom DSH home directory path
|
|
853
|
+
--dsh-bin <path> Custom DSH CLI executable / entry path
|
|
854
|
+
--json Output structured JSON
|
|
855
|
+
-v, --version Print version and exit
|
|
856
|
+
-h, --help Show help information
|
|
857
|
+
|
|
858
|
+
Examples:
|
|
859
|
+
npx dsh-agy-cli doctor
|
|
860
|
+
npx dsh-agy-cli doctor --profile web
|
|
861
|
+
npx dsh-agy-cli doctor --profile web --json
|
|
862
|
+
`;
|
|
863
|
+
}
|
|
864
|
+
export async function runDoctorCli(argv, stdout = text => process.stdout.write(text), stderr = text => process.stderr.write(text)) {
|
|
865
|
+
const args = [...argv];
|
|
866
|
+
if (args[0] === 'doctor')
|
|
867
|
+
args.shift();
|
|
868
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
869
|
+
stdout(`${formatDoctorHelp()}\n`);
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
873
|
+
stdout(`${packageMetadata.version ?? 'unknown'}\n`);
|
|
874
|
+
return 0;
|
|
875
|
+
}
|
|
876
|
+
const jsonOutput = args.includes('--json');
|
|
877
|
+
let profile;
|
|
878
|
+
const profileIdx = args.indexOf('--profile');
|
|
879
|
+
if (profileIdx !== -1 && profileIdx + 1 < args.length) {
|
|
880
|
+
profile = args[profileIdx + 1];
|
|
881
|
+
}
|
|
882
|
+
let dshHome;
|
|
883
|
+
const dshHomeIdx = args.indexOf('--dsh-home');
|
|
884
|
+
if (dshHomeIdx !== -1 && dshHomeIdx + 1 < args.length) {
|
|
885
|
+
dshHome = args[dshHomeIdx + 1];
|
|
886
|
+
}
|
|
887
|
+
let dshBin;
|
|
888
|
+
const dshBinIdx = args.indexOf('--dsh-bin');
|
|
889
|
+
if (dshBinIdx !== -1 && dshBinIdx + 1 < args.length) {
|
|
890
|
+
dshBin = args[dshBinIdx + 1];
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const result = await runDoctor({
|
|
894
|
+
...(profile === undefined ? {} : { profile }),
|
|
895
|
+
...(dshHome === undefined ? {} : { dshHome }),
|
|
896
|
+
...(dshBin === undefined ? {} : { dshBin }),
|
|
897
|
+
config: Config({}),
|
|
898
|
+
});
|
|
899
|
+
if (jsonOutput) {
|
|
900
|
+
stdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
stdout(`${formatDoctorHuman(result)}\n`);
|
|
904
|
+
}
|
|
905
|
+
return result.ok ? 0 : 1;
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
const message = redactText(error instanceof Error ? error.message : String(error), 512);
|
|
909
|
+
if (jsonOutput) {
|
|
910
|
+
stdout(JSON.stringify({
|
|
911
|
+
schemaVersion: 1,
|
|
912
|
+
ok: false,
|
|
913
|
+
quotaUsed: false,
|
|
914
|
+
errors: [{ component: 'config', code: 'DOCTOR_EXECUTION_FAILED', message }],
|
|
915
|
+
}, null, 2) + '\n');
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
stderr(`Error running doctor: ${message}\n`);
|
|
919
|
+
}
|
|
920
|
+
return 1;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
//# sourceMappingURL=doctor.js.map
|