@wot-ui/cli 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +356 -124
- package/dist/index.mjs +1903 -9
- package/dist/{server-Cbs8eIrc.mjs → server-DDRLv2Z2.mjs} +6 -3
- package/dist/{update-check-DlG1c3ZB.mjs → update-check-BacAuhBM.mjs} +34 -3
- package/package.json +10 -2
- package/skills/wot-ui-cli/SKILL.md +108 -0
- package/skills/wot-ui-cli/references/overview.md +164 -0
- package/skills/wot-ui-v2/SKILL.md +46 -0
- package/skills/wot-ui-v2/references/overview.md +85 -0
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,1596 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { _ as version, a as filterComponents, d as formatStatusLabel, f as writeStderrLine, g as name, h as loadMetadataFile, i as lintProject, m as resolveVersion, o as findComponent, p as detectVersion, r as analyzeUsage, s as listComponents, t as checkForCliUpdate, u as formatLogMessage } from "./update-check-BacAuhBM.mjs";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import {
|
|
5
|
+
import { constants, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { access, chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { isDeepStrictEqual, stripVTControlCharacters } from "node:util";
|
|
14
|
+
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
15
|
+
import { parse as parse$1 } from "smol-toml";
|
|
16
|
+
import { LATEST_PROTOCOL_VERSION, ReadBuffer, serializeMessage } from "@modelcontextprotocol/server";
|
|
7
17
|
|
|
18
|
+
//#region src/application/change-plan.ts
|
|
19
|
+
async function pathExists(path) {
|
|
20
|
+
try {
|
|
21
|
+
await access(path, constants.F_OK);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readOptionalFile(path) {
|
|
28
|
+
try {
|
|
29
|
+
return await readFile(path, "utf8");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.code === "ENOENT") return void 0;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var ChangePlanCleanupError = class extends Error {
|
|
36
|
+
applied;
|
|
37
|
+
cleanupFailures;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
super(`Change plan was applied, but transaction cleanup failed for ${options.cleanupFailures.map((failure) => failure.path).join(", ")}`);
|
|
40
|
+
this.name = "ChangePlanCleanupError";
|
|
41
|
+
this.applied = options.applied;
|
|
42
|
+
this.cleanupFailures = options.cleanupFailures;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var ChangePlanApplyError = class extends Error {
|
|
46
|
+
cause;
|
|
47
|
+
applied;
|
|
48
|
+
rolledBack;
|
|
49
|
+
rollbackFailures;
|
|
50
|
+
cleanupFailures;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
const causeMessage = options.cause instanceof Error ? options.cause.message : String(options.cause);
|
|
53
|
+
const rollbackMessage = options.rollbackFailures.length > 0 ? `rollback incomplete for ${options.rollbackFailures.map((failure) => failure.backupPath ? `${failure.path} (backup: ${failure.backupPath})` : failure.path).join(", ")}` : options.applied.length > 0 ? `rolled back ${options.rolledBack.length} applied change${options.rolledBack.length === 1 ? "" : "s"}` : "no files were changed";
|
|
54
|
+
const cleanupMessage = options.cleanupFailures.length > 0 ? `; cleanup incomplete for ${options.cleanupFailures.map((failure) => failure.path).join(", ")}` : "";
|
|
55
|
+
super(`Failed to apply change plan; ${rollbackMessage}${cleanupMessage}: ${causeMessage}`);
|
|
56
|
+
this.name = "ChangePlanApplyError";
|
|
57
|
+
this.cause = options.cause;
|
|
58
|
+
this.applied = options.applied;
|
|
59
|
+
this.rolledBack = options.rolledBack;
|
|
60
|
+
this.rollbackFailures = options.rollbackFailures;
|
|
61
|
+
this.cleanupFailures = options.cleanupFailures;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function isWithinRoot(root, path) {
|
|
65
|
+
const relation = relative(root, path);
|
|
66
|
+
return relation === "" || !relation.startsWith("..") && !isAbsolute(relation);
|
|
67
|
+
}
|
|
68
|
+
async function nearestExistingPath(path) {
|
|
69
|
+
let current = path;
|
|
70
|
+
while (true) try {
|
|
71
|
+
await lstat(current);
|
|
72
|
+
return current;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code !== "ENOENT") throw error;
|
|
75
|
+
const parent = dirname(current);
|
|
76
|
+
if (parent === current) throw error;
|
|
77
|
+
current = parent;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function assertPlanPath(change) {
|
|
81
|
+
const { path, allowedRoot } = change;
|
|
82
|
+
if (!path || path === "/" || path === process.env.HOME) throw new Error(`Refusing unsafe change target: ${path || "<empty>"}`);
|
|
83
|
+
if (!allowedRoot) throw new Error(`Missing allowed root for change target: ${path}`);
|
|
84
|
+
const [resolvedRoot, existingPath] = await Promise.all([realpath(allowedRoot), nearestExistingPath(path)]);
|
|
85
|
+
if (!isWithinRoot(resolvedRoot, await realpath(existingPath))) throw new Error(`Refusing change target outside allowed root: ${path}`);
|
|
86
|
+
if (existingPath === path && (await lstat(path)).isSymbolicLink()) throw new Error(`Refusing symbolic link change target: ${path}`);
|
|
87
|
+
}
|
|
88
|
+
async function assertUnchanged(change) {
|
|
89
|
+
await assertPlanPath(change);
|
|
90
|
+
if (await readOptionalFile(change.path) !== change.before) throw new Error(`Target changed after planning: ${change.path}`);
|
|
91
|
+
}
|
|
92
|
+
function transactionPath(path, transactionId, index, suffix) {
|
|
93
|
+
return `${path}.open-wot-${transactionId}-${index}.${suffix}`;
|
|
94
|
+
}
|
|
95
|
+
async function prepareChange(change, transactionId, index) {
|
|
96
|
+
await assertPlanPath(change);
|
|
97
|
+
if (change.type === "delete-file") return {
|
|
98
|
+
change,
|
|
99
|
+
touched: false,
|
|
100
|
+
backupCreated: false
|
|
101
|
+
};
|
|
102
|
+
await mkdir(dirname(change.path), { recursive: true });
|
|
103
|
+
const temporaryPath = transactionPath(change.path, transactionId, index, "tmp");
|
|
104
|
+
try {
|
|
105
|
+
await writeFile(temporaryPath, change.after ?? "", "utf8");
|
|
106
|
+
if (change.before !== void 0 && await pathExists(change.path)) await chmod(temporaryPath, (await stat(change.path)).mode);
|
|
107
|
+
return {
|
|
108
|
+
change,
|
|
109
|
+
temporaryPath,
|
|
110
|
+
touched: false,
|
|
111
|
+
backupCreated: false
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
await rm(temporaryPath, { force: true }).catch(() => {});
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function commitChange(prepared, transactionId, index, hooks) {
|
|
119
|
+
const { change } = prepared;
|
|
120
|
+
await assertUnchanged(change);
|
|
121
|
+
if (change.before !== void 0) {
|
|
122
|
+
prepared.backupPath = transactionPath(change.path, transactionId, index, "rollback");
|
|
123
|
+
await hooks.beforeBackup?.(change, prepared.backupPath, index);
|
|
124
|
+
await copyFile(change.path, prepared.backupPath, constants.COPYFILE_EXCL);
|
|
125
|
+
prepared.backupCreated = true;
|
|
126
|
+
}
|
|
127
|
+
if (change.type === "write-file") {
|
|
128
|
+
await rename(prepared.temporaryPath, change.path);
|
|
129
|
+
prepared.touched = true;
|
|
130
|
+
} else {
|
|
131
|
+
await rm(change.path);
|
|
132
|
+
prepared.touched = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function rollbackPreparedChange(prepared) {
|
|
136
|
+
const { change } = prepared;
|
|
137
|
+
if (!prepared.touched) return;
|
|
138
|
+
if (prepared.backupCreated && prepared.backupPath) {
|
|
139
|
+
await rm(change.path, { force: true });
|
|
140
|
+
await rename(prepared.backupPath, change.path);
|
|
141
|
+
prepared.backupCreated = false;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await rm(change.path, { force: true });
|
|
145
|
+
}
|
|
146
|
+
async function cleanupPreparedChanges(preparedChanges, removeBackups, hooks) {
|
|
147
|
+
const failures = [];
|
|
148
|
+
await Promise.all(preparedChanges.flatMap((prepared) => {
|
|
149
|
+
const paths = [prepared.temporaryPath];
|
|
150
|
+
if (removeBackups) paths.push(prepared.backupPath);
|
|
151
|
+
return paths.filter((path) => Boolean(path)).map(async (path) => {
|
|
152
|
+
try {
|
|
153
|
+
await hooks.beforeCleanup?.(path);
|
|
154
|
+
await rm(path, { force: true });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
failures.push({
|
|
157
|
+
path,
|
|
158
|
+
error: error instanceof Error ? error.message : String(error)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}));
|
|
163
|
+
return failures;
|
|
164
|
+
}
|
|
165
|
+
async function applyChangePlan(plan, hooks = {}) {
|
|
166
|
+
const transactionId = `${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
167
|
+
const preparedChanges = [];
|
|
168
|
+
const applied = [];
|
|
169
|
+
try {
|
|
170
|
+
for (const change of plan.changes) await assertUnchanged(change);
|
|
171
|
+
for (const [index, change] of plan.changes.entries()) preparedChanges.push(await prepareChange(change, transactionId, index));
|
|
172
|
+
for (const [index, prepared] of preparedChanges.entries()) {
|
|
173
|
+
await hooks.beforeCommit?.(prepared.change, index);
|
|
174
|
+
await commitChange(prepared, transactionId, index, hooks);
|
|
175
|
+
applied.push(prepared);
|
|
176
|
+
}
|
|
177
|
+
const cleanupFailures = await cleanupPreparedChanges(preparedChanges, true, hooks);
|
|
178
|
+
if (cleanupFailures.length > 0) throw new ChangePlanCleanupError({
|
|
179
|
+
applied: applied.map((item) => item.change.path),
|
|
180
|
+
cleanupFailures
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
changed: applied.length > 0,
|
|
184
|
+
applied: applied.map((item) => item.change),
|
|
185
|
+
rolledBack: false
|
|
186
|
+
};
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof ChangePlanCleanupError) throw error;
|
|
189
|
+
const rollbackTargets = preparedChanges.filter((change) => change.touched).reverse();
|
|
190
|
+
const rolledBack = [];
|
|
191
|
+
const rollbackFailures = [];
|
|
192
|
+
for (const [index, prepared] of rollbackTargets.entries()) try {
|
|
193
|
+
await hooks.beforeRollback?.(prepared.change, index);
|
|
194
|
+
await rollbackPreparedChange(prepared);
|
|
195
|
+
rolledBack.push(prepared.change.path);
|
|
196
|
+
} catch (rollbackError) {
|
|
197
|
+
rollbackFailures.push({
|
|
198
|
+
path: prepared.change.path,
|
|
199
|
+
error: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
200
|
+
...prepared.backupCreated && prepared.backupPath ? { backupPath: prepared.backupPath } : {}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const cleanupFailures = await cleanupPreparedChanges(preparedChanges, rollbackFailures.length === 0, hooks);
|
|
204
|
+
throw new ChangePlanApplyError({
|
|
205
|
+
cause: error,
|
|
206
|
+
applied: applied.map((item) => item.change.path),
|
|
207
|
+
rolledBack,
|
|
208
|
+
rollbackFailures,
|
|
209
|
+
cleanupFailures
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function mergeChangePlans(summary, plans) {
|
|
214
|
+
const changes = [];
|
|
215
|
+
const targets = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const change of plans.flatMap((plan) => plan.changes)) {
|
|
217
|
+
const existing = targets.get(change.path);
|
|
218
|
+
if (!existing) {
|
|
219
|
+
targets.set(change.path, change);
|
|
220
|
+
changes.push(change);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (existing.type !== change.type || existing.allowedRoot !== change.allowedRoot || existing.before !== change.before || existing.after !== change.after) throw new Error(`Conflicting planned changes for ${change.path}`);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
id: plans.map((plan) => plan.id).join("+") || "empty",
|
|
227
|
+
summary,
|
|
228
|
+
changes,
|
|
229
|
+
warnings: plans.flatMap((plan) => plan.warnings),
|
|
230
|
+
requiresConfirmation: plans.some((plan) => plan.requiresConfirmation)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function formatChangePlan(plan, cwd = process.cwd()) {
|
|
234
|
+
const lines = [plan.summary];
|
|
235
|
+
if (plan.changes.length === 0) lines.push("No changes required.");
|
|
236
|
+
for (const change of plan.changes) {
|
|
237
|
+
const path = relative(cwd, change.path) || change.path;
|
|
238
|
+
lines.push(`${change.type}: ${path}`, ` ${change.reason}`);
|
|
239
|
+
lines.push(...getSafePreview(change).map((line) => ` ${line}`));
|
|
240
|
+
}
|
|
241
|
+
for (const warning of plan.warnings) lines.push(`warning: ${warning}`);
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
function getSafePreview(change) {
|
|
245
|
+
if (change.preview?.length) return change.preview;
|
|
246
|
+
if (change.type === "delete-file" || change.after === void 0) return ["- delete file"];
|
|
247
|
+
return [change.before === void 0 ? "+ create file" : "~ update file"];
|
|
248
|
+
}
|
|
249
|
+
function toPublicChangePlan(plan, cwd = process.cwd()) {
|
|
250
|
+
return {
|
|
251
|
+
id: plan.id,
|
|
252
|
+
summary: plan.summary,
|
|
253
|
+
changes: plan.changes.map((change) => ({
|
|
254
|
+
type: change.type,
|
|
255
|
+
path: relative(cwd, change.path) || change.path,
|
|
256
|
+
reason: change.reason,
|
|
257
|
+
preview: getSafePreview(change)
|
|
258
|
+
})),
|
|
259
|
+
warnings: [...plan.warnings],
|
|
260
|
+
requiresConfirmation: plan.requiresConfirmation
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function confirmChangePlan(plan, options = {}) {
|
|
264
|
+
if (plan.changes.length === 0 || options.yes) return true;
|
|
265
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Refusing to modify files in a non-interactive session without --yes. Use --dry-run to preview.");
|
|
266
|
+
const prompt = createInterface({
|
|
267
|
+
input: process.stdin,
|
|
268
|
+
output: process.stdout
|
|
269
|
+
});
|
|
270
|
+
try {
|
|
271
|
+
process.stdout.write(`${formatChangePlan(plan, options.cwd)}\n`);
|
|
272
|
+
const answer = await prompt.question(`Apply ${plan.changes.length} change${plan.changes.length === 1 ? "" : "s"}? [y/N] `);
|
|
273
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
274
|
+
} finally {
|
|
275
|
+
prompt.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/utils/redaction.ts
|
|
281
|
+
const SENSITIVE_KEY = String.raw`(?:api[_-]?key|access[_-]?token|auth(?:orization)?|bearer|client[_-]?secret|password|passwd|secret|token|cookie)`;
|
|
282
|
+
const BEARER_PATTERN = /(authorization\s*:\s*bearer\s+)[^\s,;]+/gi;
|
|
283
|
+
const ASSIGNMENT_PATTERN = new RegExp(`(["']?${SENSITIVE_KEY}["']?\\s*[:=]\\s*)(?:"[^"]*"|'[^']*'|\\S+)`, "gi");
|
|
284
|
+
function redactSensitiveText(value, secretValues = []) {
|
|
285
|
+
let redacted = value.replace(BEARER_PATTERN, "$1[REDACTED]");
|
|
286
|
+
redacted = redacted.replace(ASSIGNMENT_PATTERN, "$1[REDACTED]");
|
|
287
|
+
for (const secret of secretValues) if (secret) redacted = redacted.split(secret).join("[REDACTED]");
|
|
288
|
+
return redacted;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/mcp/clients/client-command.ts
|
|
293
|
+
const TERMINATION_GRACE_MS = 500;
|
|
294
|
+
async function runClientCommand(executable, args, options) {
|
|
295
|
+
const platform = options.platform ?? process.platform;
|
|
296
|
+
const useShell = platform === "win32" && /\.(?:cmd|bat)$/i.test(executable);
|
|
297
|
+
const totalTimeoutMs = Math.max(1, options.timeoutMs ?? 5e3);
|
|
298
|
+
const terminationGraceMs = Math.min(TERMINATION_GRACE_MS, Math.floor(totalTimeoutMs / 5));
|
|
299
|
+
const commandTimeoutMs = totalTimeoutMs - terminationGraceMs;
|
|
300
|
+
return new Promise((resolve$1) => {
|
|
301
|
+
const child = spawn(executable, args, {
|
|
302
|
+
cwd: options.cwd,
|
|
303
|
+
env: options.env,
|
|
304
|
+
windowsHide: true,
|
|
305
|
+
shell: useShell,
|
|
306
|
+
stdio: [
|
|
307
|
+
"ignore",
|
|
308
|
+
"pipe",
|
|
309
|
+
"pipe"
|
|
310
|
+
]
|
|
311
|
+
});
|
|
312
|
+
let stdout = "";
|
|
313
|
+
let stderr = "";
|
|
314
|
+
let settled = false;
|
|
315
|
+
let timedOut = false;
|
|
316
|
+
let timer;
|
|
317
|
+
let terminationTimer;
|
|
318
|
+
const finish = (result) => {
|
|
319
|
+
if (settled) return;
|
|
320
|
+
settled = true;
|
|
321
|
+
if (timer) clearTimeout(timer);
|
|
322
|
+
if (terminationTimer) clearTimeout(terminationTimer);
|
|
323
|
+
resolve$1({
|
|
324
|
+
...result,
|
|
325
|
+
stdout: stdout.slice(-8e3),
|
|
326
|
+
stderr: stderr.slice(-8e3)
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
timer = setTimeout(() => {
|
|
330
|
+
timedOut = true;
|
|
331
|
+
if (platform === "win32" && child.pid) spawn("taskkill", [
|
|
332
|
+
"/pid",
|
|
333
|
+
String(child.pid),
|
|
334
|
+
"/T",
|
|
335
|
+
"/F"
|
|
336
|
+
], {
|
|
337
|
+
windowsHide: true,
|
|
338
|
+
stdio: "ignore"
|
|
339
|
+
}).once("error", () => child.kill("SIGTERM"));
|
|
340
|
+
else child.kill("SIGTERM");
|
|
341
|
+
terminationTimer = setTimeout(() => child.kill("SIGKILL"), terminationGraceMs);
|
|
342
|
+
terminationTimer.unref();
|
|
343
|
+
}, commandTimeoutMs);
|
|
344
|
+
timer.unref();
|
|
345
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString("utf8"));
|
|
346
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString("utf8"));
|
|
347
|
+
child.once("error", (error) => finish(timedOut ? {
|
|
348
|
+
timedOut: true,
|
|
349
|
+
error: `Client command timed out after ${totalTimeoutMs}ms`
|
|
350
|
+
} : {
|
|
351
|
+
timedOut: false,
|
|
352
|
+
error: error.message
|
|
353
|
+
}));
|
|
354
|
+
child.once("close", (code) => finish(timedOut ? {
|
|
355
|
+
timedOut: true,
|
|
356
|
+
error: `Client command timed out after ${totalTimeoutMs}ms`
|
|
357
|
+
} : {
|
|
358
|
+
timedOut: false,
|
|
359
|
+
...code === null ? {} : { exitCode: code }
|
|
360
|
+
}));
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function clientCommandOutput(result) {
|
|
364
|
+
return redactSensitiveText(stripVTControlCharacters(`${result.stdout}\n${result.stderr}`).trim());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/mcp/clients/detect.ts
|
|
369
|
+
async function isExecutable(path, platform) {
|
|
370
|
+
try {
|
|
371
|
+
await access(path, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
372
|
+
return true;
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function findExecutable(names, context) {
|
|
378
|
+
const env = context.env ?? process.env;
|
|
379
|
+
const platform = context.platform ?? process.platform;
|
|
380
|
+
const pathEntries = (env.PATH ?? "").split(platform === "win32" ? ";" : ":").filter(Boolean);
|
|
381
|
+
const pathExts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
|
|
382
|
+
for (const name$1 of names) {
|
|
383
|
+
const extensions = platform === "win32" && !extname(name$1) ? pathExts : [""];
|
|
384
|
+
for (const directory of pathEntries) for (const extension of extensions) {
|
|
385
|
+
const candidate = join(directory, `${name$1}${extension}`);
|
|
386
|
+
if (await isExecutable(candidate, platform)) return candidate;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/mcp/clients/json-adapter.ts
|
|
393
|
+
async function readOptional$2(path) {
|
|
394
|
+
try {
|
|
395
|
+
return await readFile(path, "utf8");
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error.code === "ENOENT") return void 0;
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function parseObject(content, path) {
|
|
402
|
+
const errors = [];
|
|
403
|
+
const value = parse(content, errors, {
|
|
404
|
+
allowTrailingComma: true,
|
|
405
|
+
disallowComments: false
|
|
406
|
+
});
|
|
407
|
+
if (errors.length > 0) {
|
|
408
|
+
const first = errors[0];
|
|
409
|
+
throw new Error(`Invalid JSONC in ${path}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
|
|
410
|
+
}
|
|
411
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Expected an object in ${path}`);
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
function isStringMap(value) {
|
|
415
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((item) => typeof item === "string");
|
|
416
|
+
}
|
|
417
|
+
function isServerDefinition(value) {
|
|
418
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
419
|
+
const candidate = value;
|
|
420
|
+
const env = candidate.env;
|
|
421
|
+
return typeof candidate.command === "string" && Array.isArray(candidate.args) && candidate.args.every((argument) => typeof argument === "string") && (env === void 0 || isStringMap(env));
|
|
422
|
+
}
|
|
423
|
+
function serverMatches$1(actual, expected) {
|
|
424
|
+
return actual?.command === expected.command && JSON.stringify(actual.args) === JSON.stringify(expected.args) && JSON.stringify(actual.env ?? {}) === JSON.stringify(expected.env ?? {});
|
|
425
|
+
}
|
|
426
|
+
function serverPreview$1(server, serverKey) {
|
|
427
|
+
return [
|
|
428
|
+
`+ ${serverKey}.wot-ui`,
|
|
429
|
+
` command: ${server.command}`,
|
|
430
|
+
` args: ${server.args.join(" ")}`,
|
|
431
|
+
...Object.keys(server.env ?? {}).map((key) => ` env.${key}: [REDACTED]`)
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
var JsonMcpClientAdapter = class {
|
|
435
|
+
id;
|
|
436
|
+
displayName;
|
|
437
|
+
supportedScopes;
|
|
438
|
+
options;
|
|
439
|
+
constructor(options) {
|
|
440
|
+
this.options = options;
|
|
441
|
+
this.id = options.id;
|
|
442
|
+
this.displayName = options.displayName;
|
|
443
|
+
this.supportedScopes = options.supportedScopes;
|
|
444
|
+
}
|
|
445
|
+
assertScope(scope) {
|
|
446
|
+
if (!this.supportedScopes.includes(scope)) throw new Error(`${this.displayName} does not support ${scope} scope`);
|
|
447
|
+
}
|
|
448
|
+
async detect(context) {
|
|
449
|
+
const configLocations = this.supportedScopes.map((scope) => this.options.configPath(context, scope));
|
|
450
|
+
const executable = await findExecutable(this.options.executableNames, context);
|
|
451
|
+
const hasConfig = (await Promise.all(configLocations.map(async (path) => {
|
|
452
|
+
try {
|
|
453
|
+
await access(path);
|
|
454
|
+
return true;
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}))).some(Boolean);
|
|
459
|
+
return {
|
|
460
|
+
client: this.id,
|
|
461
|
+
displayName: this.displayName,
|
|
462
|
+
installed: Boolean(executable) || hasConfig,
|
|
463
|
+
...executable ? { executable } : {},
|
|
464
|
+
supportedScopes: this.supportedScopes,
|
|
465
|
+
configLocations,
|
|
466
|
+
preferredMethod: "config-file"
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async inspect(context) {
|
|
470
|
+
this.assertScope(context.scope);
|
|
471
|
+
const path = this.options.configPath(context, context.scope);
|
|
472
|
+
const content = await readOptional$2(path);
|
|
473
|
+
if (content === void 0) return {
|
|
474
|
+
client: this.id,
|
|
475
|
+
displayName: this.displayName,
|
|
476
|
+
scope: context.scope,
|
|
477
|
+
path,
|
|
478
|
+
exists: false,
|
|
479
|
+
configured: false,
|
|
480
|
+
matches: false
|
|
481
|
+
};
|
|
482
|
+
try {
|
|
483
|
+
const servers = parseObject(content || "{}", path)[this.options.serverKey];
|
|
484
|
+
const entry = servers && typeof servers === "object" && !Array.isArray(servers) ? servers["wot-ui"] : void 0;
|
|
485
|
+
const server = isServerDefinition(entry) ? entry : void 0;
|
|
486
|
+
return {
|
|
487
|
+
client: this.id,
|
|
488
|
+
displayName: this.displayName,
|
|
489
|
+
scope: context.scope,
|
|
490
|
+
path,
|
|
491
|
+
exists: true,
|
|
492
|
+
configured: entry !== void 0,
|
|
493
|
+
matches: serverMatches$1(server, context.server),
|
|
494
|
+
...server ? { server } : {},
|
|
495
|
+
...!server && entry !== void 0 ? { problem: "The wot-ui entry is not a supported stdio server definition" } : {}
|
|
496
|
+
};
|
|
497
|
+
} catch (error) {
|
|
498
|
+
return {
|
|
499
|
+
client: this.id,
|
|
500
|
+
displayName: this.displayName,
|
|
501
|
+
scope: context.scope,
|
|
502
|
+
path,
|
|
503
|
+
exists: true,
|
|
504
|
+
configured: false,
|
|
505
|
+
matches: false,
|
|
506
|
+
problem: error instanceof Error ? error.message : String(error)
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async planInstall(context) {
|
|
511
|
+
this.assertScope(context.scope);
|
|
512
|
+
const path = this.options.configPath(context, context.scope);
|
|
513
|
+
const before = await readOptional$2(path);
|
|
514
|
+
const source = before?.trim() ? before : "{}\n";
|
|
515
|
+
parseObject(source, path);
|
|
516
|
+
const after = applyEdits(source, modify(source, [this.options.serverKey, "wot-ui"], context.server, { formattingOptions: {
|
|
517
|
+
insertSpaces: true,
|
|
518
|
+
tabSize: 2,
|
|
519
|
+
eol: source.includes("\r\n") ? "\r\n" : "\n"
|
|
520
|
+
} }));
|
|
521
|
+
const unchanged = before !== void 0 ? before === after : after === "{}\n";
|
|
522
|
+
return {
|
|
523
|
+
id: `${this.id}-${context.scope}-install`,
|
|
524
|
+
summary: `Configure wot-ui MCP for ${this.displayName}`,
|
|
525
|
+
changes: unchanged ? [] : [{
|
|
526
|
+
type: "write-file",
|
|
527
|
+
path,
|
|
528
|
+
allowedRoot: context.scope === "project" ? context.cwd : context.homeDir,
|
|
529
|
+
before,
|
|
530
|
+
after,
|
|
531
|
+
reason: `Add or update mcp server "wot-ui" under ${this.options.serverKey}`,
|
|
532
|
+
preview: serverPreview$1(context.server, this.options.serverKey)
|
|
533
|
+
}],
|
|
534
|
+
warnings: [],
|
|
535
|
+
requiresConfirmation: !unchanged
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async planRemove(context) {
|
|
539
|
+
this.assertScope(context.scope);
|
|
540
|
+
const path = this.options.configPath(context, context.scope);
|
|
541
|
+
const before = await readOptional$2(path);
|
|
542
|
+
if (before === void 0) return {
|
|
543
|
+
id: `${this.id}-${context.scope}-remove`,
|
|
544
|
+
summary: `Remove wot-ui MCP from ${this.displayName}`,
|
|
545
|
+
changes: [],
|
|
546
|
+
warnings: [],
|
|
547
|
+
requiresConfirmation: false
|
|
548
|
+
};
|
|
549
|
+
parseObject(before || "{}", path);
|
|
550
|
+
const after = applyEdits(before, modify(before, [this.options.serverKey, "wot-ui"], void 0, { formattingOptions: {
|
|
551
|
+
insertSpaces: true,
|
|
552
|
+
tabSize: 2,
|
|
553
|
+
eol: before.includes("\r\n") ? "\r\n" : "\n"
|
|
554
|
+
} }));
|
|
555
|
+
return {
|
|
556
|
+
id: `${this.id}-${context.scope}-remove`,
|
|
557
|
+
summary: `Remove wot-ui MCP from ${this.displayName}`,
|
|
558
|
+
changes: before === after ? [] : [{
|
|
559
|
+
type: "write-file",
|
|
560
|
+
path,
|
|
561
|
+
allowedRoot: context.scope === "project" ? context.cwd : context.homeDir,
|
|
562
|
+
before,
|
|
563
|
+
after,
|
|
564
|
+
reason: "Remove only the wot-ui MCP server entry",
|
|
565
|
+
preview: [`- ${this.options.serverKey}.wot-ui`]
|
|
566
|
+
}],
|
|
567
|
+
warnings: [],
|
|
568
|
+
requiresConfirmation: before !== after
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
async print(context) {
|
|
572
|
+
this.assertScope(context.scope);
|
|
573
|
+
return JSON.stringify({ [this.options.serverKey]: { "wot-ui": context.server } }, null, 2);
|
|
574
|
+
}
|
|
575
|
+
async verifyRegistration(context, state, options) {
|
|
576
|
+
if (this.options.verifyRegistration) return this.options.verifyRegistration(context, state, options);
|
|
577
|
+
return {
|
|
578
|
+
status: "unsupported",
|
|
579
|
+
message: `${this.displayName} does not expose a stable CLI registration status check`,
|
|
580
|
+
action: `Restart ${this.displayName} and confirm wot-ui in its MCP server panel.`
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/mcp/clients/claude.ts
|
|
587
|
+
const claudeAdapter = new JsonMcpClientAdapter({
|
|
588
|
+
id: "claude",
|
|
589
|
+
displayName: "Claude Code",
|
|
590
|
+
executableNames: ["claude"],
|
|
591
|
+
supportedScopes: ["project", "user"],
|
|
592
|
+
configPath: (context, scope) => scope === "project" ? resolve(context.cwd, ".mcp.json") : join(context.homeDir, ".claude.json"),
|
|
593
|
+
serverKey: "mcpServers",
|
|
594
|
+
verifyRegistration: async (context, _state, options) => {
|
|
595
|
+
const executable = await findExecutable(["claude"], context);
|
|
596
|
+
if (!executable) return {
|
|
597
|
+
status: "unknown",
|
|
598
|
+
message: "Claude Code executable was not found; registration could not be verified",
|
|
599
|
+
action: "Open Claude Code in this project and inspect /mcp."
|
|
600
|
+
};
|
|
601
|
+
const result = await runClientCommand(executable, [
|
|
602
|
+
"mcp",
|
|
603
|
+
"get",
|
|
604
|
+
"wot-ui"
|
|
605
|
+
], {
|
|
606
|
+
...context,
|
|
607
|
+
timeoutMs: options?.timeoutMs
|
|
608
|
+
});
|
|
609
|
+
const output = clientCommandOutput(result);
|
|
610
|
+
const command = `${executable} mcp get wot-ui`;
|
|
611
|
+
if (result.timedOut || result.error) return {
|
|
612
|
+
status: "failed",
|
|
613
|
+
message: result.error || "Claude Code registration check failed",
|
|
614
|
+
action: "Retry claude mcp get wot-ui and inspect the client process if it still fails.",
|
|
615
|
+
command
|
|
616
|
+
};
|
|
617
|
+
if (/pending approval|approval.*pending|untrusted|not trusted|trust (?:this )?(?:project|workspace)/i.test(output)) return {
|
|
618
|
+
status: "pending",
|
|
619
|
+
message: "Claude Code found wot-ui, but project approval is pending",
|
|
620
|
+
action: "Open Claude Code in this project and approve the wot-ui MCP server.",
|
|
621
|
+
command
|
|
622
|
+
};
|
|
623
|
+
if (/rejected|disabled/i.test(output)) return {
|
|
624
|
+
status: "failed",
|
|
625
|
+
message: "Claude Code has rejected or disabled the wot-ui MCP server",
|
|
626
|
+
action: "Open /mcp in Claude Code and enable or re-approve wot-ui.",
|
|
627
|
+
command
|
|
628
|
+
};
|
|
629
|
+
if (result.exitCode === 0 && !result.timedOut) return {
|
|
630
|
+
status: "ready",
|
|
631
|
+
message: "Claude Code recognizes the wot-ui MCP server",
|
|
632
|
+
command
|
|
633
|
+
};
|
|
634
|
+
return {
|
|
635
|
+
status: "failed",
|
|
636
|
+
message: "Claude Code did not recognize the wot-ui MCP server",
|
|
637
|
+
action: "Run claude mcp get wot-ui and inspect the configured scope.",
|
|
638
|
+
command
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/mcp/clients/types.ts
|
|
645
|
+
const MCP_CLIENT_IDS = [
|
|
646
|
+
"claude",
|
|
647
|
+
"cursor",
|
|
648
|
+
"vscode",
|
|
649
|
+
"codex"
|
|
650
|
+
];
|
|
651
|
+
const REQUIRED_WOT_MCP_TOOLS = ["wot_status", "wot_list"];
|
|
652
|
+
function isMcpClientId(value) {
|
|
653
|
+
return MCP_CLIENT_IDS.includes(value);
|
|
654
|
+
}
|
|
655
|
+
function isMcpScope(value) {
|
|
656
|
+
return value === "project" || value === "user";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/mcp/clients/toml-adapter.ts
|
|
661
|
+
const START = "# open-wot managed mcp server start";
|
|
662
|
+
const END = "# open-wot managed mcp server end";
|
|
663
|
+
const TABLE_PATTERN = /^\[mcp_servers\.(?:wot-ui|"wot-ui")\]\s*$/m;
|
|
664
|
+
const CHILD_TABLE_PATTERN = /^\s*\[\[?\s*mcp_servers\s*\.\s*(?:wot-ui|"wot-ui")\s*\./m;
|
|
665
|
+
async function readOptional$1(path) {
|
|
666
|
+
try {
|
|
667
|
+
return await readFile(path, "utf8");
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (error.code === "ENOENT") return void 0;
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function quoteToml(value) {
|
|
674
|
+
return JSON.stringify(value);
|
|
675
|
+
}
|
|
676
|
+
function createBlock(server) {
|
|
677
|
+
const lines = [
|
|
678
|
+
START,
|
|
679
|
+
"[mcp_servers.wot-ui]",
|
|
680
|
+
`command = ${quoteToml(server.command)}`,
|
|
681
|
+
`args = [${server.args.map(quoteToml).join(", ")}]`
|
|
682
|
+
];
|
|
683
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
684
|
+
const env = Object.entries(server.env).map(([key, value]) => `${quoteToml(key)} = ${quoteToml(value)}`).join(", ");
|
|
685
|
+
lines.push(`env = { ${env} }`);
|
|
686
|
+
}
|
|
687
|
+
lines.push(END);
|
|
688
|
+
return lines.join("\n");
|
|
689
|
+
}
|
|
690
|
+
function serverPreview(server) {
|
|
691
|
+
return [
|
|
692
|
+
"+ mcp_servers.wot-ui",
|
|
693
|
+
` command: ${server.command}`,
|
|
694
|
+
` args: ${server.args.join(" ")}`,
|
|
695
|
+
...Object.keys(server.env ?? {}).map((key) => ` env.${key}: [REDACTED]`)
|
|
696
|
+
];
|
|
697
|
+
}
|
|
698
|
+
function managedPattern() {
|
|
699
|
+
return /* @__PURE__ */ new RegExp(`${START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
700
|
+
}
|
|
701
|
+
function parseTomlDocument(content, path) {
|
|
702
|
+
try {
|
|
703
|
+
return parse$1(content);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
throw new Error(`Invalid TOML in ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function isRecord$1(value) {
|
|
709
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
710
|
+
}
|
|
711
|
+
function getServerEntry(config) {
|
|
712
|
+
const servers = config.mcp_servers;
|
|
713
|
+
return isRecord$1(servers) ? servers["wot-ui"] : void 0;
|
|
714
|
+
}
|
|
715
|
+
function optionalStringArray(value) {
|
|
716
|
+
if (value === void 0) return void 0;
|
|
717
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
718
|
+
}
|
|
719
|
+
function parseServer(entry) {
|
|
720
|
+
if (!isRecord$1(entry) || typeof entry.command !== "string" || !Array.isArray(entry.args) || !entry.args.every((argument) => typeof argument === "string")) return void 0;
|
|
721
|
+
const env = entry.env;
|
|
722
|
+
if (env !== void 0 && (!isRecord$1(env) || !Object.values(env).every((value) => typeof value === "string"))) return void 0;
|
|
723
|
+
if (entry.enabled !== void 0 && typeof entry.enabled !== "boolean") return void 0;
|
|
724
|
+
const enabledTools = optionalStringArray(entry.enabled_tools);
|
|
725
|
+
const disabledTools = optionalStringArray(entry.disabled_tools);
|
|
726
|
+
if (enabledTools === null || disabledTools === null) return void 0;
|
|
727
|
+
return {
|
|
728
|
+
server: {
|
|
729
|
+
command: entry.command,
|
|
730
|
+
args: entry.args,
|
|
731
|
+
...env === void 0 ? {} : { env }
|
|
732
|
+
},
|
|
733
|
+
...entry.enabled === void 0 ? {} : { enabled: entry.enabled },
|
|
734
|
+
...enabledTools === void 0 ? {} : { enabledTools },
|
|
735
|
+
...disabledTools === void 0 ? {} : { disabledTools }
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function serverProblem(actual) {
|
|
739
|
+
if (!actual) return "The wot-ui entry is not a supported stdio server definition";
|
|
740
|
+
if (actual.enabled === false) return "The wot-ui MCP server is disabled in Codex";
|
|
741
|
+
const missingEnabledTools = actual.enabledTools === void 0 ? [] : REQUIRED_WOT_MCP_TOOLS.filter((tool) => !actual.enabledTools.includes(tool));
|
|
742
|
+
const disabledRequiredTools = actual.disabledTools === void 0 ? [] : REQUIRED_WOT_MCP_TOOLS.filter((tool) => actual.disabledTools.includes(tool));
|
|
743
|
+
const unavailableTools = [...new Set([...missingEnabledTools, ...disabledRequiredTools])];
|
|
744
|
+
if (unavailableTools.length > 0) return `The Codex tool filters exclude required tools: ${unavailableTools.join(", ")}`;
|
|
745
|
+
}
|
|
746
|
+
function serverMatches(actual, expected) {
|
|
747
|
+
return !serverProblem(actual) && actual?.server.command === expected.command && JSON.stringify(actual.server.args) === JSON.stringify(expected.args) && JSON.stringify(actual.server.env ?? {}) === JSON.stringify(expected.env ?? {});
|
|
748
|
+
}
|
|
749
|
+
function assertNoExternalChildTables(content, path) {
|
|
750
|
+
if (CHILD_TABLE_PATTERN.test(content)) throw new Error(`Cannot safely manage nested mcp_servers.wot-ui TOML tables in ${path}; migrate them into the main server definition first`);
|
|
751
|
+
}
|
|
752
|
+
function assertManagedEntryOwnsServer(content, path) {
|
|
753
|
+
const managed = managedPattern().exec(content)?.[0];
|
|
754
|
+
if (!managed) return;
|
|
755
|
+
if (!isDeepStrictEqual(getServerEntry(parseTomlDocument(content, path)), getServerEntry(parseTomlDocument(managed, path)))) throw new Error(`Cannot safely manage mcp_servers.wot-ui settings outside the open-wot managed block in ${path}`);
|
|
756
|
+
}
|
|
757
|
+
function assertInstalledEntry(content, block, path) {
|
|
758
|
+
if (!isDeepStrictEqual(getServerEntry(parseTomlDocument(content, path)), getServerEntry(parseTomlDocument(block, path)))) throw new Error(`Cannot safely install mcp_servers.wot-ui while external settings remain in ${path}`);
|
|
759
|
+
}
|
|
760
|
+
function replaceServerSection(content, block) {
|
|
761
|
+
const pattern = managedPattern();
|
|
762
|
+
if (pattern.test(content)) return content.replace(pattern, () => block);
|
|
763
|
+
const tableMatch = TABLE_PATTERN.exec(content);
|
|
764
|
+
if (tableMatch) {
|
|
765
|
+
const start = tableMatch.index;
|
|
766
|
+
const remainder = content.slice(start + tableMatch[0].length);
|
|
767
|
+
const nextTable = /\n\s*\[[^\]]+\]/.exec(remainder);
|
|
768
|
+
const end = nextTable ? start + tableMatch[0].length + nextTable.index + 1 : content.length;
|
|
769
|
+
return `${content.slice(0, start)}${block}\n${content.slice(end)}`;
|
|
770
|
+
}
|
|
771
|
+
if (!content) return `${block}\n`;
|
|
772
|
+
return `${content}${content.endsWith("\n") ? "" : "\n"}${block}\n`;
|
|
773
|
+
}
|
|
774
|
+
function removeServerSection(content) {
|
|
775
|
+
const match = managedPattern().exec(content);
|
|
776
|
+
if (!match) return content;
|
|
777
|
+
let end = match.index + match[0].length;
|
|
778
|
+
if (content.startsWith("\r\n", end)) end += 2;
|
|
779
|
+
else if (content.startsWith("\n", end)) end += 1;
|
|
780
|
+
return `${content.slice(0, match.index)}${content.slice(end)}`;
|
|
781
|
+
}
|
|
782
|
+
var TomlMcpClientAdapter = class {
|
|
783
|
+
id;
|
|
784
|
+
displayName;
|
|
785
|
+
supportedScopes;
|
|
786
|
+
options;
|
|
787
|
+
constructor(options) {
|
|
788
|
+
this.options = options;
|
|
789
|
+
this.id = options.id;
|
|
790
|
+
this.displayName = options.displayName;
|
|
791
|
+
this.supportedScopes = options.supportedScopes;
|
|
792
|
+
}
|
|
793
|
+
async detect(context) {
|
|
794
|
+
const configLocations = this.supportedScopes.map((scope) => this.options.configPath(context, scope));
|
|
795
|
+
const executable = await findExecutable(this.options.executableNames, context);
|
|
796
|
+
const hasConfig = (await Promise.all(configLocations.map((path) => access(path).then(() => true).catch(() => false)))).some(Boolean);
|
|
797
|
+
return {
|
|
798
|
+
client: this.id,
|
|
799
|
+
displayName: this.displayName,
|
|
800
|
+
installed: Boolean(executable) || hasConfig,
|
|
801
|
+
...executable ? { executable } : {},
|
|
802
|
+
supportedScopes: this.supportedScopes,
|
|
803
|
+
configLocations,
|
|
804
|
+
preferredMethod: "config-file"
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async inspect(context) {
|
|
808
|
+
const path = this.options.configPath(context, context.scope);
|
|
809
|
+
const content = await readOptional$1(path);
|
|
810
|
+
if (content === void 0) return {
|
|
811
|
+
client: this.id,
|
|
812
|
+
displayName: this.displayName,
|
|
813
|
+
scope: context.scope,
|
|
814
|
+
path,
|
|
815
|
+
exists: false,
|
|
816
|
+
configured: false,
|
|
817
|
+
matches: false
|
|
818
|
+
};
|
|
819
|
+
try {
|
|
820
|
+
const entry = getServerEntry(parseTomlDocument(content, path));
|
|
821
|
+
const parsedServer = parseServer(entry);
|
|
822
|
+
const problem = entry === void 0 ? void 0 : serverProblem(parsedServer);
|
|
823
|
+
return {
|
|
824
|
+
client: this.id,
|
|
825
|
+
displayName: this.displayName,
|
|
826
|
+
scope: context.scope,
|
|
827
|
+
path,
|
|
828
|
+
exists: true,
|
|
829
|
+
configured: entry !== void 0,
|
|
830
|
+
matches: serverMatches(parsedServer, context.server),
|
|
831
|
+
...parsedServer ? { server: parsedServer.server } : {},
|
|
832
|
+
...problem ? { problem } : {}
|
|
833
|
+
};
|
|
834
|
+
} catch (error) {
|
|
835
|
+
return {
|
|
836
|
+
client: this.id,
|
|
837
|
+
displayName: this.displayName,
|
|
838
|
+
scope: context.scope,
|
|
839
|
+
path,
|
|
840
|
+
exists: true,
|
|
841
|
+
configured: managedPattern().test(content) || TABLE_PATTERN.test(content),
|
|
842
|
+
matches: false,
|
|
843
|
+
problem: error instanceof Error ? error.message : String(error)
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async planInstall(context) {
|
|
848
|
+
const path = this.options.configPath(context, context.scope);
|
|
849
|
+
const before = await readOptional$1(path);
|
|
850
|
+
parseTomlDocument(before ?? "", path);
|
|
851
|
+
assertNoExternalChildTables(before ?? "", path);
|
|
852
|
+
assertManagedEntryOwnsServer(before ?? "", path);
|
|
853
|
+
const block = createBlock(context.server);
|
|
854
|
+
const after = replaceServerSection(before ?? "", block);
|
|
855
|
+
assertInstalledEntry(after, block, path);
|
|
856
|
+
return {
|
|
857
|
+
id: `${this.id}-${context.scope}-install`,
|
|
858
|
+
summary: `Configure wot-ui MCP for ${this.displayName}`,
|
|
859
|
+
changes: before === after ? [] : [{
|
|
860
|
+
type: "write-file",
|
|
861
|
+
path,
|
|
862
|
+
allowedRoot: context.scope === "project" ? context.cwd : context.homeDir,
|
|
863
|
+
before,
|
|
864
|
+
after,
|
|
865
|
+
reason: "Add or update the managed mcp_servers.wot-ui TOML section",
|
|
866
|
+
preview: serverPreview(context.server)
|
|
867
|
+
}],
|
|
868
|
+
warnings: [],
|
|
869
|
+
requiresConfirmation: before !== after
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
async planRemove(context) {
|
|
873
|
+
const path = this.options.configPath(context, context.scope);
|
|
874
|
+
const before = await readOptional$1(path);
|
|
875
|
+
const beforeDocument = before === void 0 ? void 0 : parseTomlDocument(before, path);
|
|
876
|
+
if (before !== void 0 && managedPattern().test(before)) {
|
|
877
|
+
assertNoExternalChildTables(before, path);
|
|
878
|
+
assertManagedEntryOwnsServer(before, path);
|
|
879
|
+
}
|
|
880
|
+
const after = before === void 0 ? void 0 : removeServerSection(before);
|
|
881
|
+
if (after !== void 0) {
|
|
882
|
+
const afterDocument = parseTomlDocument(after, path);
|
|
883
|
+
if (managedPattern().test(before) && getServerEntry(afterDocument) !== void 0) throw new Error(`Cannot safely remove mcp_servers.wot-ui because external settings remain in ${path}`);
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
id: `${this.id}-${context.scope}-remove`,
|
|
887
|
+
summary: `Remove wot-ui MCP from ${this.displayName}`,
|
|
888
|
+
changes: before === void 0 || before === after ? [] : [{
|
|
889
|
+
type: "write-file",
|
|
890
|
+
path,
|
|
891
|
+
allowedRoot: context.scope === "project" ? context.cwd : context.homeDir,
|
|
892
|
+
before,
|
|
893
|
+
after,
|
|
894
|
+
reason: "Remove only the open-wot managed TOML section",
|
|
895
|
+
preview: ["- mcp_servers.wot-ui"]
|
|
896
|
+
}],
|
|
897
|
+
warnings: beforeDocument !== void 0 && getServerEntry(beforeDocument) !== void 0 && !managedPattern().test(before) ? ["A non-managed wot-ui section was found and left unchanged."] : [],
|
|
898
|
+
requiresConfirmation: before !== void 0 && before !== after
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
async print(context) {
|
|
902
|
+
return createBlock(context.server);
|
|
903
|
+
}
|
|
904
|
+
async verifyRegistration(context, state, options) {
|
|
905
|
+
if (this.options.verifyRegistration) return this.options.verifyRegistration(context, state, options);
|
|
906
|
+
return {
|
|
907
|
+
status: "unsupported",
|
|
908
|
+
message: `${this.displayName} does not expose a stable CLI registration status check`,
|
|
909
|
+
action: `Restart ${this.displayName} and confirm wot-ui in its MCP server panel.`
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/mcp/clients/codex.ts
|
|
916
|
+
function isRecord(value) {
|
|
917
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
918
|
+
}
|
|
919
|
+
function stringArray(value) {
|
|
920
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
921
|
+
}
|
|
922
|
+
const codexAdapter = new TomlMcpClientAdapter({
|
|
923
|
+
id: "codex",
|
|
924
|
+
displayName: "Codex",
|
|
925
|
+
executableNames: ["codex"],
|
|
926
|
+
supportedScopes: ["project", "user"],
|
|
927
|
+
configPath: (context, scope) => scope === "project" ? resolve(context.cwd, ".codex", "config.toml") : join(context.homeDir, ".codex", "config.toml"),
|
|
928
|
+
verifyRegistration: async (context, state, options) => {
|
|
929
|
+
const executable = await findExecutable(["codex"], context);
|
|
930
|
+
if (!executable) return {
|
|
931
|
+
status: "unknown",
|
|
932
|
+
message: "Codex executable was not found; registration could not be verified",
|
|
933
|
+
action: "Open Codex in this project and inspect MCP server settings."
|
|
934
|
+
};
|
|
935
|
+
const result = await runClientCommand(executable, [
|
|
936
|
+
"mcp",
|
|
937
|
+
"get",
|
|
938
|
+
"wot-ui",
|
|
939
|
+
"--json"
|
|
940
|
+
], {
|
|
941
|
+
...context,
|
|
942
|
+
timeoutMs: options?.timeoutMs
|
|
943
|
+
});
|
|
944
|
+
const output = clientCommandOutput(result);
|
|
945
|
+
const command = `${executable} mcp get wot-ui --json`;
|
|
946
|
+
if (result.timedOut || result.error) return {
|
|
947
|
+
status: "failed",
|
|
948
|
+
message: result.error || "Codex registration check failed",
|
|
949
|
+
action: "Retry codex mcp get wot-ui --json and inspect the client process if it still fails.",
|
|
950
|
+
command
|
|
951
|
+
};
|
|
952
|
+
if (result.exitCode === 0 && !result.timedOut) try {
|
|
953
|
+
const registration = JSON.parse(result.stdout);
|
|
954
|
+
if (!isRecord(registration) || registration.name !== "wot-ui") return {
|
|
955
|
+
status: "unknown",
|
|
956
|
+
message: "Codex returned an unreadable registration response",
|
|
957
|
+
command
|
|
958
|
+
};
|
|
959
|
+
if (registration.enabled === false) return {
|
|
960
|
+
status: "failed",
|
|
961
|
+
message: "Codex recognizes the wot-ui MCP server, but it is disabled",
|
|
962
|
+
action: "Enable the wot-ui MCP server in Codex and retry the doctor command.",
|
|
963
|
+
command
|
|
964
|
+
};
|
|
965
|
+
const enabledTools = stringArray(registration.enabled_tools);
|
|
966
|
+
const disabledTools = stringArray(registration.disabled_tools);
|
|
967
|
+
const unavailableTools = REQUIRED_WOT_MCP_TOOLS.filter((tool) => enabledTools !== void 0 && !enabledTools.includes(tool) || disabledTools?.includes(tool));
|
|
968
|
+
if (unavailableTools.length > 0) return {
|
|
969
|
+
status: "failed",
|
|
970
|
+
message: `Codex tool filters exclude required tools: ${unavailableTools.join(", ")}`,
|
|
971
|
+
action: "Update the wot-ui MCP tool filters in Codex and retry the doctor command.",
|
|
972
|
+
command
|
|
973
|
+
};
|
|
974
|
+
return {
|
|
975
|
+
status: "ready",
|
|
976
|
+
message: "Codex recognizes the wot-ui MCP server",
|
|
977
|
+
command
|
|
978
|
+
};
|
|
979
|
+
} catch {
|
|
980
|
+
return {
|
|
981
|
+
status: "unknown",
|
|
982
|
+
message: "Codex returned an unreadable registration response",
|
|
983
|
+
command
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
if (/pending approval|approval.*pending|untrusted|not trusted|trust (?:this )?(?:project|workspace)/i.test(output)) return {
|
|
987
|
+
status: "pending",
|
|
988
|
+
message: "Codex requires project trust or MCP approval",
|
|
989
|
+
action: "Trust this project in Codex, restart it, and verify the wot-ui MCP server.",
|
|
990
|
+
command
|
|
991
|
+
};
|
|
992
|
+
if (context.scope === "project" && state.matches && /no mcp server named ["']?wot-ui["']? found/i.test(output)) return {
|
|
993
|
+
status: "pending",
|
|
994
|
+
message: "Codex is not loading the project-level wot-ui MCP configuration yet",
|
|
995
|
+
action: "Trust this project by opening it in Codex, then restart Codex and retry the doctor command.",
|
|
996
|
+
command
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
status: "failed",
|
|
1000
|
+
message: "Codex did not recognize the wot-ui MCP server",
|
|
1001
|
+
action: "Run codex mcp get wot-ui --json and inspect the configured scope.",
|
|
1002
|
+
command
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region src/mcp/clients/cursor.ts
|
|
1009
|
+
const cursorAdapter = new JsonMcpClientAdapter({
|
|
1010
|
+
id: "cursor",
|
|
1011
|
+
displayName: "Cursor",
|
|
1012
|
+
executableNames: ["cursor-agent", "cursor"],
|
|
1013
|
+
supportedScopes: ["project", "user"],
|
|
1014
|
+
configPath: (context, scope) => scope === "project" ? resolve(context.cwd, ".cursor", "mcp.json") : join(context.homeDir, ".cursor", "mcp.json"),
|
|
1015
|
+
serverKey: "mcpServers"
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/mcp/clients/vscode.ts
|
|
1020
|
+
const vscodeAdapter = new JsonMcpClientAdapter({
|
|
1021
|
+
id: "vscode",
|
|
1022
|
+
displayName: "VS Code",
|
|
1023
|
+
executableNames: ["code"],
|
|
1024
|
+
supportedScopes: ["project"],
|
|
1025
|
+
configPath: (context) => resolve(context.cwd, ".vscode", "mcp.json"),
|
|
1026
|
+
serverKey: "servers"
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
//#endregion
|
|
1030
|
+
//#region src/mcp/clients/index.ts
|
|
1031
|
+
const mcpClientAdapters = [
|
|
1032
|
+
claudeAdapter,
|
|
1033
|
+
cursorAdapter,
|
|
1034
|
+
vscodeAdapter,
|
|
1035
|
+
codexAdapter
|
|
1036
|
+
];
|
|
1037
|
+
function getMcpClientAdapter(id) {
|
|
1038
|
+
const adapter = mcpClientAdapters.find((candidate) => candidate.id === id);
|
|
1039
|
+
if (!adapter) throw new Error(`Unsupported MCP client: ${id}`);
|
|
1040
|
+
return adapter;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/application/mcp-onboarding.ts
|
|
1045
|
+
function createMcpServerDefinition(pin) {
|
|
1046
|
+
const version$1 = typeof pin === "string" ? pin : pin ? version : void 0;
|
|
1047
|
+
return {
|
|
1048
|
+
command: "npx",
|
|
1049
|
+
args: [
|
|
1050
|
+
"-y",
|
|
1051
|
+
version$1 ? `${name}@${version$1}` : name,
|
|
1052
|
+
"mcp"
|
|
1053
|
+
]
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function createDetectContext(options = {}) {
|
|
1057
|
+
return {
|
|
1058
|
+
cwd: resolve(options.cwd ?? process.cwd()),
|
|
1059
|
+
homeDir: options.homeDir ?? homedir(),
|
|
1060
|
+
env: options.env ?? process.env,
|
|
1061
|
+
platform: options.platform ?? process.platform
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function createPlanContext(options = {}) {
|
|
1065
|
+
return {
|
|
1066
|
+
...createDetectContext(options),
|
|
1067
|
+
scope: options.scope ?? "project",
|
|
1068
|
+
server: createMcpServerDefinition(options.pin)
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
async function detectMcpClients(options = {}) {
|
|
1072
|
+
const context = createDetectContext(options);
|
|
1073
|
+
return Promise.all(mcpClientAdapters.map((adapter) => adapter.detect(context)));
|
|
1074
|
+
}
|
|
1075
|
+
async function resolveMcpAdapters(client, options = {}) {
|
|
1076
|
+
if (client === "all") return mcpClientAdapters.filter((adapter) => adapter.supportedScopes.includes(options.scope ?? "project"));
|
|
1077
|
+
if (client !== "auto") return [getMcpClientAdapter(client)];
|
|
1078
|
+
const detected = (await detectMcpClients(options)).filter((item) => item.installed && item.supportedScopes.includes(options.scope ?? "project")).map((item) => getMcpClientAdapter(item.client));
|
|
1079
|
+
if (detected.length === 0) throw new Error("No supported MCP client detected. Pass --client explicitly.");
|
|
1080
|
+
if (detected.length > 1) throw new Error(`Multiple MCP clients detected (${detected.map((item) => item.id).join(", ")}). Pass --client or --client all.`);
|
|
1081
|
+
return detected;
|
|
1082
|
+
}
|
|
1083
|
+
async function inspectMcpClients(client, options = {}) {
|
|
1084
|
+
const adapters = await resolveMcpAdapters(client, options);
|
|
1085
|
+
const context = createPlanContext(options);
|
|
1086
|
+
return Promise.all(adapters.map((adapter) => adapter.inspect(context)));
|
|
1087
|
+
}
|
|
1088
|
+
async function planMcpInstall(client, options = {}) {
|
|
1089
|
+
const adapters = await resolveMcpAdapters(client, options);
|
|
1090
|
+
const context = createPlanContext(options);
|
|
1091
|
+
return Promise.all(adapters.map((adapter) => adapter.planInstall(context)));
|
|
1092
|
+
}
|
|
1093
|
+
async function planMcpRemove(client, options = {}) {
|
|
1094
|
+
const adapters = await resolveMcpAdapters(client, options);
|
|
1095
|
+
const context = createPlanContext(options);
|
|
1096
|
+
return Promise.all(adapters.map((adapter) => adapter.planRemove(context)));
|
|
1097
|
+
}
|
|
1098
|
+
async function printMcpConfig(client, options = {}) {
|
|
1099
|
+
return getMcpClientAdapter(client).print(createPlanContext(options));
|
|
1100
|
+
}
|
|
1101
|
+
function toPublicClientConfigState(state) {
|
|
1102
|
+
const secretValues = Object.values(state.server?.env ?? {});
|
|
1103
|
+
return {
|
|
1104
|
+
...state,
|
|
1105
|
+
server: void 0,
|
|
1106
|
+
problem: state.problem ? redactSensitiveText(state.problem, secretValues) : void 0
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/application/agent-onboarding.ts
|
|
1112
|
+
const INSTRUCTIONS_START = "<!-- open-wot agent instructions start -->";
|
|
1113
|
+
const INSTRUCTIONS_END = "<!-- open-wot agent instructions end -->";
|
|
1114
|
+
function findManagedInstructionsRange(content) {
|
|
1115
|
+
const start = content.indexOf(INSTRUCTIONS_START);
|
|
1116
|
+
const endMarker = content.indexOf(INSTRUCTIONS_END);
|
|
1117
|
+
if (start === -1 && endMarker === -1) return void 0;
|
|
1118
|
+
const hasDuplicateStart = start !== -1 && content.includes(INSTRUCTIONS_START, start + 42);
|
|
1119
|
+
const hasDuplicateEnd = endMarker !== -1 && content.includes(INSTRUCTIONS_END, endMarker + 40);
|
|
1120
|
+
if (start === -1 || endMarker === -1 || endMarker < start || hasDuplicateStart || hasDuplicateEnd) throw new Error("Malformed open-wot Agent instructions markers; expected exactly one ordered start/end pair");
|
|
1121
|
+
return {
|
|
1122
|
+
start,
|
|
1123
|
+
end: endMarker + 40
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
async function exists(path) {
|
|
1127
|
+
return access(path).then(() => true).catch(() => false);
|
|
1128
|
+
}
|
|
1129
|
+
async function readOptional(path) {
|
|
1130
|
+
try {
|
|
1131
|
+
return await readFile(path, "utf8");
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (error.code === "ENOENT") return void 0;
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function getSkillTarget(projectDir, client) {
|
|
1138
|
+
return client === "claude" ? resolve(projectDir, ".claude", "skills", "wot-ui-v2") : resolve(projectDir, ".agents", "skills", "wot-ui-v2");
|
|
1139
|
+
}
|
|
1140
|
+
function getInstructionsTarget(projectDir, client) {
|
|
1141
|
+
return resolve(projectDir, client === "claude" ? "CLAUDE.md" : "AGENTS.md");
|
|
1142
|
+
}
|
|
1143
|
+
function createAgentInstructions(client) {
|
|
1144
|
+
return [
|
|
1145
|
+
INSTRUCTIONS_START,
|
|
1146
|
+
"## Wot UI Agent Instructions",
|
|
1147
|
+
"",
|
|
1148
|
+
`Before generating or modifying wot-ui component code, read the project Skill at \`${client === "claude" ? ".claude/skills/wot-ui-v2/SKILL.md" : ".agents/skills/wot-ui-v2/SKILL.md"}\` and query the configured \`wot-ui\` MCP server for version-accurate APIs and examples.`,
|
|
1149
|
+
"",
|
|
1150
|
+
INSTRUCTIONS_END
|
|
1151
|
+
].join("\n");
|
|
1152
|
+
}
|
|
1153
|
+
function updateManagedInstructions(content, block) {
|
|
1154
|
+
const range = findManagedInstructionsRange(content);
|
|
1155
|
+
if (range) return `${content.slice(0, range.start)}${block}${content.slice(range.end)}`;
|
|
1156
|
+
if (!content) return `${block}\n`;
|
|
1157
|
+
return `${content}${content.endsWith("\n\n") ? "" : content.endsWith("\n") ? "\n" : "\n\n"}${block}\n`;
|
|
1158
|
+
}
|
|
1159
|
+
function removeManagedInstructions(content) {
|
|
1160
|
+
const range = findManagedInstructionsRange(content);
|
|
1161
|
+
if (!range) return content;
|
|
1162
|
+
return `${content.slice(0, range.start)}${content.slice(range.end)}`;
|
|
1163
|
+
}
|
|
1164
|
+
function resolveBundledWotSkill() {
|
|
1165
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1166
|
+
const found = [
|
|
1167
|
+
resolve(here, "../../skills/wot-ui-v2"),
|
|
1168
|
+
resolve(here, "../skills/wot-ui-v2"),
|
|
1169
|
+
resolve(process.cwd(), "skills/wot-ui-v2")
|
|
1170
|
+
].find((path) => existsSync(join(path, "SKILL.md")));
|
|
1171
|
+
if (!found) throw new Error("Bundled wot-ui-v2 Skill not found");
|
|
1172
|
+
return found;
|
|
1173
|
+
}
|
|
1174
|
+
async function listFiles(root, current = root) {
|
|
1175
|
+
if (!await exists(current)) return [];
|
|
1176
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
1177
|
+
const files = [];
|
|
1178
|
+
for (const entry of entries) {
|
|
1179
|
+
if (entry.name === ".DS_Store") continue;
|
|
1180
|
+
const path = join(current, entry.name);
|
|
1181
|
+
if (entry.isDirectory()) files.push(...await listFiles(root, path));
|
|
1182
|
+
else if (entry.isFile()) files.push(relative(root, path));
|
|
1183
|
+
}
|
|
1184
|
+
return files.sort();
|
|
1185
|
+
}
|
|
1186
|
+
async function planSkillInstall(projectDir, client) {
|
|
1187
|
+
const source = resolveBundledWotSkill();
|
|
1188
|
+
const target = getSkillTarget(projectDir, client);
|
|
1189
|
+
const changes = [];
|
|
1190
|
+
const warnings = [];
|
|
1191
|
+
for (const file of await listFiles(source)) {
|
|
1192
|
+
const sourcePath = join(source, file);
|
|
1193
|
+
const targetPath = join(target, file);
|
|
1194
|
+
const before = await readOptional(targetPath);
|
|
1195
|
+
const after = await readFile(sourcePath, "utf8");
|
|
1196
|
+
if (before !== void 0 && before !== after) {
|
|
1197
|
+
warnings.push(`Modified Skill file left unchanged: ${targetPath}`);
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (before !== after) changes.push({
|
|
1201
|
+
type: "write-file",
|
|
1202
|
+
path: targetPath,
|
|
1203
|
+
allowedRoot: projectDir,
|
|
1204
|
+
before,
|
|
1205
|
+
after,
|
|
1206
|
+
reason: `Install bundled Skill file ${file}`,
|
|
1207
|
+
preview: [before === void 0 ? "+ install bundled Skill file" : "~ refresh bundled Skill file"]
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
id: `${client}-skill-install`,
|
|
1212
|
+
summary: `Install wot-ui-v2 Skill for ${client}`,
|
|
1213
|
+
changes,
|
|
1214
|
+
warnings,
|
|
1215
|
+
requiresConfirmation: changes.length > 0
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async function planSkillRemove(projectDir, client) {
|
|
1219
|
+
const source = resolveBundledWotSkill();
|
|
1220
|
+
const target = getSkillTarget(projectDir, client);
|
|
1221
|
+
const changes = [];
|
|
1222
|
+
const warnings = [];
|
|
1223
|
+
for (const file of await listFiles(source)) {
|
|
1224
|
+
const path = join(target, file);
|
|
1225
|
+
const before = await readOptional(path);
|
|
1226
|
+
if (before === void 0) continue;
|
|
1227
|
+
if (before !== await readFile(join(source, file), "utf8")) {
|
|
1228
|
+
warnings.push(`Modified Skill file left unchanged: ${path}`);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
changes.push({
|
|
1232
|
+
type: "delete-file",
|
|
1233
|
+
path,
|
|
1234
|
+
allowedRoot: projectDir,
|
|
1235
|
+
before,
|
|
1236
|
+
reason: `Remove installed Skill file ${file}`,
|
|
1237
|
+
preview: ["- remove bundled Skill file"]
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
id: `${client}-skill-remove`,
|
|
1242
|
+
summary: `Remove wot-ui-v2 Skill for ${client}`,
|
|
1243
|
+
changes,
|
|
1244
|
+
warnings,
|
|
1245
|
+
requiresConfirmation: changes.length > 0
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
async function planInstructions(projectDir, client, remove) {
|
|
1249
|
+
const path = getInstructionsTarget(projectDir, client);
|
|
1250
|
+
const before = await readOptional(path);
|
|
1251
|
+
const after = remove ? removeManagedInstructions(before ?? "") : updateManagedInstructions(before ?? "", createAgentInstructions(client));
|
|
1252
|
+
return {
|
|
1253
|
+
id: `${client}-instructions-${remove ? "remove" : "install"}`,
|
|
1254
|
+
summary: `${remove ? "Remove" : "Install"} managed Agent instructions for ${client}`,
|
|
1255
|
+
changes: (before ?? "") === after ? [] : [{
|
|
1256
|
+
type: "write-file",
|
|
1257
|
+
path,
|
|
1258
|
+
allowedRoot: projectDir,
|
|
1259
|
+
before,
|
|
1260
|
+
after,
|
|
1261
|
+
reason: `${remove ? "Remove" : "Update"} only the open-wot managed instructions block`,
|
|
1262
|
+
preview: [remove ? "- open-wot managed instructions block" : "+ open-wot managed instructions block"]
|
|
1263
|
+
}],
|
|
1264
|
+
warnings: [],
|
|
1265
|
+
requiresConfirmation: (before ?? "") !== after
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
async function planAgentInit(options) {
|
|
1269
|
+
const context = createDetectContext(options);
|
|
1270
|
+
const plans = [];
|
|
1271
|
+
if (options.capabilities.includes("mcp")) plans.push(...await planMcpInstall(options.client, options));
|
|
1272
|
+
if (options.capabilities.includes("skill")) plans.push(await planSkillInstall(context.cwd, options.client));
|
|
1273
|
+
if (options.capabilities.includes("instructions")) plans.push(await planInstructions(context.cwd, options.client, false));
|
|
1274
|
+
return mergeChangePlans(`Initialize wot-ui Agent integration for ${options.client}`, plans);
|
|
1275
|
+
}
|
|
1276
|
+
async function planAgentRemove(options) {
|
|
1277
|
+
const context = createDetectContext(options);
|
|
1278
|
+
const plans = [];
|
|
1279
|
+
if (options.capabilities.includes("mcp")) plans.push(...await planMcpRemove(options.client, options));
|
|
1280
|
+
if (options.capabilities.includes("skill")) plans.push(await planSkillRemove(context.cwd, options.client));
|
|
1281
|
+
if (options.capabilities.includes("instructions")) plans.push(await planInstructions(context.cwd, options.client, true));
|
|
1282
|
+
return mergeChangePlans(`Remove wot-ui Agent integration for ${options.client}`, plans);
|
|
1283
|
+
}
|
|
1284
|
+
async function inspectAgentSkill(projectDir, client) {
|
|
1285
|
+
const source = resolveBundledWotSkill();
|
|
1286
|
+
const target = getSkillTarget(projectDir, client);
|
|
1287
|
+
const skillInstalled = await exists(join(target, "SKILL.md"));
|
|
1288
|
+
let skillMatches = skillInstalled;
|
|
1289
|
+
for (const file of await listFiles(source)) if (await readOptional(join(target, file)) !== await readFile(join(source, file), "utf8")) {
|
|
1290
|
+
skillMatches = false;
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
installed: skillInstalled,
|
|
1295
|
+
matches: skillMatches
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
async function inspectAgentInstructions(projectDir, client) {
|
|
1299
|
+
const instructions = await readOptional(getInstructionsTarget(projectDir, client));
|
|
1300
|
+
if (!instructions) return { installed: false };
|
|
1301
|
+
try {
|
|
1302
|
+
return { installed: Boolean(findManagedInstructionsRange(instructions)) };
|
|
1303
|
+
} catch {
|
|
1304
|
+
return { installed: false };
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
//#endregion
|
|
1309
|
+
//#region src/application/mcp-handshake.ts
|
|
1310
|
+
function resolveMcpSpawnCommand(command, platform) {
|
|
1311
|
+
const executable = platform === "win32" && !/\.(?:exe|cmd|bat|com)$/i.test(command) ? `${command}.cmd` : command;
|
|
1312
|
+
return {
|
|
1313
|
+
executable,
|
|
1314
|
+
useShell: platform === "win32" && /\.(?:cmd|bat)$/i.test(executable)
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
async function waitForChildClose(child, timeoutMs) {
|
|
1318
|
+
if (child.exitCode !== null || child.signalCode !== null) return true;
|
|
1319
|
+
return new Promise((resolve$1) => {
|
|
1320
|
+
let timer;
|
|
1321
|
+
const onClose = () => {
|
|
1322
|
+
clearTimeout(timer);
|
|
1323
|
+
resolve$1(true);
|
|
1324
|
+
};
|
|
1325
|
+
timer = setTimeout(() => {
|
|
1326
|
+
child.off("close", onClose);
|
|
1327
|
+
resolve$1(false);
|
|
1328
|
+
}, timeoutMs);
|
|
1329
|
+
timer.unref();
|
|
1330
|
+
child.once("close", onClose);
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
async function terminate(child, platform, deadline) {
|
|
1334
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
1335
|
+
if (platform === "win32" && child.pid) spawn("taskkill", [
|
|
1336
|
+
"/pid",
|
|
1337
|
+
String(child.pid),
|
|
1338
|
+
"/T",
|
|
1339
|
+
"/F"
|
|
1340
|
+
], {
|
|
1341
|
+
windowsHide: true,
|
|
1342
|
+
stdio: "ignore"
|
|
1343
|
+
}).once("error", () => child.kill("SIGTERM"));
|
|
1344
|
+
else child.kill("SIGTERM");
|
|
1345
|
+
if (await waitForChildClose(child, Math.min(1e3, Math.max(0, deadline - Date.now())))) return;
|
|
1346
|
+
child.kill("SIGKILL");
|
|
1347
|
+
const forceWaitMs = Math.min(500, Math.max(0, deadline - Date.now()));
|
|
1348
|
+
if (forceWaitMs > 0) await waitForChildClose(child, forceWaitMs);
|
|
1349
|
+
}
|
|
1350
|
+
async function verifyMcpHandshake(server, options = {}) {
|
|
1351
|
+
const startedAt = Date.now();
|
|
1352
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
1353
|
+
const deadline = startedAt + timeoutMs;
|
|
1354
|
+
const platform = options.platform ?? process.platform;
|
|
1355
|
+
const spawnCommand = resolveMcpSpawnCommand(server.command, platform);
|
|
1356
|
+
const child = spawn(spawnCommand.executable, server.args, {
|
|
1357
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1358
|
+
env: {
|
|
1359
|
+
...process.env,
|
|
1360
|
+
...options.env,
|
|
1361
|
+
...server.env,
|
|
1362
|
+
WOT_DISABLE_UPDATE_CHECK: "1"
|
|
1363
|
+
},
|
|
1364
|
+
stdio: [
|
|
1365
|
+
"pipe",
|
|
1366
|
+
"pipe",
|
|
1367
|
+
"pipe"
|
|
1368
|
+
],
|
|
1369
|
+
windowsHide: true,
|
|
1370
|
+
shell: spawnCommand.useShell
|
|
1371
|
+
});
|
|
1372
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1373
|
+
const readBuffer = new ReadBuffer();
|
|
1374
|
+
let stderr = "";
|
|
1375
|
+
let nextId = 1;
|
|
1376
|
+
const failPending = (error) => {
|
|
1377
|
+
for (const request of pending.values()) request.reject(error);
|
|
1378
|
+
pending.clear();
|
|
1379
|
+
};
|
|
1380
|
+
child.stdout.on("data", (chunk) => {
|
|
1381
|
+
try {
|
|
1382
|
+
readBuffer.append(chunk);
|
|
1383
|
+
while (true) {
|
|
1384
|
+
const message = readBuffer.readMessage();
|
|
1385
|
+
if (message === null) break;
|
|
1386
|
+
if ("id" in message && typeof message.id === "number") {
|
|
1387
|
+
const request = pending.get(message.id);
|
|
1388
|
+
if (!request) continue;
|
|
1389
|
+
pending.delete(message.id);
|
|
1390
|
+
if ("error" in message) request.reject(/* @__PURE__ */ new Error(`${message.error.code}: ${message.error.message}`));
|
|
1391
|
+
else if ("result" in message) request.resolve(message.result);
|
|
1392
|
+
else request.reject(/* @__PURE__ */ new Error("Unexpected JSON-RPC request from MCP server"));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
failPending(error instanceof Error ? error : new Error(String(error)));
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
child.stderr.on("data", (chunk) => {
|
|
1400
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-4e3);
|
|
1401
|
+
});
|
|
1402
|
+
child.once("error", failPending);
|
|
1403
|
+
child.stdin.once("error", failPending);
|
|
1404
|
+
child.once("exit", (code, signal) => {
|
|
1405
|
+
failPending(/* @__PURE__ */ new Error(`MCP server exited before verification completed (${signal ?? code ?? "unknown"})`));
|
|
1406
|
+
});
|
|
1407
|
+
const sendRequest = (method, params) => {
|
|
1408
|
+
const id = nextId++;
|
|
1409
|
+
return new Promise((resolve$1, reject) => {
|
|
1410
|
+
pending.set(id, {
|
|
1411
|
+
resolve: resolve$1,
|
|
1412
|
+
reject
|
|
1413
|
+
});
|
|
1414
|
+
child.stdin.write(serializeMessage({
|
|
1415
|
+
jsonrpc: "2.0",
|
|
1416
|
+
id,
|
|
1417
|
+
method,
|
|
1418
|
+
params
|
|
1419
|
+
}));
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
const timeout = new Promise((_, reject) => {
|
|
1423
|
+
setTimeout(() => reject(/* @__PURE__ */ new Error(`MCP handshake timed out after ${timeoutMs}ms`)), timeoutMs).unref();
|
|
1424
|
+
});
|
|
1425
|
+
try {
|
|
1426
|
+
const initialize = await Promise.race([sendRequest("initialize", {
|
|
1427
|
+
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
1428
|
+
capabilities: {},
|
|
1429
|
+
clientInfo: {
|
|
1430
|
+
name: "open-wot-doctor",
|
|
1431
|
+
version: "1.0.0"
|
|
1432
|
+
}
|
|
1433
|
+
}), timeout]);
|
|
1434
|
+
child.stdin.write(serializeMessage({
|
|
1435
|
+
jsonrpc: "2.0",
|
|
1436
|
+
method: "notifications/initialized",
|
|
1437
|
+
params: {}
|
|
1438
|
+
}));
|
|
1439
|
+
const toolResult = await Promise.race([sendRequest("tools/list", {}), timeout]);
|
|
1440
|
+
const tools = Array.isArray(toolResult.tools) ? toolResult.tools.flatMap((tool) => {
|
|
1441
|
+
if (!tool || typeof tool !== "object") return [];
|
|
1442
|
+
const name$1 = tool.name;
|
|
1443
|
+
return typeof name$1 === "string" ? [name$1] : [];
|
|
1444
|
+
}) : [];
|
|
1445
|
+
const serverInfo = initialize.serverInfo && typeof initialize.serverInfo === "object" ? initialize.serverInfo : {};
|
|
1446
|
+
return {
|
|
1447
|
+
ok: true,
|
|
1448
|
+
protocolVersion: typeof initialize.protocolVersion === "string" ? initialize.protocolVersion : void 0,
|
|
1449
|
+
serverName: typeof serverInfo.name === "string" ? serverInfo.name : void 0,
|
|
1450
|
+
serverVersion: typeof serverInfo.version === "string" ? serverInfo.version : void 0,
|
|
1451
|
+
tools,
|
|
1452
|
+
durationMs: Date.now() - startedAt,
|
|
1453
|
+
...stderr.trim() ? { stderr: stderr.trim() } : {}
|
|
1454
|
+
};
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
return {
|
|
1457
|
+
ok: false,
|
|
1458
|
+
tools: [],
|
|
1459
|
+
durationMs: Date.now() - startedAt,
|
|
1460
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1461
|
+
...stderr.trim() ? { stderr: stderr.trim() } : {}
|
|
1462
|
+
};
|
|
1463
|
+
} finally {
|
|
1464
|
+
failPending(/* @__PURE__ */ new Error("MCP verification finished"));
|
|
1465
|
+
await terminate(child, platform, deadline);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/application/mcp-doctor.ts
|
|
1471
|
+
function validateMcpHandshake(result) {
|
|
1472
|
+
if (!result.ok) return {
|
|
1473
|
+
status: "fail",
|
|
1474
|
+
message: result.error ? redactSensitiveText(result.error) : "MCP handshake failed"
|
|
1475
|
+
};
|
|
1476
|
+
if (result.serverName !== "wot-ui") return {
|
|
1477
|
+
status: "fail",
|
|
1478
|
+
message: `Expected MCP server name "wot-ui", received "${result.serverName ?? "<missing>"}"`
|
|
1479
|
+
};
|
|
1480
|
+
const missingTools = REQUIRED_WOT_MCP_TOOLS.filter((tool) => !result.tools.includes(tool));
|
|
1481
|
+
if (missingTools.length > 0) return {
|
|
1482
|
+
status: "fail",
|
|
1483
|
+
message: `MCP server is missing required tools: ${missingTools.join(", ")}`
|
|
1484
|
+
};
|
|
1485
|
+
return { status: "pass" };
|
|
1486
|
+
}
|
|
1487
|
+
function overallStatus(config, handshake, registration) {
|
|
1488
|
+
if (config === "fail" || handshake === "fail" || registration.status === "failed") return "failed";
|
|
1489
|
+
if (registration.status === "pending") return "action-required";
|
|
1490
|
+
if (registration.status === "ready") return "ready";
|
|
1491
|
+
return "server-ready";
|
|
1492
|
+
}
|
|
1493
|
+
async function doctorMcpClients(client, options = {}) {
|
|
1494
|
+
const deadline = Date.now() + (options.timeoutMs ?? 1e4);
|
|
1495
|
+
const adapters = await resolveMcpAdapters(client, options);
|
|
1496
|
+
const context = createPlanContext(options);
|
|
1497
|
+
const states = await Promise.all(adapters.map((adapter) => adapter.inspect(context)));
|
|
1498
|
+
return Promise.all(adapters.map(async (adapter, index) => {
|
|
1499
|
+
const state = states[index];
|
|
1500
|
+
const configStatus = state.matches ? "pass" : "fail";
|
|
1501
|
+
if (!state.matches || !state.server) return {
|
|
1502
|
+
client: adapter.id,
|
|
1503
|
+
displayName: adapter.displayName,
|
|
1504
|
+
overall: "failed",
|
|
1505
|
+
config: {
|
|
1506
|
+
status: configStatus,
|
|
1507
|
+
state
|
|
1508
|
+
},
|
|
1509
|
+
handshake: {
|
|
1510
|
+
status: "skipped",
|
|
1511
|
+
message: state.problem ?? "wot-ui MCP configuration is missing or differs from the expected definition"
|
|
1512
|
+
},
|
|
1513
|
+
clientRegistration: {
|
|
1514
|
+
status: "skipped",
|
|
1515
|
+
message: "Client registration was not checked because the configuration is invalid or missing"
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
const handshakeTimeoutMs = Math.max(1, deadline - Date.now());
|
|
1519
|
+
const handshakeResult = await verifyMcpHandshake(state.server, {
|
|
1520
|
+
cwd: context.cwd,
|
|
1521
|
+
env: context.env,
|
|
1522
|
+
platform: context.platform,
|
|
1523
|
+
timeoutMs: handshakeTimeoutMs
|
|
1524
|
+
});
|
|
1525
|
+
const handshake = validateMcpHandshake(handshakeResult);
|
|
1526
|
+
const registrationTimeoutMs = deadline - Date.now();
|
|
1527
|
+
const registration = registrationTimeoutMs > 0 ? await getMcpClientAdapter(adapter.id).verifyRegistration(context, state, { timeoutMs: registrationTimeoutMs }) : {
|
|
1528
|
+
status: "failed",
|
|
1529
|
+
message: `Doctor timed out after ${options.timeoutMs ?? 1e4}ms before client registration could be verified`,
|
|
1530
|
+
action: "Retry with a larger --timeout value if the MCP server needs more time to start."
|
|
1531
|
+
};
|
|
1532
|
+
return {
|
|
1533
|
+
client: adapter.id,
|
|
1534
|
+
displayName: adapter.displayName,
|
|
1535
|
+
overall: overallStatus(configStatus, handshake.status, registration),
|
|
1536
|
+
config: {
|
|
1537
|
+
status: configStatus,
|
|
1538
|
+
state
|
|
1539
|
+
},
|
|
1540
|
+
handshake: {
|
|
1541
|
+
...handshake,
|
|
1542
|
+
result: handshakeResult
|
|
1543
|
+
},
|
|
1544
|
+
clientRegistration: registration
|
|
1545
|
+
};
|
|
1546
|
+
}));
|
|
1547
|
+
}
|
|
1548
|
+
function formatMcpDoctorReport(report) {
|
|
1549
|
+
const secretValues = Object.values(report.config.state.server?.env ?? {});
|
|
1550
|
+
const handshakeDetail = redactSensitiveText(report.handshake.result?.ok ? `${report.handshake.result.tools.length} tools, ${report.handshake.result.durationMs}ms${report.handshake.message ? `; ${report.handshake.message}` : ""}` : report.handshake.message ?? report.handshake.result?.error ?? "not run", secretValues);
|
|
1551
|
+
return [
|
|
1552
|
+
`${report.overall} ${report.displayName} (${report.client})`,
|
|
1553
|
+
` ${report.config.status} config: ${report.config.state.path}`,
|
|
1554
|
+
` ${report.handshake.status} handshake: ${handshakeDetail}`,
|
|
1555
|
+
` ${report.clientRegistration.status} client: ${redactSensitiveText(report.clientRegistration.message, secretValues)}`,
|
|
1556
|
+
...report.clientRegistration.action ? [` action: ${report.clientRegistration.action}`] : []
|
|
1557
|
+
].join("\n");
|
|
1558
|
+
}
|
|
1559
|
+
function mcpDoctorExitCode(reports) {
|
|
1560
|
+
if (reports.some((report) => report.overall === "failed")) return 1;
|
|
1561
|
+
if (reports.some((report) => report.overall === "action-required")) return 2;
|
|
1562
|
+
return 0;
|
|
1563
|
+
}
|
|
1564
|
+
function toPublicMcpDoctorReport(report) {
|
|
1565
|
+
const secretValues = Object.values(report.config.state.server?.env ?? {});
|
|
1566
|
+
const handshakeResult = report.handshake.result ? {
|
|
1567
|
+
...report.handshake.result,
|
|
1568
|
+
error: report.handshake.result.error ? redactSensitiveText(report.handshake.result.error, secretValues) : void 0,
|
|
1569
|
+
stderr: void 0
|
|
1570
|
+
} : void 0;
|
|
1571
|
+
return {
|
|
1572
|
+
...report,
|
|
1573
|
+
config: {
|
|
1574
|
+
...report.config,
|
|
1575
|
+
state: {
|
|
1576
|
+
...report.config.state,
|
|
1577
|
+
server: void 0,
|
|
1578
|
+
problem: report.config.state.problem ? redactSensitiveText(report.config.state.problem, secretValues) : void 0
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
handshake: {
|
|
1582
|
+
...report.handshake,
|
|
1583
|
+
message: report.handshake.message ? redactSensitiveText(report.handshake.message, secretValues) : void 0,
|
|
1584
|
+
result: handshakeResult
|
|
1585
|
+
},
|
|
1586
|
+
clientRegistration: {
|
|
1587
|
+
...report.clientRegistration,
|
|
1588
|
+
message: redactSensitiveText(report.clientRegistration.message, secretValues)
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
//#endregion
|
|
8
1594
|
//#region src/utils/output.ts
|
|
9
1595
|
function writeStdout(message) {
|
|
10
1596
|
process.stdout.write(message);
|
|
@@ -37,6 +1623,12 @@ function printError(message, format) {
|
|
|
37
1623
|
});
|
|
38
1624
|
else console.error(formatLogMessage("error", message));
|
|
39
1625
|
}
|
|
1626
|
+
function parsePositiveIntegerOption(value, defaultValue, optionName) {
|
|
1627
|
+
if (value === void 0) return defaultValue;
|
|
1628
|
+
const parsed = Number(value);
|
|
1629
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`Invalid ${optionName} value "${value}". Expected a positive integer.`);
|
|
1630
|
+
return parsed;
|
|
1631
|
+
}
|
|
40
1632
|
function getComponentLabel(component) {
|
|
41
1633
|
return `${component.name} ${component.nameZh}`;
|
|
42
1634
|
}
|
|
@@ -51,6 +1643,193 @@ function formatCssVars(cssVars) {
|
|
|
51
1643
|
}));
|
|
52
1644
|
}
|
|
53
1645
|
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region src/commands/agent.ts
|
|
1648
|
+
function parseCapabilities(value) {
|
|
1649
|
+
const capabilities = (value ?? "mcp,skill,instructions").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1650
|
+
const supported = [
|
|
1651
|
+
"mcp",
|
|
1652
|
+
"skill",
|
|
1653
|
+
"instructions"
|
|
1654
|
+
];
|
|
1655
|
+
if (capabilities.length === 0 || capabilities.some((item) => !supported.includes(item))) throw new Error("Unsupported --with value. Use mcp, skill, instructions, or a comma-separated combination.");
|
|
1656
|
+
return [...new Set(capabilities)];
|
|
1657
|
+
}
|
|
1658
|
+
function parseScope$1(value) {
|
|
1659
|
+
const scope = value ?? "project";
|
|
1660
|
+
if (!isMcpScope(scope)) throw new Error(`Unsupported scope "${scope}". Use project or user.`);
|
|
1661
|
+
return scope;
|
|
1662
|
+
}
|
|
1663
|
+
function parseClient$1(value) {
|
|
1664
|
+
const client = value ?? "auto";
|
|
1665
|
+
if (client === "auto" || client === "all" || isMcpClientId(client)) return client;
|
|
1666
|
+
throw new Error(`Unsupported Agent client "${client}".`);
|
|
1667
|
+
}
|
|
1668
|
+
function addOptions(command, defaultClient = "auto") {
|
|
1669
|
+
return command.option("--client <client>", "Agent client: auto, all, claude, cursor, vscode, codex", defaultClient).option("--scope <scope>", "MCP configuration scope: project or user", "project").option("--with <capabilities>", "capabilities: mcp,skill,instructions", "mcp,skill,instructions").option("--cwd <directory>", "project directory", process.cwd()).option("--format <format>", "output format: text or json", "text").option("--pin [version]", "pin @wot-ui/cli to the current or specified version");
|
|
1670
|
+
}
|
|
1671
|
+
async function runSafely$1(options, action) {
|
|
1672
|
+
try {
|
|
1673
|
+
await action();
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
printError(error instanceof Error ? error.message : String(error), options.format === "json" ? "json" : "text");
|
|
1676
|
+
process.exitCode = 1;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
async function resolveClients(options) {
|
|
1680
|
+
return (await resolveMcpAdapters(parseClient$1(options.client), {
|
|
1681
|
+
cwd: options.cwd,
|
|
1682
|
+
scope: parseScope$1(options.scope),
|
|
1683
|
+
pin: options.pin
|
|
1684
|
+
})).map((adapter) => adapter.id);
|
|
1685
|
+
}
|
|
1686
|
+
async function inspectCapabilities(client, capabilities, options) {
|
|
1687
|
+
const projectDir = options.cwd ?? process.cwd();
|
|
1688
|
+
const [mcpStates, skill, instructions] = await Promise.all([
|
|
1689
|
+
capabilities.includes("mcp") ? inspectMcpClients(client, {
|
|
1690
|
+
cwd: options.cwd,
|
|
1691
|
+
scope: parseScope$1(options.scope),
|
|
1692
|
+
pin: options.pin
|
|
1693
|
+
}) : Promise.resolve([]),
|
|
1694
|
+
capabilities.includes("skill") ? inspectAgentSkill(projectDir, client) : Promise.resolve(void 0),
|
|
1695
|
+
capabilities.includes("instructions") ? inspectAgentInstructions(projectDir, client) : Promise.resolve(void 0)
|
|
1696
|
+
]);
|
|
1697
|
+
const mcp = mcpStates[0];
|
|
1698
|
+
return {
|
|
1699
|
+
client,
|
|
1700
|
+
capabilities: {
|
|
1701
|
+
mcp: capabilities.includes("mcp") ? {
|
|
1702
|
+
status: mcp?.matches ? "pass" : "fail",
|
|
1703
|
+
state: mcp ? toPublicClientConfigState(mcp) : void 0
|
|
1704
|
+
} : { status: "skipped" },
|
|
1705
|
+
skill: capabilities.includes("skill") ? {
|
|
1706
|
+
status: skill?.matches ? "pass" : "fail",
|
|
1707
|
+
installed: skill?.installed,
|
|
1708
|
+
matches: skill?.matches
|
|
1709
|
+
} : { status: "skipped" },
|
|
1710
|
+
instructions: capabilities.includes("instructions") ? {
|
|
1711
|
+
status: instructions?.installed ? "pass" : "fail",
|
|
1712
|
+
installed: instructions?.installed
|
|
1713
|
+
} : { status: "skipped" }
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function reportFailed(report) {
|
|
1718
|
+
return Object.values(report.capabilities).some((capability) => capability.status === "fail");
|
|
1719
|
+
}
|
|
1720
|
+
function reportNeedsAction(report) {
|
|
1721
|
+
return Object.values(report.capabilities).some((capability) => capability.status === "action-required");
|
|
1722
|
+
}
|
|
1723
|
+
function reportHasWarning(report) {
|
|
1724
|
+
return Object.values(report.capabilities).some((capability) => capability.status === "warning");
|
|
1725
|
+
}
|
|
1726
|
+
function formatCapabilityReport(report) {
|
|
1727
|
+
const state = reportFailed(report) ? "incomplete" : reportNeedsAction(report) ? "action-required" : reportHasWarning(report) ? "warning" : "ok";
|
|
1728
|
+
const capabilities = Object.entries(report.capabilities).map(([name$1, capability]) => `${name$1}=${capability.status}`).join(" ");
|
|
1729
|
+
return `${state} ${report.client}: ${capabilities}`;
|
|
1730
|
+
}
|
|
1731
|
+
function registerAgentCommand(program) {
|
|
1732
|
+
const agent = program.command("agent").description("Connect AI agents to wot-ui MCP and Skills");
|
|
1733
|
+
addOptions(agent.command("init").description("Initialize MCP, Skill, and Agent instructions")).option("--dry-run", "preview changes without writing files").option("-y, --yes", "apply changes without prompting").action(async (options) => runSafely$1(options, async () => {
|
|
1734
|
+
const clients = await resolveClients(options);
|
|
1735
|
+
const capabilities = parseCapabilities(options.with);
|
|
1736
|
+
const plan = mergeChangePlans("Initialize wot-ui Agent integration", await Promise.all(clients.map((client) => planAgentInit({
|
|
1737
|
+
client,
|
|
1738
|
+
capabilities,
|
|
1739
|
+
cwd: options.cwd,
|
|
1740
|
+
scope: parseScope$1(options.scope),
|
|
1741
|
+
pin: options.pin
|
|
1742
|
+
}))));
|
|
1743
|
+
if (!options.dryRun) {
|
|
1744
|
+
if (!await confirmChangePlan(plan, {
|
|
1745
|
+
yes: options.yes,
|
|
1746
|
+
cwd: options.cwd
|
|
1747
|
+
})) {
|
|
1748
|
+
writeLine("Cancelled.");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
await applyChangePlan(plan);
|
|
1752
|
+
}
|
|
1753
|
+
if (options.format === "json") writeJson({
|
|
1754
|
+
dryRun: Boolean(options.dryRun),
|
|
1755
|
+
plan: toPublicChangePlan(plan, options.cwd)
|
|
1756
|
+
});
|
|
1757
|
+
else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
|
|
1758
|
+
}));
|
|
1759
|
+
agent.command("list").description("List supported and detected AI clients").option("--cwd <directory>", "project directory", process.cwd()).option("--format <format>", "output format: text or json", "text").action(async (options) => runSafely$1(options, async () => {
|
|
1760
|
+
const clients = await detectMcpClients({ cwd: options.cwd });
|
|
1761
|
+
if (options.format === "json") writeJson({ clients });
|
|
1762
|
+
else writeLine(clients.map((client) => `${client.installed ? "detected" : "not detected"} ${client.displayName} (${client.client})`).join("\n"));
|
|
1763
|
+
}));
|
|
1764
|
+
addOptions(agent.command("status").description("Inspect MCP, Skill, and instructions"), "all").action(async (options) => runSafely$1(options, async () => {
|
|
1765
|
+
const clients = await resolveClients(options);
|
|
1766
|
+
const capabilities = parseCapabilities(options.with);
|
|
1767
|
+
const reports = await Promise.all(clients.map((client) => inspectCapabilities(client, capabilities, options)));
|
|
1768
|
+
if (options.format === "json") writeJson({ clients: reports });
|
|
1769
|
+
else writeLine(reports.map(formatCapabilityReport).join("\n"));
|
|
1770
|
+
if (reports.some(reportFailed)) process.exitCode = 1;
|
|
1771
|
+
}));
|
|
1772
|
+
addOptions(agent.command("doctor").description("Validate Agent files and perform a real MCP handshake")).option("--timeout <milliseconds>", "total doctor timeout in milliseconds", "10000").action(async (options) => runSafely$1(options, async () => {
|
|
1773
|
+
const clients = await resolveClients(options);
|
|
1774
|
+
const capabilities = parseCapabilities(options.with);
|
|
1775
|
+
const timeoutMs = parsePositiveIntegerOption(options.timeout, 1e4, "--timeout");
|
|
1776
|
+
const deadline = Date.now() + timeoutMs;
|
|
1777
|
+
const reports = [];
|
|
1778
|
+
for (const client of clients) {
|
|
1779
|
+
const report = await inspectCapabilities(client, capabilities, options);
|
|
1780
|
+
const mcpCheck = report.capabilities.mcp;
|
|
1781
|
+
if (mcpCheck.status !== "skipped") {
|
|
1782
|
+
const [doctor] = await doctorMcpClients(client, {
|
|
1783
|
+
cwd: options.cwd,
|
|
1784
|
+
scope: parseScope$1(options.scope),
|
|
1785
|
+
pin: options.pin,
|
|
1786
|
+
timeoutMs: Math.max(1, deadline - Date.now())
|
|
1787
|
+
});
|
|
1788
|
+
const status = doctor.overall === "failed" ? "fail" : doctor.overall === "action-required" ? "action-required" : doctor.overall === "server-ready" ? "warning" : "pass";
|
|
1789
|
+
report.capabilities.mcp = {
|
|
1790
|
+
...mcpCheck,
|
|
1791
|
+
status,
|
|
1792
|
+
doctor: toPublicMcpDoctorReport(doctor)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
reports.push(report);
|
|
1796
|
+
}
|
|
1797
|
+
if (options.format === "json") writeJson({ clients: reports });
|
|
1798
|
+
else writeLine(reports.flatMap((report) => {
|
|
1799
|
+
const doctor = report.capabilities.mcp.doctor;
|
|
1800
|
+
return [formatCapabilityReport(report), ...doctor ? [formatMcpDoctorReport(doctor)] : []];
|
|
1801
|
+
}).join("\n"));
|
|
1802
|
+
if (reports.some(reportFailed)) process.exitCode = 1;
|
|
1803
|
+
else if (reports.some(reportNeedsAction)) process.exitCode = 2;
|
|
1804
|
+
}));
|
|
1805
|
+
addOptions(agent.command("remove").description("Remove managed MCP, Skill, and instructions")).option("--dry-run", "preview changes without writing files").option("-y, --yes", "apply changes without prompting").action(async (options) => runSafely$1(options, async () => {
|
|
1806
|
+
const clients = await resolveClients(options);
|
|
1807
|
+
const capabilities = parseCapabilities(options.with);
|
|
1808
|
+
const plan = mergeChangePlans("Remove wot-ui Agent integration", await Promise.all(clients.map((client) => planAgentRemove({
|
|
1809
|
+
client,
|
|
1810
|
+
capabilities,
|
|
1811
|
+
cwd: options.cwd,
|
|
1812
|
+
scope: parseScope$1(options.scope),
|
|
1813
|
+
pin: options.pin
|
|
1814
|
+
}))));
|
|
1815
|
+
if (!options.dryRun) {
|
|
1816
|
+
if (!await confirmChangePlan(plan, {
|
|
1817
|
+
yes: options.yes,
|
|
1818
|
+
cwd: options.cwd
|
|
1819
|
+
})) {
|
|
1820
|
+
writeLine("Cancelled.");
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
await applyChangePlan(plan);
|
|
1824
|
+
}
|
|
1825
|
+
if (options.format === "json") writeJson({
|
|
1826
|
+
dryRun: Boolean(options.dryRun),
|
|
1827
|
+
plan: toPublicChangePlan(plan, options.cwd)
|
|
1828
|
+
});
|
|
1829
|
+
else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
|
|
1830
|
+
}));
|
|
1831
|
+
}
|
|
1832
|
+
|
|
54
1833
|
//#endregion
|
|
55
1834
|
//#region src/commands/changelog.ts
|
|
56
1835
|
function registerChangelogCommand(program) {
|
|
@@ -304,10 +2083,10 @@ function registerLintCommand(program) {
|
|
|
304
2083
|
//#endregion
|
|
305
2084
|
//#region src/commands/list.ts
|
|
306
2085
|
function registerListCommand(program) {
|
|
307
|
-
addQueryOptions(program.command("list").description("List available wot-ui components")).action((options) => {
|
|
2086
|
+
addQueryOptions(program.command("list").argument("[keyword]", "Filter by component name, tag, category, or description").description("List available wot-ui components")).action((keyword, options) => {
|
|
308
2087
|
const query = normalizeQueryOptions(options);
|
|
309
2088
|
try {
|
|
310
|
-
const components = listComponents(query.version);
|
|
2089
|
+
const components = filterComponents(listComponents(query.version), keyword);
|
|
311
2090
|
if (query.format === "json") {
|
|
312
2091
|
writeJson({ components });
|
|
313
2092
|
return;
|
|
@@ -322,11 +2101,125 @@ function registerListCommand(program) {
|
|
|
322
2101
|
|
|
323
2102
|
//#endregion
|
|
324
2103
|
//#region src/commands/mcp.ts
|
|
2104
|
+
function outputFormat(value) {
|
|
2105
|
+
return value === "json" ? "json" : "text";
|
|
2106
|
+
}
|
|
2107
|
+
function parseClient(value, fallback = "auto") {
|
|
2108
|
+
const client = value ?? fallback;
|
|
2109
|
+
if (client === "auto" || client === "all" || isMcpClientId(client)) return client;
|
|
2110
|
+
throw new Error(`Unsupported MCP client "${client}". Use claude, cursor, vscode, codex, auto, or all.`);
|
|
2111
|
+
}
|
|
2112
|
+
function parseScope(value) {
|
|
2113
|
+
const scope = value ?? "project";
|
|
2114
|
+
if (!isMcpScope(scope)) throw new Error(`Unsupported scope "${scope}". Use project or user.`);
|
|
2115
|
+
return scope;
|
|
2116
|
+
}
|
|
2117
|
+
function addCommonOptions(command, defaultClient = "auto") {
|
|
2118
|
+
return command.option("--client <client>", "MCP client: auto, all, claude, cursor, vscode, codex", defaultClient).option("--scope <scope>", "configuration scope: project or user", "project").option("--cwd <directory>", "project directory", process.cwd()).option("--format <format>", "output format: text or json", "text").option("--pin [version]", "pin @wot-ui/cli to the current or specified version");
|
|
2119
|
+
}
|
|
2120
|
+
async function runSafely(options, action) {
|
|
2121
|
+
try {
|
|
2122
|
+
await action();
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
printError(error instanceof Error ? error.message : String(error), outputFormat(options.format));
|
|
2125
|
+
process.exitCode = 1;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async function startServer() {
|
|
2129
|
+
const { startMcpServer } = await import("./server-DDRLv2Z2.mjs");
|
|
2130
|
+
await startMcpServer();
|
|
2131
|
+
}
|
|
325
2132
|
function registerMcpCommand(program) {
|
|
326
|
-
program.command("mcp").description("Start the wot-ui MCP server").action(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
2133
|
+
const mcp = program.command("mcp").description("Start or manage the wot-ui MCP server").action(startServer);
|
|
2134
|
+
mcp.command("serve").description("Start the wot-ui MCP server explicitly").action(startServer);
|
|
2135
|
+
addCommonOptions(mcp.command("init").description("Configure wot-ui MCP for an AI client")).option("--dry-run", "preview changes without writing files").option("-y, --yes", "apply changes without prompting").action(async (options) => runSafely(options, async () => {
|
|
2136
|
+
const client = parseClient(options.client);
|
|
2137
|
+
const scope = parseScope(options.scope);
|
|
2138
|
+
const plan = mergeChangePlans("Initialize wot-ui MCP integration", await planMcpInstall(client, {
|
|
2139
|
+
cwd: options.cwd,
|
|
2140
|
+
scope,
|
|
2141
|
+
pin: options.pin
|
|
2142
|
+
}));
|
|
2143
|
+
if (!options.dryRun) {
|
|
2144
|
+
if (!await confirmChangePlan(plan, {
|
|
2145
|
+
yes: options.yes,
|
|
2146
|
+
cwd: options.cwd
|
|
2147
|
+
})) {
|
|
2148
|
+
writeLine("Cancelled.");
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
await applyChangePlan(plan);
|
|
2152
|
+
}
|
|
2153
|
+
if (outputFormat(options.format) === "json") writeJson({
|
|
2154
|
+
dryRun: Boolean(options.dryRun),
|
|
2155
|
+
plan: toPublicChangePlan(plan, options.cwd)
|
|
2156
|
+
});
|
|
2157
|
+
else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
|
|
2158
|
+
}));
|
|
2159
|
+
mcp.command("list").description("List supported MCP clients and detection results").option("--cwd <directory>", "project directory", process.cwd()).option("--format <format>", "output format: text or json", "text").action(async (options) => runSafely(options, async () => {
|
|
2160
|
+
const detections = await detectMcpClients({ cwd: options.cwd });
|
|
2161
|
+
if (outputFormat(options.format) === "json") writeJson({ clients: detections });
|
|
2162
|
+
else writeLine(detections.map((item) => `${item.installed ? "detected" : "not detected"} ${item.displayName} (${item.client})\n ${item.configLocations.join("\n ")}`).join("\n"));
|
|
2163
|
+
}));
|
|
2164
|
+
addCommonOptions(mcp.command("status").description("Inspect wot-ui MCP client configuration"), "all").action(async (options) => runSafely(options, async () => {
|
|
2165
|
+
const states = await inspectMcpClients(parseClient(options.client, "all"), {
|
|
2166
|
+
cwd: options.cwd,
|
|
2167
|
+
scope: parseScope(options.scope),
|
|
2168
|
+
pin: options.pin
|
|
2169
|
+
});
|
|
2170
|
+
const publicStates = states.map(toPublicClientConfigState);
|
|
2171
|
+
if (outputFormat(options.format) === "json") writeJson({ clients: publicStates });
|
|
2172
|
+
else writeLine(publicStates.map((state) => `${state.matches ? "ok" : state.configured ? "mismatch" : "missing"} ${state.displayName}: ${state.path}${state.problem ? `\n ${state.problem}` : ""}`).join("\n"));
|
|
2173
|
+
if (states.some((state) => !state.matches)) process.exitCode = 1;
|
|
2174
|
+
}));
|
|
2175
|
+
addCommonOptions(mcp.command("remove").description("Remove the managed wot-ui MCP configuration")).option("--dry-run", "preview changes without writing files").option("-y, --yes", "apply changes without prompting").action(async (options) => runSafely(options, async () => {
|
|
2176
|
+
const plan = mergeChangePlans("Remove wot-ui MCP integration", await planMcpRemove(parseClient(options.client), {
|
|
2177
|
+
cwd: options.cwd,
|
|
2178
|
+
scope: parseScope(options.scope),
|
|
2179
|
+
pin: options.pin
|
|
2180
|
+
}));
|
|
2181
|
+
if (!options.dryRun) {
|
|
2182
|
+
if (!await confirmChangePlan(plan, {
|
|
2183
|
+
yes: options.yes,
|
|
2184
|
+
cwd: options.cwd
|
|
2185
|
+
})) {
|
|
2186
|
+
writeLine("Cancelled.");
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
await applyChangePlan(plan);
|
|
2190
|
+
}
|
|
2191
|
+
if (outputFormat(options.format) === "json") writeJson({
|
|
2192
|
+
dryRun: Boolean(options.dryRun),
|
|
2193
|
+
plan: toPublicChangePlan(plan, options.cwd)
|
|
2194
|
+
});
|
|
2195
|
+
else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
|
|
2196
|
+
}));
|
|
2197
|
+
addCommonOptions(mcp.command("doctor").description("Validate configuration and perform a real MCP handshake")).option("--timeout <milliseconds>", "total doctor timeout in milliseconds", "10000").action(async (options) => runSafely(options, async () => {
|
|
2198
|
+
const reports = await doctorMcpClients(parseClient(options.client), {
|
|
2199
|
+
cwd: options.cwd,
|
|
2200
|
+
scope: parseScope(options.scope),
|
|
2201
|
+
pin: options.pin,
|
|
2202
|
+
timeoutMs: parsePositiveIntegerOption(options.timeout, 1e4, "--timeout")
|
|
2203
|
+
});
|
|
2204
|
+
if (outputFormat(options.format) === "json") writeJson({ clients: reports.map(toPublicMcpDoctorReport) });
|
|
2205
|
+
else writeLine(reports.map(formatMcpDoctorReport).join("\n"));
|
|
2206
|
+
const exitCode = mcpDoctorExitCode(reports);
|
|
2207
|
+
if (exitCode > 0) process.exitCode = exitCode;
|
|
2208
|
+
}));
|
|
2209
|
+
addCommonOptions(mcp.command("print").description("Print the MCP configuration snippet")).action(async (options) => runSafely(options, async () => {
|
|
2210
|
+
const client = parseClient(options.client);
|
|
2211
|
+
if (client === "auto" || client === "all") throw new Error("mcp print requires one explicit client");
|
|
2212
|
+
const config = await printMcpConfig(client, {
|
|
2213
|
+
cwd: options.cwd,
|
|
2214
|
+
scope: parseScope(options.scope),
|
|
2215
|
+
pin: options.pin
|
|
2216
|
+
});
|
|
2217
|
+
if (outputFormat(options.format) === "json") writeJson({
|
|
2218
|
+
client,
|
|
2219
|
+
config
|
|
2220
|
+
});
|
|
2221
|
+
else writeLine(config);
|
|
2222
|
+
}));
|
|
330
2223
|
}
|
|
331
2224
|
|
|
332
2225
|
//#endregion
|
|
@@ -406,6 +2299,7 @@ function createCliProgram() {
|
|
|
406
2299
|
registerUsageCommand(program);
|
|
407
2300
|
registerLintCommand(program);
|
|
408
2301
|
registerMcpCommand(program);
|
|
2302
|
+
registerAgentCommand(program);
|
|
409
2303
|
return program;
|
|
410
2304
|
}
|
|
411
2305
|
|