@wot-ui/cli 1.0.3 → 1.0.5-beta.1

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/dist/index.mjs CHANGED
@@ -1,10 +1,1596 @@
1
1
  #!/usr/bin/env node
2
- import { a as findComponent, c as formatStatusLabel, d as resolveVersion, f as loadMetadataFile, i as lintProject, l as writeStderrLine, m as version, o as listComponents, p as name, r as analyzeUsage, s as formatLogMessage, t as checkForCliUpdate, u as detectVersion } from "./update-check-cp6uTpce.mjs";
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-YZ9xD4u9.mjs";
3
3
  import process from "node:process";
4
4
  import { Command } from "commander";
5
- import { existsSync, readFileSync } from "node:fs";
6
- import { join, resolve } from "node:path";
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-B4R7zC43.mjs");
2130
+ await startMcpServer();
2131
+ }
325
2132
  function registerMcpCommand(program) {
326
- program.command("mcp").description("Start the wot-ui MCP server").action(async () => {
327
- const { startMcpServer } = await import("./server-CbSmyhFy.mjs");
328
- await startMcpServer();
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