@wot-ui/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,280 +1,84 @@
1
1
  #!/usr/bin/env node
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-DjA3lf-3.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-C6Y0GckX.mjs";
3
3
  import process from "node:process";
4
4
  import { Command } from "commander";
5
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";
6
+ import { access, chmod, copyFile, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
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";
9
+ import { createHash, randomUUID } from "node:crypto";
12
10
  import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
13
11
  import { spawn } from "node:child_process";
14
12
  import { isDeepStrictEqual, stripVTControlCharacters } from "node:util";
15
13
  import { parse as parse$1 } from "smol-toml";
14
+ import { isAlias, isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
15
+ import { Buffer } from "node:buffer";
16
+ import { createInterface } from "node:readline/promises";
17
+ import { homedir } from "node:os";
16
18
  import { LATEST_PROTOCOL_VERSION, ReadBuffer, serializeMessage } from "@modelcontextprotocol/server";
17
19
 
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 });
20
+ //#region runtime/deepseek-project-bridge.mjs
21
+ const declarationPath = ".dsh/wot-ui.json";
22
+ function digest(source) {
23
+ return createHash("sha256").update(source).digest("hex");
145
24
  }
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;
25
+ function keys(value, expected) {
26
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === expected.length && expected.every((key) => Object.hasOwn(value, key));
164
27
  }
165
- async function applyChangePlan(plan, hooks = {}) {
166
- const transactionId = `${process.pid}-${Date.now()}-${randomUUID()}`;
167
- const preparedChanges = [];
168
- const applied = [];
28
+ function parseDeclaration(source) {
29
+ let value;
169
30
  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
- });
31
+ value = JSON.parse(source);
32
+ } catch {
33
+ throw new Error("Invalid DeepSeek project JSON");
211
34
  }
35
+ const server = value?.server;
36
+ if (!keys(value, ["schemaVersion", "server"]) || value.schemaVersion !== 1 || !keys(server, ["command", "args"]) || server.command !== "npx" || !Array.isArray(server.args) || server.args.length !== 3 || server.args[0] !== "-y" || server.args[2] !== "mcp" || typeof server.args[1] !== "string" || !/^@wot-ui\/cli(?:@[a-z\d][\w.+-]*)?$/i.test(server.args[1])) throw new Error("Unsupported DeepSeek project declaration; use wot mcp print --client deepseek");
37
+ return value;
212
38
  }
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;
39
+ function validateRegistry(config) {
40
+ if (!keys(config, [
41
+ "schemaVersion",
42
+ "bridgeHash",
43
+ "projects"
44
+ ]) || config.schemaVersion !== 1 || !/^[a-f\d]{64}$/.test(config.bridgeHash) || !Array.isArray(config.projects) || config.projects.some((project) => !keys(project, ["root", "digest"]) || typeof project.root !== "string" || !isAbsolute(project.root) || resolve(project.root) !== project.root || !/^[a-f\d]{64}$/.test(project.digest)) || new Set(config.projects.map((project) => project.root)).size !== config.projects.length) throw new Error("Conflicting or modified DeepSeek project bridge registration");
45
+ return config;
46
+ }
47
+ function within(root, path) {
48
+ const rel = relative(root, path);
49
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
50
+ }
51
+ async function gitRoot(cwd) {
52
+ let current = await realpath(cwd);
53
+ for (;;) {
54
+ try {
55
+ await access(join(current, ".git"));
56
+ return current;
57
+ } catch (error) {
58
+ if (error.code !== "ENOENT") throw error;
222
59
  }
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}`));
60
+ const parent = dirname(current);
61
+ if (parent === current) return void 0;
62
+ current = parent;
240
63
  }
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
64
  }
249
- function toPublicChangePlan(plan, cwd = process.cwd()) {
65
+ var DeepseekProjectLocationError = class extends Error {};
66
+ async function installRoot(cwd) {
67
+ const root = await realpath(cwd);
68
+ if (!(await stat(root)).isDirectory()) throw new Error("DeepSeek project cwd must be a directory");
69
+ const git = await gitRoot(root);
70
+ if (git && git !== root) throw new DeepseekProjectLocationError(`Run from the Git root using --cwd ${JSON.stringify(git)}`);
71
+ return root;
72
+ }
73
+ async function readDeclaration(root) {
74
+ const path = join(root, declarationPath);
75
+ if (!within(root, await realpath(path))) throw new Error("DeepSeek project declaration escapes its registered root");
76
+ const source = await readFile(path, "utf8");
250
77
  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
78
+ source,
79
+ declaration: parseDeclaration(source)
261
80
  };
262
81
  }
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
82
 
279
83
  //#endregion
280
84
  //#region src/mcp/clients/detect.ts
@@ -302,7 +106,7 @@ async function findExecutable(names, context) {
302
106
 
303
107
  //#endregion
304
108
  //#region src/mcp/clients/json-adapter.ts
305
- async function readOptional$3(path) {
109
+ async function readOptional$4(path) {
306
110
  try {
307
111
  return await readFile(path, "utf8");
308
112
  } catch (error) {
@@ -358,9 +162,9 @@ var JsonMcpClientAdapter = class {
358
162
  if (!this.supportedScopes.includes(scope)) throw new Error(`${this.displayName} does not support ${scope} scope`);
359
163
  }
360
164
  async detect(context) {
361
- const configLocations = this.supportedScopes.map((scope) => this.options.configPath(context, scope));
362
- const executable = await findExecutable(this.options.executableNames, context);
363
- const hasConfig = (await Promise.all(configLocations.map(async (path) => {
165
+ const configLocations = await Promise.all(this.supportedScopes.map((scope) => this.options.configPath(context, scope)));
166
+ const executable = this.options.detectExecutable ? await this.options.detectExecutable(context) : await findExecutable(this.options.executableNames, context);
167
+ const hasConfig = (await Promise.all((this.options.detectionPaths?.(context) ?? configLocations).map(async (path) => {
364
168
  try {
365
169
  await access(path);
366
170
  return true;
@@ -380,8 +184,8 @@ var JsonMcpClientAdapter = class {
380
184
  }
381
185
  async inspect(context) {
382
186
  this.assertScope(context.scope);
383
- const path = this.options.configPath(context, context.scope);
384
- const content = await readOptional$3(path);
187
+ const path = await this.options.configPath(context, context.scope);
188
+ const content = await readOptional$4(path);
385
189
  if (content === void 0) return {
386
190
  client: this.id,
387
191
  displayName: this.displayName,
@@ -393,19 +197,19 @@ var JsonMcpClientAdapter = class {
393
197
  };
394
198
  try {
395
199
  const servers = parseObject$1(content || "{}", path)[this.options.serverKey];
396
- const entry = servers && typeof servers === "object" && !Array.isArray(servers) ? servers["wot-ui"] : void 0;
397
- const server = isServerDefinition(entry) ? entry : void 0;
398
- const serverProblem$2 = server ? this.options.serverProblem?.(entry) : void 0;
200
+ const entry$2 = servers && typeof servers === "object" && !Array.isArray(servers) ? servers["wot-ui"] : void 0;
201
+ const server = isServerDefinition(entry$2) ? entry$2 : void 0;
202
+ const serverProblem$4 = server ? this.options.serverProblem?.(entry$2) : void 0;
399
203
  return {
400
204
  client: this.id,
401
205
  displayName: this.displayName,
402
206
  scope: context.scope,
403
207
  path,
404
208
  exists: true,
405
- configured: entry !== void 0,
406
- matches: !serverProblem$2 && serverMatches$2(server, context.server),
209
+ configured: entry$2 !== void 0,
210
+ matches: !serverProblem$4 && serverMatches$2(server, context.server),
407
211
  ...server ? { server } : {},
408
- ...serverProblem$2 ? { problem: serverProblem$2 } : !server && entry !== void 0 ? { problem: "The wot-ui entry is not a supported stdio server definition" } : {}
212
+ ...serverProblem$4 ? { problem: serverProblem$4 } : !server && entry$2 !== void 0 ? { problem: "The wot-ui entry is not a supported stdio server definition" } : {}
409
213
  };
410
214
  } catch (error) {
411
215
  return {
@@ -422,8 +226,8 @@ var JsonMcpClientAdapter = class {
422
226
  }
423
227
  async planInstall(context) {
424
228
  this.assertScope(context.scope);
425
- const path = this.options.configPath(context, context.scope);
426
- const before = await readOptional$3(path);
229
+ const path = await this.options.configPath(context, context.scope);
230
+ const before = await readOptional$4(path);
427
231
  const source = before?.trim() ? before : "{}\n";
428
232
  parseObject$1(source, path);
429
233
  const after = applyEdits(source, modify(source, [this.options.serverKey, "wot-ui"], context.server, { formattingOptions: {
@@ -438,7 +242,7 @@ var JsonMcpClientAdapter = class {
438
242
  changes: unchanged ? [] : [{
439
243
  type: "write-file",
440
244
  path,
441
- allowedRoot: context.scope === "project" ? context.cwd : context.homeDir,
245
+ allowedRoot: this.options.allowedRoot?.(context) ?? (context.scope === "project" ? context.cwd : context.homeDir),
442
246
  before,
443
247
  after,
444
248
  reason: `Add or update mcp server "wot-ui" under ${this.options.serverKey}`,
@@ -450,8 +254,8 @@ var JsonMcpClientAdapter = class {
450
254
  }
451
255
  async planRemove(context) {
452
256
  this.assertScope(context.scope);
453
- const path = this.options.configPath(context, context.scope);
454
- const before = await readOptional$3(path);
257
+ const path = await this.options.configPath(context, context.scope);
258
+ const before = await readOptional$4(path);
455
259
  if (before === void 0) return {
456
260
  id: `${this.id}-${context.scope}-remove`,
457
261
  summary: `Remove wot-ui MCP from ${this.displayName}`,
@@ -471,13 +275,13 @@ var JsonMcpClientAdapter = class {
471
275
  changes: before === after ? [] : [{
472
276
  type: "write-file",
473
277
  path,
474
- allowedRoot: context.scope === "project" ? context.cwd : context.homeDir,
278
+ allowedRoot: this.options.allowedRoot?.(context) ?? (context.scope === "project" ? context.cwd : context.homeDir),
475
279
  before,
476
280
  after,
477
281
  reason: "Remove only the wot-ui MCP server entry",
478
282
  preview: [`- ${this.options.serverKey}.wot-ui`]
479
283
  }],
480
- warnings: [],
284
+ warnings: before !== after && this.options.removeWarning ? [this.options.removeWarning] : [],
481
285
  requiresConfirmation: before !== after
482
286
  };
483
287
  }
@@ -503,7 +307,12 @@ const MCP_CLIENT_IDS = [
503
307
  "vscode",
504
308
  "codex",
505
309
  "opencode",
506
- "antigravity"
310
+ "antigravity",
311
+ "deepseek",
312
+ "qoder",
313
+ "qoder-cli",
314
+ "codebuddy",
315
+ "codebuddy-cli"
507
316
  ];
508
317
  const REQUIRED_WOT_MCP_TOOLS = ["wot_status", "wot_list"];
509
318
  function isMcpClientId(value) {
@@ -515,12 +324,12 @@ function isMcpScope(value) {
515
324
 
516
325
  //#endregion
517
326
  //#region src/mcp/clients/antigravity.ts
518
- function serverProblem$1(value) {
327
+ function serverProblem$3(value) {
519
328
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
520
- const entry = value;
521
- if (entry.disabled === true) return "The wot-ui MCP server is disabled in Antigravity";
522
- if (entry.disabled !== void 0 && typeof entry.disabled !== "boolean") return "The Antigravity wot-ui disabled field must be a boolean";
523
- const disabledTools = entry.disabledTools;
329
+ const entry$2 = value;
330
+ if (entry$2.disabled === true) return "The wot-ui MCP server is disabled in Antigravity";
331
+ if (entry$2.disabled !== void 0 && typeof entry$2.disabled !== "boolean") return "The Antigravity wot-ui disabled field must be a boolean";
332
+ const disabledTools = entry$2.disabledTools;
524
333
  if (disabledTools !== void 0) {
525
334
  if (!Array.isArray(disabledTools) || !disabledTools.every((tool) => typeof tool === "string")) return "The Antigravity wot-ui disabledTools field must be a string array";
526
335
  const unavailableTools = REQUIRED_WOT_MCP_TOOLS.filter((tool) => disabledTools.includes(tool));
@@ -534,7 +343,7 @@ const antigravityAdapter = new JsonMcpClientAdapter({
534
343
  supportedScopes: ["project", "user"],
535
344
  configPath: (context, scope) => scope === "project" ? resolve(context.cwd, ".agents", "mcp_config.json") : join(context.homeDir, ".gemini", "config", "mcp_config.json"),
536
345
  serverKey: "mcpServers",
537
- serverProblem: serverProblem$1,
346
+ serverProblem: serverProblem$3,
538
347
  verifyRegistration: async () => ({
539
348
  status: "unsupported",
540
349
  message: "Antigravity does not expose a stable non-interactive MCP registration status check",
@@ -689,15 +498,97 @@ const claudeAdapter = new JsonMcpClientAdapter({
689
498
  });
690
499
 
691
500
  //#endregion
692
- //#region src/mcp/clients/toml-adapter.ts
693
- const START = "# open-wot managed mcp server start";
694
- const END = "# open-wot managed mcp server end";
695
- const TABLE_PATTERN = /^\[mcp_servers\.(?:wot-ui|"wot-ui")\]\s*$/m;
696
- const CHILD_TABLE_PATTERN = /^\s*\[\[?\s*mcp_servers\s*\.\s*(?:wot-ui|"wot-ui")\s*\./m;
697
- async function readOptional$2(path) {
698
- try {
699
- return await readFile(path, "utf8");
700
- } catch (error) {
501
+ //#region src/mcp/clients/codebuddy.ts
502
+ function cliConfigPaths(context, scope) {
503
+ return scope === "project" ? [join(context.cwd, ".mcp.json"), join(context.cwd, "mcp.json")] : [
504
+ join(context.homeDir, ".codebuddy", ".mcp.json"),
505
+ join(context.homeDir, ".codebuddy", "mcp.json"),
506
+ join(context.homeDir, ".codebuddy.json")
507
+ ];
508
+ }
509
+ async function cliConfigPath(context, scope) {
510
+ const paths = cliConfigPaths(context, scope);
511
+ for (const path of paths) try {
512
+ await lstat(path);
513
+ return path;
514
+ } catch (error) {
515
+ if (error.code !== "ENOENT") throw error;
516
+ }
517
+ return paths[0];
518
+ }
519
+ function serverProblem$2(value) {
520
+ const entry$2 = value;
521
+ if (entry$2.disabled === true) return "The wot-ui MCP server is disabled in CodeBuddy";
522
+ if (entry$2.type !== void 0 && entry$2.type !== "stdio") return "The CodeBuddy wot-ui server must use stdio transport";
523
+ }
524
+ const removeWarning$1 = "This MCP configuration may be shared by other clients; removing wot-ui affects those clients too.";
525
+ const codebuddyAdapter = new JsonMcpClientAdapter({
526
+ id: "codebuddy",
527
+ displayName: "CodeBuddy IDE",
528
+ executableNames: ["buddy"],
529
+ supportedScopes: ["project", "user"],
530
+ configPath: (context, scope) => scope === "project" ? join(context.cwd, ".mcp.json") : join(context.homeDir, ".codebuddy", "mcp.json"),
531
+ detectionPaths: () => [],
532
+ serverKey: "mcpServers",
533
+ serverProblem: serverProblem$2,
534
+ removeWarning: removeWarning$1
535
+ });
536
+ const codebuddyCliAdapter = new JsonMcpClientAdapter({
537
+ id: "codebuddy-cli",
538
+ displayName: "CodeBuddy Code CLI",
539
+ executableNames: ["codebuddy", "cbc"],
540
+ supportedScopes: ["project", "user"],
541
+ configPath: cliConfigPath,
542
+ detectionPaths: (context) => [join(context.homeDir, ".codebuddy", ".mcp.json"), join(context.homeDir, ".codebuddy.json")],
543
+ serverKey: "mcpServers",
544
+ serverProblem: serverProblem$2,
545
+ removeWarning: removeWarning$1,
546
+ verifyRegistration: async () => ({
547
+ status: "unsupported",
548
+ message: "CodeBuddy CLI registration has not been verified by open-wot",
549
+ action: "Run codebuddy mcp get wot-ui in this project, then use /mcp and approve the project server if prompted."
550
+ })
551
+ });
552
+ async function reconcileCodebuddyConfigPlans(plans, context) {
553
+ const paths = cliConfigPaths(context, context.scope);
554
+ const created = plans.flatMap((plan$1) => plan$1.changes).filter((change) => change.type === "write-file" && change.before === void 0 && paths.includes(change.path));
555
+ if (created.length === 0) return;
556
+ const existing = await cliConfigPath(context, context.scope);
557
+ const shadowing = created.find((change) => paths.indexOf(change.path) < paths.indexOf(existing));
558
+ if (shadowing) {
559
+ const errors = [];
560
+ const content = await readFile(existing, "utf8");
561
+ const config = parse(content.trim() ? content : "{}", errors, { allowTrailingComma: true });
562
+ if (errors.length || !config || typeof config !== "object" || Array.isArray(config)) throw new Error(`Cannot check CodeBuddy CLI configuration ${existing}: expected a valid JSONC object. Repair or move that file before retrying.`);
563
+ const servers = config.mcpServers;
564
+ const identicalWotOnly = servers && typeof servers === "object" && !Array.isArray(servers) && Object.keys(servers).length === 1 && isDeepStrictEqual(servers["wot-ui"], context.server);
565
+ if (identicalWotOnly || servers === void 0 || servers && typeof servers === "object" && !Array.isArray(servers) && Object.keys(servers).length === 0) {
566
+ for (const plan$1 of plans) {
567
+ plan$1.changes = plan$1.changes.filter((change) => change.path !== existing);
568
+ plan$1.requiresConfirmation = plan$1.changes.length > 0;
569
+ }
570
+ if (identicalWotOnly) {
571
+ const migration = await codebuddyCliAdapter.planRemove(context);
572
+ if (migration.changes.length !== 1 || migration.changes[0].path !== existing || migration.changes[0].before !== content) throw new Error(`CodeBuddy CLI configuration ${existing} changed while planning migration. Retry the command.`);
573
+ migration.summary = "Remove the identical legacy wot-ui entry after migration";
574
+ plans.push(migration);
575
+ }
576
+ return;
577
+ }
578
+ throw new Error(`Refusing to create ${shadowing.path}: it would hide existing CodeBuddy CLI configuration ${existing}. Merge or migrate its mcpServers into ${shadowing.path} before retrying.`);
579
+ }
580
+ }
581
+
582
+ //#endregion
583
+ //#region src/mcp/clients/toml-adapter.ts
584
+ const START = "# open-wot managed mcp server start";
585
+ const END = "# open-wot managed mcp server end";
586
+ const TABLE_PATTERN = /^\[mcp_servers\.(?:wot-ui|"wot-ui")\]\s*$/m;
587
+ const CHILD_TABLE_PATTERN = /^\s*\[\[?\s*mcp_servers\s*\.\s*(?:wot-ui|"wot-ui")\s*\./m;
588
+ async function readOptional$3(path) {
589
+ try {
590
+ return await readFile(path, "utf8");
591
+ } catch (error) {
701
592
  if (error.code === "ENOENT") return void 0;
702
593
  throw error;
703
594
  }
@@ -748,26 +639,26 @@ function optionalStringArray(value) {
748
639
  if (value === void 0) return void 0;
749
640
  return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
750
641
  }
751
- function parseServer(entry) {
752
- if (!isRecord$2(entry) || typeof entry.command !== "string" || !Array.isArray(entry.args) || !entry.args.every((argument) => typeof argument === "string")) return void 0;
753
- const env = entry.env;
642
+ function parseServer(entry$2) {
643
+ if (!isRecord$2(entry$2) || typeof entry$2.command !== "string" || !Array.isArray(entry$2.args) || !entry$2.args.every((argument) => typeof argument === "string")) return void 0;
644
+ const env = entry$2.env;
754
645
  if (env !== void 0 && (!isRecord$2(env) || !Object.values(env).every((value) => typeof value === "string"))) return void 0;
755
- if (entry.enabled !== void 0 && typeof entry.enabled !== "boolean") return void 0;
756
- const enabledTools = optionalStringArray(entry.enabled_tools);
757
- const disabledTools = optionalStringArray(entry.disabled_tools);
646
+ if (entry$2.enabled !== void 0 && typeof entry$2.enabled !== "boolean") return void 0;
647
+ const enabledTools = optionalStringArray(entry$2.enabled_tools);
648
+ const disabledTools = optionalStringArray(entry$2.disabled_tools);
758
649
  if (enabledTools === null || disabledTools === null) return void 0;
759
650
  return {
760
651
  server: {
761
- command: entry.command,
762
- args: entry.args,
652
+ command: entry$2.command,
653
+ args: entry$2.args,
763
654
  ...env === void 0 ? {} : { env }
764
655
  },
765
- ...entry.enabled === void 0 ? {} : { enabled: entry.enabled },
656
+ ...entry$2.enabled === void 0 ? {} : { enabled: entry$2.enabled },
766
657
  ...enabledTools === void 0 ? {} : { enabledTools },
767
658
  ...disabledTools === void 0 ? {} : { disabledTools }
768
659
  };
769
660
  }
770
- function serverProblem(actual) {
661
+ function serverProblem$1(actual) {
771
662
  if (!actual) return "The wot-ui entry is not a supported stdio server definition";
772
663
  if (actual.enabled === false) return "The wot-ui MCP server is disabled in Codex";
773
664
  const missingEnabledTools = actual.enabledTools === void 0 ? [] : REQUIRED_WOT_MCP_TOOLS.filter((tool) => !actual.enabledTools.includes(tool));
@@ -776,7 +667,7 @@ function serverProblem(actual) {
776
667
  if (unavailableTools.length > 0) return `The Codex tool filters exclude required tools: ${unavailableTools.join(", ")}`;
777
668
  }
778
669
  function serverMatches$1(actual, expected) {
779
- 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 ?? {});
670
+ return !serverProblem$1(actual) && actual?.server.command === expected.command && JSON.stringify(actual.server.args) === JSON.stringify(expected.args) && JSON.stringify(actual.server.env ?? {}) === JSON.stringify(expected.env ?? {});
780
671
  }
781
672
  function assertNoExternalChildTables(content, path) {
782
673
  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`);
@@ -838,7 +729,7 @@ var TomlMcpClientAdapter = class {
838
729
  }
839
730
  async inspect(context) {
840
731
  const path = this.options.configPath(context, context.scope);
841
- const content = await readOptional$2(path);
732
+ const content = await readOptional$3(path);
842
733
  if (content === void 0) return {
843
734
  client: this.id,
844
735
  displayName: this.displayName,
@@ -849,16 +740,16 @@ var TomlMcpClientAdapter = class {
849
740
  matches: false
850
741
  };
851
742
  try {
852
- const entry = getServerEntry$1(parseTomlDocument(content, path));
853
- const parsedServer = parseServer(entry);
854
- const problem = entry === void 0 ? void 0 : serverProblem(parsedServer);
743
+ const entry$2 = getServerEntry$1(parseTomlDocument(content, path));
744
+ const parsedServer = parseServer(entry$2);
745
+ const problem = entry$2 === void 0 ? void 0 : serverProblem$1(parsedServer);
855
746
  return {
856
747
  client: this.id,
857
748
  displayName: this.displayName,
858
749
  scope: context.scope,
859
750
  path,
860
751
  exists: true,
861
- configured: entry !== void 0,
752
+ configured: entry$2 !== void 0,
862
753
  matches: serverMatches$1(parsedServer, context.server),
863
754
  ...parsedServer ? { server: parsedServer.server } : {},
864
755
  ...problem ? { problem } : {}
@@ -878,7 +769,7 @@ var TomlMcpClientAdapter = class {
878
769
  }
879
770
  async planInstall(context) {
880
771
  const path = this.options.configPath(context, context.scope);
881
- const before = await readOptional$2(path);
772
+ const before = await readOptional$3(path);
882
773
  parseTomlDocument(before ?? "", path);
883
774
  assertNoExternalChildTables(before ?? "", path);
884
775
  assertManagedEntryOwnsServer(before ?? "", path);
@@ -903,7 +794,7 @@ var TomlMcpClientAdapter = class {
903
794
  }
904
795
  async planRemove(context) {
905
796
  const path = this.options.configPath(context, context.scope);
906
- const before = await readOptional$2(path);
797
+ const before = await readOptional$3(path);
907
798
  const beforeDocument = before === void 0 ? void 0 : parseTomlDocument(before, path);
908
799
  if (before !== void 0 && managedPattern().test(before)) {
909
800
  assertNoExternalChildTables(before, path);
@@ -1047,6 +938,564 @@ const cursorAdapter = new JsonMcpClientAdapter({
1047
938
  serverKey: "mcpServers"
1048
939
  });
1049
940
 
941
+ //#endregion
942
+ //#region src/mcp/clients/deepseek-config.ts
943
+ const ENTRY_ID = "open-wot-mcp";
944
+ const PLUGIN = "@deepseek-ai/dsh-mcp-client";
945
+ function entry$1(server) {
946
+ return { insert: [{
947
+ id: ENTRY_ID,
948
+ name: PLUGIN,
949
+ config: {
950
+ serverName: "wot-ui",
951
+ transport: "stdio",
952
+ command: server.command,
953
+ args: server.args
954
+ }
955
+ }] };
956
+ }
957
+ function printDeepseekConfig(server) {
958
+ return stringify([entry$1(server)]);
959
+ }
960
+ function conflict$1() {
961
+ throw new Error("Conflicting or modified DeepSeek wot-ui configuration; preserve it and resolve the open-wot-mcp / wot-ui entries manually.");
962
+ }
963
+ function hasKeys(node, keys$1) {
964
+ return node.items.length === keys$1.length && keys$1.every((key) => node.has(key));
965
+ }
966
+ function parseConfig(source) {
967
+ const doc = parseDocument(source, {
968
+ keepSourceTokens: true,
969
+ prettyErrors: false,
970
+ strict: true
971
+ });
972
+ if (doc.errors.length) {
973
+ const error = doc.errors[0];
974
+ throw new Error(`Invalid DeepSeek YAML (${error.code}) at offset ${error.pos[0]}`);
975
+ }
976
+ const root = doc.contents;
977
+ const empty = root === null || isScalar(root) && root.value === null && root.source === "";
978
+ if (!empty && !isSeq(root)) throw new Error("DeepSeek configuration must be a YAML patch list");
979
+ let identityCount = 0;
980
+ let serverCount = 0;
981
+ visit(doc, {
982
+ Alias() {
983
+ throw new Error("DeepSeek YAML aliases cannot be edited safely; resolve aliases before managing wot-ui");
984
+ },
985
+ Map(_key, node) {
986
+ if (node.get("id") === ENTRY_ID) identityCount++;
987
+ if (node.get("serverName") === "wot-ui") serverCount++;
988
+ }
989
+ });
990
+ if (empty) return {
991
+ root: void 0,
992
+ doc,
993
+ index: -1,
994
+ server: void 0
995
+ };
996
+ const list = root;
997
+ if (list.tag || list.anchor) throw new Error("Tagged or anchored DeepSeek patch lists cannot be edited safely");
998
+ const index = list.items.findIndex((patch$1) => {
999
+ if (!isMap(patch$1)) return false;
1000
+ const inserted$1 = patch$1.get("insert", true);
1001
+ return isSeq(inserted$1) && inserted$1.items.some((item) => isMap(item) && item.get("id") === ENTRY_ID);
1002
+ });
1003
+ if (index === -1) {
1004
+ if (identityCount || serverCount) conflict$1();
1005
+ return {
1006
+ root: list,
1007
+ doc,
1008
+ index,
1009
+ server: void 0
1010
+ };
1011
+ }
1012
+ if (identityCount !== 1 || serverCount !== 1) conflict$1();
1013
+ const patch = list.items[index];
1014
+ const inserted = patch.get("insert", true);
1015
+ if (!hasKeys(patch, ["insert"]) || !isSeq(inserted) || inserted.items.length !== 1) conflict$1();
1016
+ const plugin = inserted.items[0];
1017
+ if (!isMap(plugin) || !hasKeys(plugin, [
1018
+ "id",
1019
+ "name",
1020
+ "config"
1021
+ ]) || plugin.get("name") !== PLUGIN) conflict$1();
1022
+ const config = plugin.get("config", true);
1023
+ if (!isMap(config) || !hasKeys(config, [
1024
+ "serverName",
1025
+ "transport",
1026
+ "command",
1027
+ "args"
1028
+ ]) || config.get("transport") !== "stdio" || config.get("command") !== "npx") conflict$1();
1029
+ visit(patch, (_key, node) => {
1030
+ if (isAlias(node) || (isMap(node) || isSeq(node) || isScalar(node)) && (node.tag || node.anchor)) conflict$1();
1031
+ });
1032
+ const args = config.get("args", true);
1033
+ if (!isSeq(args) || args.items.length !== 3 || !args.items.every((item) => isScalar(item) && typeof item.value === "string")) conflict$1();
1034
+ const values = args.items.map((item) => item.value);
1035
+ if (values[0] !== "-y" || values[2] !== "mcp" || !/^@wot-ui\/cli(?:@\S+)?$/.test(values[1])) conflict$1();
1036
+ return {
1037
+ root: list,
1038
+ doc,
1039
+ index,
1040
+ server: {
1041
+ command: "npx",
1042
+ args: values
1043
+ },
1044
+ packageNode: args.items[1]
1045
+ };
1046
+ }
1047
+ function inspectDeepseekConfig(source) {
1048
+ return parseConfig(source).server;
1049
+ }
1050
+ function editSource(source, edits) {
1051
+ for (const edit of edits.sort((a, b) => b.start - a.start)) source = source.slice(0, edit.start) + edit.text + source.slice(edit.end);
1052
+ return source;
1053
+ }
1054
+ function comments(token) {
1055
+ if (!token || typeof token !== "object") return [];
1056
+ const value = token;
1057
+ if (value.type === "comment") return [String(value.source)];
1058
+ return Object.values(value).flatMap((child) => Array.isArray(child) ? child.flatMap(comments) : comments(child));
1059
+ }
1060
+ function editDeepseekConfig(source, server) {
1061
+ const parsed = parseConfig(source);
1062
+ const { root, index } = parsed;
1063
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
1064
+ let after;
1065
+ if (index !== -1 && server) {
1066
+ if (JSON.stringify(parsed.server) === JSON.stringify(server)) return source;
1067
+ const range = parsed.packageNode.range;
1068
+ after = editSource(source, [{
1069
+ start: range[0],
1070
+ end: range[1],
1071
+ text: JSON.stringify(server.args[1])
1072
+ }]);
1073
+ } else if (index !== -1 && root) {
1074
+ const node = root.items[index];
1075
+ const token = root.srcToken;
1076
+ const item = token.items[index];
1077
+ const edits = [];
1078
+ if (token.type === "flow-collection") {
1079
+ const next = token.items[index + 1]?.start.find((part) => part.type === "comma");
1080
+ const previous = item.start.find((part) => part.type === "comma");
1081
+ const comma = next ?? previous;
1082
+ if (comma) edits.push({
1083
+ start: comma.offset,
1084
+ end: comma.offset + 1,
1085
+ text: ""
1086
+ });
1087
+ const kept = comments(item.value);
1088
+ edits.push({
1089
+ start: node.range[0],
1090
+ end: node.range[1],
1091
+ text: kept.length ? `${eol}${kept.join(eol)}${eol}` : ""
1092
+ });
1093
+ } else {
1094
+ const indicator = item.start.find((part) => part.type === "seq-item-ind");
1095
+ const kept = comments(item.value);
1096
+ const replacement = `${root.items.length === 1 ? `[]${eol}` : ""}${kept.length ? `${kept.join(eol)}${eol}` : ""}`;
1097
+ edits.push({
1098
+ start: indicator.offset,
1099
+ end: node.range[2],
1100
+ text: replacement
1101
+ });
1102
+ }
1103
+ after = editSource(source, edits);
1104
+ } else if (!server) return source;
1105
+ else if (root?.flow) {
1106
+ const offset = root.srcToken.start.offset + 1;
1107
+ after = editSource(source, [{
1108
+ start: offset,
1109
+ end: offset,
1110
+ text: `${JSON.stringify(entry$1(server))}${root.items.length ? "," : ""}`
1111
+ }]);
1112
+ } else {
1113
+ const offset = root?.range?.[1] ?? parsed.doc.contents?.range?.[0] ?? parsed.doc.range?.[1] ?? source.length;
1114
+ after = editSource(source, [{
1115
+ start: offset,
1116
+ end: offset,
1117
+ text: (offset > 0 && source[offset - 1] !== "\n" ? eol : "") + printDeepseekConfig(server).replaceAll("\n", eol)
1118
+ }]);
1119
+ }
1120
+ const result = parseConfig(after);
1121
+ if (Boolean(result.server) !== Boolean(server)) throw new Error("Could not safely edit the DeepSeek patch list");
1122
+ return after;
1123
+ }
1124
+
1125
+ //#endregion
1126
+ //#region src/mcp/clients/deepseek-desktop.ts
1127
+ function desktopPaths(context) {
1128
+ if (context.platform === "darwin") return [join(context.homeDir, "Applications/DeepSeek Harness.app"), "/Applications/DeepSeek Harness.app"];
1129
+ if (context.platform === "win32") return [join(context.env?.LOCALAPPDATA ?? context.homeDir, "Programs/DeepSeek Harness/DeepSeek Harness.exe")];
1130
+ return [join(context.homeDir, ".local/share/deepseek-harness"), "/opt/DeepSeek Harness/deepseek-harness"];
1131
+ }
1132
+ async function detectDesktop(context) {
1133
+ return (await Promise.all(desktopPaths(context).map((path) => access(path).then(() => true).catch(() => false)))).some(Boolean);
1134
+ }
1135
+ async function desktopCompatibilityProblem(context) {
1136
+ if (context.platform !== "darwin") return void 0;
1137
+ for (const path of desktopPaths(context)) {
1138
+ const plist = await readFile(join(path, "Contents/Info.plist"), "utf8").catch(() => void 0);
1139
+ if (!plist) continue;
1140
+ const match = (plist.match(/<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/)?.[1])?.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
1141
+ if (match && Number(match[1]) === 0 && (Number(match[2]) < 2 || Number(match[2]) === 2 && Number(match[3]) === 0 && match[4] !== void 0 && Number(match[4]) < 2)) return "DeepSeek project bridge requires desktop 0.2.0-rc.2 or newer; upgrade before using project scope";
1142
+ return;
1143
+ }
1144
+ }
1145
+
1146
+ //#endregion
1147
+ //#region src/mcp/clients/deepseek-project-config.ts
1148
+ const ID = "open-wot-project-bridge";
1149
+ function entry(path, config) {
1150
+ return { insert: [{
1151
+ id: ID,
1152
+ name: path,
1153
+ config
1154
+ }] };
1155
+ }
1156
+ function conflict() {
1157
+ throw new Error("Conflicting or modified DeepSeek project bridge; preserve the patch and resolve open-wot-project-bridge manually");
1158
+ }
1159
+ function parse$2(source, path) {
1160
+ inspectDeepseekConfig(source);
1161
+ const doc = parseDocument(source, {
1162
+ keepSourceTokens: true,
1163
+ prettyErrors: false,
1164
+ strict: true
1165
+ });
1166
+ const root = isSeq(doc.contents) ? doc.contents : void 0;
1167
+ let count = 0;
1168
+ visit(doc, { Map(_key, node) {
1169
+ if (node.get("id") === ID) count++;
1170
+ } });
1171
+ const index = root?.items.findIndex((node) => {
1172
+ if (!isMap(node)) return false;
1173
+ const inserted = node.get("insert", true);
1174
+ return isSeq(inserted) && inserted.items.some((item) => isMap(item) && item.get("id") === ID);
1175
+ }) ?? -1;
1176
+ if (index === -1) {
1177
+ if (count) conflict();
1178
+ return {
1179
+ doc,
1180
+ root,
1181
+ index,
1182
+ config: void 0
1183
+ };
1184
+ }
1185
+ const patch = root.items[index];
1186
+ if (count !== 1 || !isMap(patch) || !hasKeys(patch, ["insert"])) conflict();
1187
+ const inserts = patch.get("insert", true);
1188
+ if (!isSeq(inserts) || inserts.items.length !== 1) conflict();
1189
+ const plugin = inserts.items[0];
1190
+ if (!isMap(plugin) || !hasKeys(plugin, [
1191
+ "id",
1192
+ "name",
1193
+ "config"
1194
+ ]) || plugin.get("name") !== path) conflict();
1195
+ visit(patch, (_key, node) => {
1196
+ if (isAlias(node) || (isMap(node) || isSeq(node) || isScalar(node)) && (node.tag || node.anchor)) conflict();
1197
+ });
1198
+ return {
1199
+ doc,
1200
+ root,
1201
+ index,
1202
+ config: validateRegistry(plugin.get("config", true)?.toJSON())
1203
+ };
1204
+ }
1205
+ function inspectProjectBridge(source, path) {
1206
+ return parse$2(source, path).config;
1207
+ }
1208
+ function editProjectBridge(source, path, config) {
1209
+ if (config) validateRegistry(config);
1210
+ const { doc, root, index, config: current } = parse$2(source, path);
1211
+ if (JSON.stringify(config) === JSON.stringify(current)) return source;
1212
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
1213
+ let after;
1214
+ if (root && index !== -1) {
1215
+ const node = root.items[index];
1216
+ const token = root.srcToken;
1217
+ const item = token.items[index];
1218
+ const kept = comments(item.value);
1219
+ const note = kept.length ? `${kept.join(eol)}${eol}` : "";
1220
+ if (token.type === "flow-collection") {
1221
+ const edits = [{
1222
+ start: node.range[0],
1223
+ end: node.range[1],
1224
+ text: config ? `${JSON.stringify(entry(path, config))}${note ? `${eol}${note}` : ""}` : note ? `${eol}${note}` : ""
1225
+ }];
1226
+ if (!config) {
1227
+ const comma = token.items[index + 1]?.start.find((part) => part.type === "comma") ?? item.start.find((part) => part.type === "comma");
1228
+ if (comma) edits.push({
1229
+ start: comma.offset,
1230
+ end: comma.offset + 1,
1231
+ text: ""
1232
+ });
1233
+ }
1234
+ after = editSource(source, edits);
1235
+ } else {
1236
+ const start = item.start.find((part) => part.type === "seq-item-ind").offset;
1237
+ const text = config ? stringify([entry(path, config)]).replaceAll("\n", eol) : root.items.length === 1 ? `[]${eol}` : "";
1238
+ after = editSource(source, [{
1239
+ start,
1240
+ end: node.range[2],
1241
+ text: note + text
1242
+ }]);
1243
+ }
1244
+ } else if (config && root?.flow) {
1245
+ const start = root.srcToken.start.offset + 1;
1246
+ after = editSource(source, [{
1247
+ start,
1248
+ end: start,
1249
+ text: `${JSON.stringify(entry(path, config))}${root.items.length ? "," : ""}`
1250
+ }]);
1251
+ } else if (config) {
1252
+ const start = root?.range?.[1] ?? doc.contents?.range?.[0] ?? doc.range?.[1] ?? source.length;
1253
+ after = editSource(source, [{
1254
+ start,
1255
+ end: start,
1256
+ text: `${start && source[start - 1] !== "\n" ? eol : ""}${stringify([entry(path, config)]).replaceAll("\n", eol)}`
1257
+ }]);
1258
+ } else return source;
1259
+ if (JSON.stringify(inspectProjectBridge(after, path)) !== JSON.stringify(config)) throw new Error("Could not safely edit the DeepSeek project bridge");
1260
+ return after;
1261
+ }
1262
+
1263
+ //#endregion
1264
+ //#region src/mcp/clients/deepseek-project.ts
1265
+ async function readOptional$2(path) {
1266
+ try {
1267
+ return await readFile(path, "utf8");
1268
+ } catch (error) {
1269
+ if (error.code === "ENOENT") return void 0;
1270
+ throw error;
1271
+ }
1272
+ }
1273
+ function bridgePath(home) {
1274
+ return join(home, "open-wot/project-bridge.mjs");
1275
+ }
1276
+ function printProject(server) {
1277
+ const source = `${JSON.stringify({
1278
+ schemaVersion: 1,
1279
+ server
1280
+ }, null, 2)}\n`;
1281
+ parseDeclaration(source);
1282
+ return source;
1283
+ }
1284
+ async function bundledBridge() {
1285
+ const here = dirname(fileURLToPath(import.meta.url));
1286
+ for (const path of [join(here, "../runtime/deepseek-project-bridge.mjs"), join(here, "../../../runtime/deepseek-project-bridge.mjs")]) {
1287
+ const source = await readOptional$2(path);
1288
+ if (source !== void 0) return source;
1289
+ }
1290
+ throw new Error("Missing packaged DeepSeek bridge; reinstall @wot-ui/cli");
1291
+ }
1292
+ async function planProject(context, home, remove) {
1293
+ const root = await installRoot(context.cwd);
1294
+ const path = join(root, declarationPath);
1295
+ const patchPath = join(home, "cordis.patch.yml");
1296
+ const bridge = bridgePath(home);
1297
+ const [before, patchBefore, bridgeBefore] = await Promise.all([
1298
+ readOptional$2(path),
1299
+ readOptional$2(patchPath),
1300
+ readOptional$2(bridge)
1301
+ ]);
1302
+ const global = inspectDeepseekConfig(patchBefore ?? "");
1303
+ if (!remove && global) throw new Error("Global wot-ui conflicts with project isolation. Run wot mcp remove --client deepseek --scope user, then initialize this project again.");
1304
+ const existing = inspectProjectBridge(patchBefore ?? "", bridge);
1305
+ const registered = existing?.projects.find((project) => project.root === root);
1306
+ const changes = [];
1307
+ const warnings = ["Reload or restart DeepSeek after first installing the bridge; reopen project sessions after changes. Each machine and moved project needs its own init registration."];
1308
+ function change(path$1, allowedRoot, before$1, after, reason) {
1309
+ if (before$1 === after) return;
1310
+ changes.push({
1311
+ type: after === void 0 ? "delete-file" : "write-file",
1312
+ path: path$1,
1313
+ allowedRoot,
1314
+ before: before$1,
1315
+ after,
1316
+ reason
1317
+ });
1318
+ }
1319
+ const projects = existing?.projects.filter((project) => project.root !== root) ?? [];
1320
+ if (remove) {
1321
+ if (before !== void 0) if (registered && digest(before) === registered.digest) change(path, root, before, void 0, "Remove registered project declaration");
1322
+ else warnings.push("Preserved modified or unregistered project declaration; remove it manually if no longer needed.");
1323
+ if (registered) {
1324
+ change(patchPath, home, patchBefore, editProjectBridge(patchBefore ?? "", bridge, projects.length ? {
1325
+ ...existing,
1326
+ projects
1327
+ } : void 0), "Unregister only this project");
1328
+ if (!projects.length && bridgeBefore !== void 0) if (digest(bridgeBefore) === existing.bridgeHash) change(bridge, home, bridgeBefore, void 0, "Remove unused managed project bridge");
1329
+ else warnings.push("Preserved modified shared bridge file.");
1330
+ }
1331
+ } else {
1332
+ if (before !== void 0) await readDeclaration(root);
1333
+ if (bridgeBefore !== void 0 && (!existing || digest(bridgeBefore) !== existing.bridgeHash)) throw new Error("Modified or unmanaged DeepSeek bridge; preserve it and resolve the shared file manually");
1334
+ const source = await bundledBridge();
1335
+ const after = before !== void 0 && JSON.stringify(parseDeclaration(before).server) === JSON.stringify(context.server) ? before : printProject(context.server);
1336
+ const registration = {
1337
+ root,
1338
+ digest: digest(after)
1339
+ };
1340
+ const next = existing?.projects.map((project) => project.root === root ? registration : project) ?? [];
1341
+ if (!registered) next.push(registration);
1342
+ change(path, root, before, after, `Configure project wot-ui: ${context.server.args[1]}`);
1343
+ change(bridge, home, bridgeBefore, source, "Install standalone project bridge in stable user directory");
1344
+ change(patchPath, home, patchBefore, editProjectBridge(patchBefore ?? "", bridge, {
1345
+ schemaVersion: 1,
1346
+ bridgeHash: digest(source),
1347
+ projects: next
1348
+ }), "Register this project for automatic per-session loading");
1349
+ }
1350
+ return {
1351
+ id: `deepseek-project-${remove ? "remove" : "install"}`,
1352
+ summary: `${remove ? "Remove" : "Configure"} DeepSeek project MCP`,
1353
+ changes,
1354
+ warnings,
1355
+ requiresConfirmation: changes.length > 0
1356
+ };
1357
+ }
1358
+ async function inspectProject(context, home) {
1359
+ const path = join(context.cwd, declarationPath);
1360
+ const state = {
1361
+ client: "deepseek",
1362
+ displayName: "DeepSeek Harness",
1363
+ scope: "project",
1364
+ path,
1365
+ exists: false,
1366
+ configured: false,
1367
+ matches: false
1368
+ };
1369
+ try {
1370
+ const root = await installRoot(context.cwd);
1371
+ const compatibility = await desktopCompatibilityProblem(context);
1372
+ if (compatibility) throw new Error(compatibility);
1373
+ state.exists = await readOptional$2(path) !== void 0;
1374
+ const patch = await readOptional$2(join(home, "cordis.patch.yml")) ?? "";
1375
+ if (inspectDeepseekConfig(patch)) throw new Error("Global wot-ui conflicts with project isolation; remove the user MCP registration first");
1376
+ const registry = inspectProjectBridge(patch, bridgePath(home));
1377
+ if (!state.exists && !registry?.projects.some((project) => project.root === root)) return state;
1378
+ const { source, declaration } = await readDeclaration(root);
1379
+ state.configured = true;
1380
+ const registered = registry?.projects.find((project) => project.root === root);
1381
+ if (!registered || registered.digest !== digest(source)) throw new Error("Missing or stale project registration; run wot agent init --client deepseek");
1382
+ const bridge = await readOptional$2(bridgePath(home));
1383
+ if (!bridge || digest(bridge) !== registry.bridgeHash || bridge !== await bundledBridge()) throw new Error("Missing, modified or outdated DeepSeek bridge; run init to repair or upgrade");
1384
+ return {
1385
+ ...state,
1386
+ server: declaration.server,
1387
+ matches: JSON.stringify(declaration.server) === JSON.stringify(context.server)
1388
+ };
1389
+ } catch (error) {
1390
+ return {
1391
+ ...state,
1392
+ problem: error instanceof Error ? error.message : "Invalid DeepSeek project configuration"
1393
+ };
1394
+ }
1395
+ }
1396
+
1397
+ //#endregion
1398
+ //#region src/mcp/clients/deepseek.ts
1399
+ function dshHome(context) {
1400
+ const override = (context.env ?? process.env).DSH_HOME;
1401
+ let path = override?.trim() ? override : join(context.homeDir, ".dsh");
1402
+ if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) path = join(context.homeDir, path.slice(2));
1403
+ return resolve(context.cwd, path);
1404
+ }
1405
+ function configPath(context) {
1406
+ return join(dshHome(context), "cordis.patch.yml");
1407
+ }
1408
+ async function plan(context, remove) {
1409
+ if (context.scope === "project") return planProject(context, dshHome(context), remove);
1410
+ const path = configPath(context);
1411
+ const before = await readOptional$2(path);
1412
+ if (!remove && inspectProjectBridge(before ?? "", bridgePath(dshHome(context)))?.projects.length) throw new Error("Project registrations conflict with global wot-ui; remove each project MCP registration before using --scope user");
1413
+ const after = editDeepseekConfig(before ?? "", remove ? void 0 : context.server);
1414
+ const changed = (before ?? "") !== after;
1415
+ return {
1416
+ id: `deepseek-user-${remove ? "remove" : "install"}`,
1417
+ summary: `${remove ? "Remove" : "Configure"} wot-ui MCP for DeepSeek Harness`,
1418
+ changes: changed ? [{
1419
+ type: "write-file",
1420
+ path,
1421
+ allowedRoot: dshHome(context),
1422
+ before,
1423
+ after,
1424
+ reason: `${remove ? "Remove" : "Configure"} only the open-wot-mcp plugin entry`,
1425
+ preview: [`${remove ? "-" : "+"} open-wot-mcp (wot-ui, stdio)`, ...remove ? [] : [` command: ${context.server.command}`, ` args: ${context.server.args.join(" ")}`]]
1426
+ }] : [],
1427
+ warnings: ["The DeepSeek user patch layer is shared by all profiles that load it. Custom and minimal profiles may not provide all required capabilities."],
1428
+ requiresConfirmation: changed
1429
+ };
1430
+ }
1431
+ const deepseekAdapter = {
1432
+ id: "deepseek",
1433
+ displayName: "DeepSeek Harness",
1434
+ supportedScopes: ["project", "user"],
1435
+ async detect(context) {
1436
+ const path = configPath(context);
1437
+ const executable = await findExecutable(["dsh"], context);
1438
+ const exists$1 = await access(path).then(() => true).catch(() => false);
1439
+ const desktop = await detectDesktop(context);
1440
+ const project = await access(join(context.cwd, ".dsh/wot-ui.json")).then(() => true).catch(() => false);
1441
+ return {
1442
+ client: this.id,
1443
+ displayName: this.displayName,
1444
+ installed: Boolean(executable) || exists$1 || desktop || project,
1445
+ ...executable ? { executable } : {},
1446
+ supportedScopes: this.supportedScopes,
1447
+ configLocations: [
1448
+ join(context.cwd, ".dsh/wot-ui.json"),
1449
+ path,
1450
+ bridgePath(dshHome(context))
1451
+ ],
1452
+ preferredMethod: "config-file"
1453
+ };
1454
+ },
1455
+ async inspect(context) {
1456
+ if (context.scope === "project") return inspectProject(context, dshHome(context));
1457
+ const path = configPath(context);
1458
+ const content = await readOptional$2(path);
1459
+ const state = {
1460
+ client: this.id,
1461
+ displayName: this.displayName,
1462
+ scope: context.scope,
1463
+ path,
1464
+ exists: content !== void 0,
1465
+ configured: false,
1466
+ matches: false
1467
+ };
1468
+ try {
1469
+ const server = inspectDeepseekConfig(content ?? "");
1470
+ if (server && inspectProjectBridge(content ?? "", bridgePath(dshHome(context)))?.projects.length) throw new Error("Global wot-ui conflicts with existing project registrations");
1471
+ return {
1472
+ ...state,
1473
+ configured: Boolean(server),
1474
+ matches: Boolean(server) && JSON.stringify(server) === JSON.stringify(context.server),
1475
+ ...server ? { server } : {}
1476
+ };
1477
+ } catch (error) {
1478
+ return {
1479
+ ...state,
1480
+ problem: error instanceof Error ? error.message : "Invalid DeepSeek configuration"
1481
+ };
1482
+ }
1483
+ },
1484
+ planInstall: (context) => plan(context, false),
1485
+ planRemove: (context) => plan(context, true),
1486
+ async print(context) {
1487
+ if (context.scope === "project") return printProject(context.server);
1488
+ return printDeepseekConfig(context.server);
1489
+ },
1490
+ async verifyRegistration() {
1491
+ return {
1492
+ status: "unsupported",
1493
+ message: "DeepSeek runtime registration has not been verified; a successful handshake only verifies the server.",
1494
+ action: "Open a DeepSeek Harness standard-mode session in this project, reload or restart its configuration, and confirm wot_status and wot_list can be called. Also load wot-ui-v2 and check AGENTS.md instructions."
1495
+ };
1496
+ }
1497
+ };
1498
+
1050
1499
  //#endregion
1051
1500
  //#region src/mcp/clients/opencode.ts
1052
1501
  function userConfigRoot(context) {
@@ -1132,13 +1581,13 @@ async function readConfigSources(context, scope) {
1132
1581
  const content = await readOptional$1(path);
1133
1582
  if (content === void 0) continue;
1134
1583
  const config = parseObject(content || "{}", path);
1135
- const entry = getServerEntry(config);
1584
+ const entry$2 = getServerEntry(config);
1136
1585
  sources.push({
1137
1586
  path,
1138
1587
  content,
1139
1588
  config,
1140
- serverEntry: entry.value,
1141
- hasServerEntry: entry.present
1589
+ serverEntry: entry$2.value,
1590
+ hasServerEntry: entry$2.present
1142
1591
  });
1143
1592
  }
1144
1593
  return sources;
@@ -1259,17 +1708,17 @@ const opencodeAdapter = {
1259
1708
  };
1260
1709
  path = selected.path;
1261
1710
  const config = mergeConfigSources(sources);
1262
- const entry = getServerEntry(config);
1263
- const server = decodeServer(entry.value);
1711
+ const entry$2 = getServerEntry(config);
1712
+ const server = decodeServer(entry$2.value);
1264
1713
  const unavailableTools = unavailableRequiredTools(config);
1265
- const problem = entry.present ? !server ? "The wot-ui entry is not an enabled OpenCode local MCP server definition" : unavailableTools.length > 0 ? `OpenCode tool filters exclude required tools: ${unavailableTools.join(", ")}` : void 0 : void 0;
1714
+ const problem = entry$2.present ? !server ? "The wot-ui entry is not an enabled OpenCode local MCP server definition" : unavailableTools.length > 0 ? `OpenCode tool filters exclude required tools: ${unavailableTools.join(", ")}` : void 0 : void 0;
1266
1715
  return {
1267
1716
  client: "opencode",
1268
1717
  displayName: "OpenCode",
1269
1718
  scope: context.scope,
1270
1719
  path,
1271
1720
  exists: true,
1272
- configured: entry.present,
1721
+ configured: entry$2.present,
1273
1722
  matches: !problem && serverMatches(server, context.server),
1274
1723
  ...server ? { server } : {},
1275
1724
  ...problem ? { problem } : {}
@@ -1343,92 +1792,427 @@ const opencodeAdapter = {
1343
1792
  }];
1344
1793
  });
1345
1794
  return {
1346
- id: `opencode-${context.scope}-remove`,
1347
- summary: "Remove wot-ui MCP from OpenCode",
1348
- changes,
1349
- warnings: [],
1350
- requiresConfirmation: changes.length > 0
1351
- };
1352
- },
1353
- async print(context) {
1354
- return JSON.stringify({ mcp: { "wot-ui": encodeServer(context.server) } }, null, 2);
1355
- },
1356
- async verifyRegistration(context, _state, options) {
1357
- const executable = await findExecutable(["opencode"], context);
1358
- if (!executable) return {
1359
- status: "unknown",
1360
- message: "OpenCode executable was not found; registration could not be verified",
1361
- action: "Open OpenCode in this project and inspect its MCP servers."
1795
+ id: `opencode-${context.scope}-remove`,
1796
+ summary: "Remove wot-ui MCP from OpenCode",
1797
+ changes,
1798
+ warnings: [],
1799
+ requiresConfirmation: changes.length > 0
1800
+ };
1801
+ },
1802
+ async print(context) {
1803
+ return JSON.stringify({ mcp: { "wot-ui": encodeServer(context.server) } }, null, 2);
1804
+ },
1805
+ async verifyRegistration(context, _state, options) {
1806
+ const executable = await findExecutable(["opencode"], context);
1807
+ if (!executable) return {
1808
+ status: "unknown",
1809
+ message: "OpenCode executable was not found; registration could not be verified",
1810
+ action: "Open OpenCode in this project and inspect its MCP servers."
1811
+ };
1812
+ const result = await runClientCommand(executable, ["mcp", "list"], {
1813
+ ...context,
1814
+ timeoutMs: options?.timeoutMs
1815
+ });
1816
+ const serverLine = clientCommandOutput(result).split(/\r?\n/).find((line) => /^\s*(?:(?:[│┃]\s*)?[✓●○⚠✗×✔!]\s+){0,2}wot-ui(?=\s|$)/i.test(line));
1817
+ const command = `${executable} mcp list`;
1818
+ if (result.timedOut || result.error) return {
1819
+ status: "failed",
1820
+ message: result.error || "OpenCode registration check failed",
1821
+ action: "Retry opencode mcp list and inspect the client process if it still fails.",
1822
+ command
1823
+ };
1824
+ if (serverLine && /pending approval|approval.*pending|untrusted|not trusted/i.test(serverLine)) return {
1825
+ status: "pending",
1826
+ message: "OpenCode requires MCP approval",
1827
+ action: "Approve the wot-ui MCP server in OpenCode and retry the doctor command.",
1828
+ command
1829
+ };
1830
+ if (serverLine && /not initialized|disabled|disconnected|failed|error|needs authentication|needs client registration/i.test(serverLine)) return {
1831
+ status: "failed",
1832
+ message: "OpenCode recognizes the wot-ui MCP server, but it is not ready",
1833
+ action: "Inspect wot-ui with opencode mcp list, then enable or repair the server.",
1834
+ command
1835
+ };
1836
+ if (result.exitCode === 0 && serverLine && /\bconnected\b/i.test(serverLine)) return {
1837
+ status: "ready",
1838
+ message: "OpenCode recognizes the wot-ui MCP server",
1839
+ command
1840
+ };
1841
+ if (result.exitCode === 0 && serverLine) return {
1842
+ status: "unknown",
1843
+ message: "OpenCode recognizes the wot-ui MCP server, but its connection status is unknown",
1844
+ action: "Inspect wot-ui with opencode mcp list and confirm that it is connected.",
1845
+ command
1846
+ };
1847
+ return {
1848
+ status: "failed",
1849
+ message: "OpenCode did not recognize the wot-ui MCP server",
1850
+ action: "Run opencode mcp list and inspect the configured scope.",
1851
+ command
1852
+ };
1853
+ }
1854
+ };
1855
+
1856
+ //#endregion
1857
+ //#region src/mcp/clients/qoder.ts
1858
+ function cliUserDirectory(context) {
1859
+ return context.env?.QODER_CONFIG_DIR ? resolve(context.cwd, context.env.QODER_CONFIG_DIR) : join(context.homeDir, ".qoder");
1860
+ }
1861
+ async function qoderExecutable(context, cli) {
1862
+ if (cli) {
1863
+ const executable$1 = await findExecutable(["qodercli"], context);
1864
+ if (executable$1) return executable$1;
1865
+ }
1866
+ const executable = await findExecutable(["qoder"], context);
1867
+ if (!executable) return void 0;
1868
+ try {
1869
+ const path = await realpath(executable);
1870
+ const file = await open(path, "r");
1871
+ let source;
1872
+ try {
1873
+ const buffer = Buffer.alloc(8192);
1874
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
1875
+ source = buffer.subarray(0, bytesRead).toString("utf8");
1876
+ } finally {
1877
+ await file.close();
1878
+ }
1879
+ if (/qoder(?:-ide)?\.app[/\\]|[/\\]qoder-ide[/\\]/i.test(path) || /ELECTRON_RUN_AS_NODE|VSCODE_PATH|out[/\\]cli\.js/.test(source)) return cli ? void 0 : executable;
1880
+ const cliLauncher = /[/\\]@qoder-ai[/\\]qodercli[/\\]/.test(`${path}\n${source}`);
1881
+ return cli && cliLauncher ? executable : void 0;
1882
+ } catch {
1883
+ return;
1884
+ }
1885
+ }
1886
+ function serverProblem(value) {
1887
+ const entry$2 = value;
1888
+ if (entry$2.disabled === true) return "The wot-ui MCP server is disabled in Qoder";
1889
+ if (entry$2.type !== void 0 && entry$2.type !== "stdio") return "The Qoder wot-ui server must use stdio transport";
1890
+ for (const tool of REQUIRED_WOT_MCP_TOOLS) if (Array.isArray(entry$2.excludeTools) && entry$2.excludeTools.includes(tool) || Array.isArray(entry$2.includeTools) && !entry$2.includeTools.includes(tool)) return `Qoder filters required wot-ui tool: ${tool}`;
1891
+ }
1892
+ const removeWarning = "This MCP configuration may be shared by other clients; removing wot-ui affects those clients too.";
1893
+ const qoderAdapter = new JsonMcpClientAdapter({
1894
+ id: "qoder",
1895
+ displayName: "Qoder IDE",
1896
+ executableNames: [],
1897
+ detectExecutable: (context) => qoderExecutable(context, false),
1898
+ supportedScopes: ["project", "user"],
1899
+ configPath: (context, scope) => scope === "project" ? join(context.cwd, ".mcp.json") : join(context.homeDir, ".qoder", "mcp.json"),
1900
+ detectionPaths: (context) => [join(context.cwd, ".qoder", "mcp.json"), join(context.homeDir, ".qoder", "mcp.json")],
1901
+ serverKey: "mcpServers",
1902
+ serverProblem,
1903
+ removeWarning
1904
+ });
1905
+ const qoderCliAdapter = new JsonMcpClientAdapter({
1906
+ id: "qoder-cli",
1907
+ displayName: "Qoder CLI",
1908
+ executableNames: [],
1909
+ detectExecutable: (context) => qoderExecutable(context, true),
1910
+ supportedScopes: ["project", "user"],
1911
+ configPath: (context, scope) => scope === "project" ? join(context.cwd, ".mcp.json") : join(cliUserDirectory(context), "settings.json"),
1912
+ allowedRoot: (context) => context.scope === "project" ? context.cwd : cliUserDirectory(context),
1913
+ detectionPaths: (context) => [join(cliUserDirectory(context), "settings.json")],
1914
+ serverKey: "mcpServers",
1915
+ serverProblem,
1916
+ removeWarning,
1917
+ verifyRegistration: async () => ({
1918
+ status: "unsupported",
1919
+ message: "Qoder CLI registration has not been verified by open-wot",
1920
+ action: "Run qoder mcp list in this project, then use /mcp reload and approve the project server if prompted."
1921
+ })
1922
+ });
1923
+
1924
+ //#endregion
1925
+ //#region src/mcp/clients/vscode.ts
1926
+ const vscodeAdapter = new JsonMcpClientAdapter({
1927
+ id: "vscode",
1928
+ displayName: "VS Code",
1929
+ executableNames: ["code"],
1930
+ supportedScopes: ["project"],
1931
+ configPath: (context) => resolve(context.cwd, ".vscode", "mcp.json"),
1932
+ serverKey: "servers"
1933
+ });
1934
+
1935
+ //#endregion
1936
+ //#region src/mcp/clients/index.ts
1937
+ const mcpClientAdapters = [
1938
+ claudeAdapter,
1939
+ cursorAdapter,
1940
+ vscodeAdapter,
1941
+ codexAdapter,
1942
+ opencodeAdapter,
1943
+ antigravityAdapter,
1944
+ deepseekAdapter,
1945
+ qoderAdapter,
1946
+ qoderCliAdapter,
1947
+ codebuddyAdapter,
1948
+ codebuddyCliAdapter
1949
+ ];
1950
+ function getMcpClientAdapter(id) {
1951
+ const adapter = mcpClientAdapters.find((candidate) => candidate.id === id);
1952
+ if (!adapter) throw new Error(`Unsupported MCP client: ${id}`);
1953
+ return adapter;
1954
+ }
1955
+
1956
+ //#endregion
1957
+ //#region src/application/change-plan.ts
1958
+ async function pathExists(path) {
1959
+ try {
1960
+ await access(path, constants.F_OK);
1961
+ return true;
1962
+ } catch {
1963
+ return false;
1964
+ }
1965
+ }
1966
+ async function readOptionalFile(path) {
1967
+ try {
1968
+ return await readFile(path, "utf8");
1969
+ } catch (error) {
1970
+ if (error.code === "ENOENT") return void 0;
1971
+ throw error;
1972
+ }
1973
+ }
1974
+ var ChangePlanCleanupError = class extends Error {
1975
+ applied;
1976
+ cleanupFailures;
1977
+ constructor(options) {
1978
+ super(`Change plan was applied, but transaction cleanup failed for ${options.cleanupFailures.map((failure) => failure.path).join(", ")}`);
1979
+ this.name = "ChangePlanCleanupError";
1980
+ this.applied = options.applied;
1981
+ this.cleanupFailures = options.cleanupFailures;
1982
+ }
1983
+ };
1984
+ var ChangePlanApplyError = class extends Error {
1985
+ cause;
1986
+ applied;
1987
+ rolledBack;
1988
+ rollbackFailures;
1989
+ cleanupFailures;
1990
+ constructor(options) {
1991
+ const causeMessage = options.cause instanceof Error ? options.cause.message : String(options.cause);
1992
+ 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";
1993
+ const cleanupMessage = options.cleanupFailures.length > 0 ? `; cleanup incomplete for ${options.cleanupFailures.map((failure) => failure.path).join(", ")}` : "";
1994
+ super(`Failed to apply change plan; ${rollbackMessage}${cleanupMessage}: ${causeMessage}`);
1995
+ this.name = "ChangePlanApplyError";
1996
+ this.cause = options.cause;
1997
+ this.applied = options.applied;
1998
+ this.rolledBack = options.rolledBack;
1999
+ this.rollbackFailures = options.rollbackFailures;
2000
+ this.cleanupFailures = options.cleanupFailures;
2001
+ }
2002
+ };
2003
+ function isWithinRoot(root, path) {
2004
+ const relation = relative(root, path);
2005
+ return relation === "" || !relation.startsWith("..") && !isAbsolute(relation);
2006
+ }
2007
+ async function nearestExistingPath(path) {
2008
+ let current = path;
2009
+ while (true) try {
2010
+ await lstat(current);
2011
+ return current;
2012
+ } catch (error) {
2013
+ if (error.code !== "ENOENT") throw error;
2014
+ const parent = dirname(current);
2015
+ if (parent === current) throw error;
2016
+ current = parent;
2017
+ }
2018
+ }
2019
+ async function assertPlanPath(change) {
2020
+ const { path, allowedRoot } = change;
2021
+ if (!path || path === "/" || path === process.env.HOME) throw new Error(`Refusing unsafe change target: ${path || "<empty>"}`);
2022
+ if (!allowedRoot) throw new Error(`Missing allowed root for change target: ${path}`);
2023
+ const [existingRoot, existingPath] = await Promise.all([nearestExistingPath(allowedRoot), nearestExistingPath(path)]);
2024
+ if (!isWithinRoot(resolve(await realpath(existingRoot), relative(existingRoot, allowedRoot)), resolve(await realpath(existingPath), relative(existingPath, path)))) throw new Error(`Refusing change target outside allowed root: ${path}`);
2025
+ if (existingPath === path && (await lstat(path)).isSymbolicLink()) throw new Error(`Refusing symbolic link change target: ${path}`);
2026
+ }
2027
+ async function assertUnchanged(change) {
2028
+ await assertPlanPath(change);
2029
+ if (await readOptionalFile(change.path) !== change.before) throw new Error(`Target changed after planning: ${change.path}`);
2030
+ }
2031
+ function transactionPath(path, transactionId, index, suffix) {
2032
+ return `${path}.open-wot-${transactionId}-${index}.${suffix}`;
2033
+ }
2034
+ async function prepareChange(change, transactionId, index) {
2035
+ await assertPlanPath(change);
2036
+ if (change.type === "delete-file") return {
2037
+ change,
2038
+ touched: false,
2039
+ backupCreated: false
2040
+ };
2041
+ await mkdir(dirname(change.path), { recursive: true });
2042
+ const temporaryPath = transactionPath(change.path, transactionId, index, "tmp");
2043
+ try {
2044
+ await writeFile(temporaryPath, change.after ?? "", "utf8");
2045
+ if (change.before !== void 0 && await pathExists(change.path)) await chmod(temporaryPath, (await stat(change.path)).mode);
2046
+ return {
2047
+ change,
2048
+ temporaryPath,
2049
+ touched: false,
2050
+ backupCreated: false
1362
2051
  };
1363
- const result = await runClientCommand(executable, ["mcp", "list"], {
1364
- ...context,
1365
- timeoutMs: options?.timeoutMs
2052
+ } catch (error) {
2053
+ await rm(temporaryPath, { force: true }).catch(() => {});
2054
+ throw error;
2055
+ }
2056
+ }
2057
+ async function commitChange(prepared, transactionId, index, hooks) {
2058
+ const { change } = prepared;
2059
+ await assertUnchanged(change);
2060
+ if (change.before !== void 0) {
2061
+ prepared.backupPath = transactionPath(change.path, transactionId, index, "rollback");
2062
+ await hooks.beforeBackup?.(change, prepared.backupPath, index);
2063
+ await copyFile(change.path, prepared.backupPath, constants.COPYFILE_EXCL);
2064
+ prepared.backupCreated = true;
2065
+ }
2066
+ if (change.type === "write-file") {
2067
+ await rename(prepared.temporaryPath, change.path);
2068
+ prepared.touched = true;
2069
+ } else {
2070
+ await rm(change.path);
2071
+ prepared.touched = true;
2072
+ }
2073
+ }
2074
+ async function rollbackPreparedChange(prepared) {
2075
+ const { change } = prepared;
2076
+ if (!prepared.touched) return;
2077
+ if (prepared.backupCreated && prepared.backupPath) {
2078
+ await rm(change.path, { force: true });
2079
+ await rename(prepared.backupPath, change.path);
2080
+ prepared.backupCreated = false;
2081
+ return;
2082
+ }
2083
+ await rm(change.path, { force: true });
2084
+ }
2085
+ async function cleanupPreparedChanges(preparedChanges, removeBackups, hooks) {
2086
+ const failures = [];
2087
+ await Promise.all(preparedChanges.flatMap((prepared) => {
2088
+ const paths = [prepared.temporaryPath];
2089
+ if (removeBackups) paths.push(prepared.backupPath);
2090
+ return paths.filter((path) => Boolean(path)).map(async (path) => {
2091
+ try {
2092
+ await hooks.beforeCleanup?.(path);
2093
+ await rm(path, { force: true });
2094
+ } catch (error) {
2095
+ failures.push({
2096
+ path,
2097
+ error: error instanceof Error ? error.message : String(error)
2098
+ });
2099
+ }
2100
+ });
2101
+ }));
2102
+ return failures;
2103
+ }
2104
+ async function applyChangePlan(plan$1, hooks = {}) {
2105
+ const transactionId = `${process.pid}-${Date.now()}-${randomUUID()}`;
2106
+ const preparedChanges = [];
2107
+ const applied = [];
2108
+ try {
2109
+ for (const change of plan$1.changes) await assertUnchanged(change);
2110
+ for (const [index, change] of plan$1.changes.entries()) preparedChanges.push(await prepareChange(change, transactionId, index));
2111
+ for (const [index, prepared] of preparedChanges.entries()) {
2112
+ await hooks.beforeCommit?.(prepared.change, index);
2113
+ await commitChange(prepared, transactionId, index, hooks);
2114
+ applied.push(prepared);
2115
+ }
2116
+ const cleanupFailures = await cleanupPreparedChanges(preparedChanges, true, hooks);
2117
+ if (cleanupFailures.length > 0) throw new ChangePlanCleanupError({
2118
+ applied: applied.map((item) => item.change.path),
2119
+ cleanupFailures
1366
2120
  });
1367
- const serverLine = clientCommandOutput(result).split(/\r?\n/).find((line) => /^\s*(?:(?:[│┃]\s*)?[✓●○⚠✗×✔!]\s+){0,2}wot-ui(?=\s|$)/i.test(line));
1368
- const command = `${executable} mcp list`;
1369
- if (result.timedOut || result.error) return {
1370
- status: "failed",
1371
- message: result.error || "OpenCode registration check failed",
1372
- action: "Retry opencode mcp list and inspect the client process if it still fails.",
1373
- command
1374
- };
1375
- if (serverLine && /pending approval|approval.*pending|untrusted|not trusted/i.test(serverLine)) return {
1376
- status: "pending",
1377
- message: "OpenCode requires MCP approval",
1378
- action: "Approve the wot-ui MCP server in OpenCode and retry the doctor command.",
1379
- command
1380
- };
1381
- if (serverLine && /not initialized|disabled|disconnected|failed|error|needs authentication|needs client registration/i.test(serverLine)) return {
1382
- status: "failed",
1383
- message: "OpenCode recognizes the wot-ui MCP server, but it is not ready",
1384
- action: "Inspect wot-ui with opencode mcp list, then enable or repair the server.",
1385
- command
1386
- };
1387
- if (result.exitCode === 0 && serverLine && /\bconnected\b/i.test(serverLine)) return {
1388
- status: "ready",
1389
- message: "OpenCode recognizes the wot-ui MCP server",
1390
- command
1391
- };
1392
- if (result.exitCode === 0 && serverLine) return {
1393
- status: "unknown",
1394
- message: "OpenCode recognizes the wot-ui MCP server, but its connection status is unknown",
1395
- action: "Inspect wot-ui with opencode mcp list and confirm that it is connected.",
1396
- command
1397
- };
1398
2121
  return {
1399
- status: "failed",
1400
- message: "OpenCode did not recognize the wot-ui MCP server",
1401
- action: "Run opencode mcp list and inspect the configured scope.",
1402
- command
2122
+ changed: applied.length > 0,
2123
+ applied: applied.map((item) => item.change),
2124
+ rolledBack: false
1403
2125
  };
2126
+ } catch (error) {
2127
+ if (error instanceof ChangePlanCleanupError) throw error;
2128
+ const rollbackTargets = preparedChanges.filter((change) => change.touched).reverse();
2129
+ const rolledBack = [];
2130
+ const rollbackFailures = [];
2131
+ for (const [index, prepared] of rollbackTargets.entries()) try {
2132
+ await hooks.beforeRollback?.(prepared.change, index);
2133
+ await rollbackPreparedChange(prepared);
2134
+ rolledBack.push(prepared.change.path);
2135
+ } catch (rollbackError) {
2136
+ rollbackFailures.push({
2137
+ path: prepared.change.path,
2138
+ error: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
2139
+ ...prepared.backupCreated && prepared.backupPath ? { backupPath: prepared.backupPath } : {}
2140
+ });
2141
+ }
2142
+ const cleanupFailures = await cleanupPreparedChanges(preparedChanges, rollbackFailures.length === 0, hooks);
2143
+ throw new ChangePlanApplyError({
2144
+ cause: error,
2145
+ applied: applied.map((item) => item.change.path),
2146
+ rolledBack,
2147
+ rollbackFailures,
2148
+ cleanupFailures
2149
+ });
2150
+ }
2151
+ }
2152
+ function mergeChangePlans(summary, plans) {
2153
+ const changes = [];
2154
+ const targets = /* @__PURE__ */ new Map();
2155
+ for (const change of plans.flatMap((plan$1) => plan$1.changes)) {
2156
+ const existing = targets.get(change.path);
2157
+ if (!existing) {
2158
+ targets.set(change.path, change);
2159
+ changes.push(change);
2160
+ continue;
2161
+ }
2162
+ 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}`);
2163
+ }
2164
+ return {
2165
+ id: plans.map((plan$1) => plan$1.id).join("+") || "empty",
2166
+ summary,
2167
+ changes,
2168
+ warnings: plans.flatMap((plan$1) => plan$1.warnings),
2169
+ requiresConfirmation: plans.some((plan$1) => plan$1.requiresConfirmation)
2170
+ };
2171
+ }
2172
+ function formatChangePlan(plan$1, cwd = process.cwd()) {
2173
+ const lines = [plan$1.summary];
2174
+ if (plan$1.changes.length === 0) lines.push("No changes required.");
2175
+ for (const change of plan$1.changes) {
2176
+ const path = relative(cwd, change.path) || change.path;
2177
+ lines.push(`${change.type}: ${path}`, ` ${change.reason}`);
2178
+ lines.push(...getSafePreview(change).map((line) => ` ${line}`));
2179
+ }
2180
+ for (const warning of plan$1.warnings) lines.push(`warning: ${warning}`);
2181
+ return lines.join("\n");
2182
+ }
2183
+ function getSafePreview(change) {
2184
+ if (change.preview?.length) return change.preview;
2185
+ if (change.type === "delete-file" || change.after === void 0) return ["- delete file"];
2186
+ return [change.before === void 0 ? "+ create file" : "~ update file"];
2187
+ }
2188
+ function toPublicChangePlan(plan$1, cwd = process.cwd()) {
2189
+ return {
2190
+ id: plan$1.id,
2191
+ summary: plan$1.summary,
2192
+ changes: plan$1.changes.map((change) => ({
2193
+ type: change.type,
2194
+ path: relative(cwd, change.path) || change.path,
2195
+ reason: change.reason,
2196
+ preview: getSafePreview(change)
2197
+ })),
2198
+ warnings: [...plan$1.warnings],
2199
+ requiresConfirmation: plan$1.requiresConfirmation
2200
+ };
2201
+ }
2202
+ async function confirmChangePlan(plan$1, options = {}) {
2203
+ if (plan$1.changes.length === 0 || options.yes) return true;
2204
+ 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.");
2205
+ const prompt = createInterface({
2206
+ input: process.stdin,
2207
+ output: process.stdout
2208
+ });
2209
+ try {
2210
+ process.stdout.write(`${formatChangePlan(plan$1, options.cwd)}\n`);
2211
+ const answer = await prompt.question(`Apply ${plan$1.changes.length} change${plan$1.changes.length === 1 ? "" : "s"}? [y/N] `);
2212
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
2213
+ } finally {
2214
+ prompt.close();
1404
2215
  }
1405
- };
1406
-
1407
- //#endregion
1408
- //#region src/mcp/clients/vscode.ts
1409
- const vscodeAdapter = new JsonMcpClientAdapter({
1410
- id: "vscode",
1411
- displayName: "VS Code",
1412
- executableNames: ["code"],
1413
- supportedScopes: ["project"],
1414
- configPath: (context) => resolve(context.cwd, ".vscode", "mcp.json"),
1415
- serverKey: "servers"
1416
- });
1417
-
1418
- //#endregion
1419
- //#region src/mcp/clients/index.ts
1420
- const mcpClientAdapters = [
1421
- claudeAdapter,
1422
- cursorAdapter,
1423
- vscodeAdapter,
1424
- codexAdapter,
1425
- opencodeAdapter,
1426
- antigravityAdapter
1427
- ];
1428
- function getMcpClientAdapter(id) {
1429
- const adapter = mcpClientAdapters.find((candidate) => candidate.id === id);
1430
- if (!adapter) throw new Error(`Unsupported MCP client: ${id}`);
1431
- return adapter;
1432
2216
  }
1433
2217
 
1434
2218
  //#endregion
@@ -1479,12 +2263,28 @@ async function inspectMcpClients(client, options = {}) {
1479
2263
  async function planMcpInstall(client, options = {}) {
1480
2264
  const adapters = await resolveMcpAdapters(client, options);
1481
2265
  const context = createPlanContext(options);
1482
- return Promise.all(adapters.map((adapter) => adapter.planInstall(context)));
2266
+ const plans = await Promise.all(adapters.map((adapter) => planSelectedClient(client, adapter.id, () => adapter.planInstall(context))));
2267
+ await reconcileCodebuddyConfigPlans(plans, context);
2268
+ return plans;
1483
2269
  }
1484
2270
  async function planMcpRemove(client, options = {}) {
1485
2271
  const adapters = await resolveMcpAdapters(client, options);
1486
2272
  const context = createPlanContext(options);
1487
- return Promise.all(adapters.map((adapter) => adapter.planRemove(context)));
2273
+ return Promise.all(adapters.map((adapter) => planSelectedClient(client, adapter.id, () => adapter.planRemove(context))));
2274
+ }
2275
+ async function planSelectedClient(selection, client, plan$1) {
2276
+ try {
2277
+ return await plan$1();
2278
+ } catch (error) {
2279
+ if (selection !== "all" || client !== "deepseek" || !(error instanceof DeepseekProjectLocationError)) throw error;
2280
+ return {
2281
+ id: "deepseek-skipped-location",
2282
+ summary: "Skip DeepSeek at this directory",
2283
+ changes: [],
2284
+ warnings: [`Skipped DeepSeek: ${error.message}`],
2285
+ requiresConfirmation: false
2286
+ };
2287
+ }
1488
2288
  }
1489
2289
  async function printMcpConfig(client, options = {}) {
1490
2290
  return getMcpClientAdapter(client).print(createPlanContext(options));
@@ -1500,6 +2300,22 @@ function toPublicClientConfigState(state) {
1500
2300
 
1501
2301
  //#endregion
1502
2302
  //#region src/application/agent-onboarding.ts
2303
+ async function agentSkillLocationProblem(projectDir, client) {
2304
+ if (client !== "deepseek") return void 0;
2305
+ const cwd = resolve(projectDir);
2306
+ let current = cwd;
2307
+ while (true) {
2308
+ if (await exists(join(current, ".git"))) return current === cwd ? void 0 : `DeepSeek discovers project Skills at the Git root. Retry with --cwd ${JSON.stringify(current)}.`;
2309
+ const parent = dirname(current);
2310
+ if (parent === current) return void 0;
2311
+ current = parent;
2312
+ }
2313
+ }
2314
+ async function assertSkillLocation(options, cwd) {
2315
+ if (!options.capabilities.includes("skill")) return;
2316
+ const problem = await agentSkillLocationProblem(cwd, options.client);
2317
+ if (problem) throw new DeepseekProjectLocationError(problem);
2318
+ }
1503
2319
  const INSTRUCTIONS_START = "<!-- open-wot agent instructions start -->";
1504
2320
  const INSTRUCTIONS_END = "<!-- open-wot agent instructions end -->";
1505
2321
  function findManagedInstructionsRange(content) {
@@ -1525,18 +2341,28 @@ async function readOptional(path) {
1525
2341
  throw error;
1526
2342
  }
1527
2343
  }
2344
+ function skillDirectory(client) {
2345
+ if (client === "claude") return ".claude";
2346
+ if (client === "qoder" || client === "qoder-cli") return ".qoder";
2347
+ if (client === "codebuddy" || client === "codebuddy-cli") return ".codebuddy";
2348
+ return ".agents";
2349
+ }
2350
+ function skillReference(client) {
2351
+ return `${skillDirectory(client)}/skills/wot-ui-v2/SKILL.md`;
2352
+ }
1528
2353
  function getSkillTarget(projectDir, client) {
1529
- return client === "claude" ? resolve(projectDir, ".claude", "skills", "wot-ui-v2") : resolve(projectDir, ".agents", "skills", "wot-ui-v2");
2354
+ return resolve(projectDir, skillDirectory(client), "skills", "wot-ui-v2");
1530
2355
  }
1531
2356
  function getInstructionsTarget(projectDir, client) {
1532
- return resolve(projectDir, client === "claude" ? "CLAUDE.md" : "AGENTS.md");
2357
+ return resolve(projectDir, client === "claude" ? "CLAUDE.md" : client === "codebuddy" || client === "codebuddy-cli" ? "CODEBUDDY.md" : "AGENTS.md");
1533
2358
  }
1534
- function createAgentInstructions(client) {
2359
+ function createAgentInstructions(client, references = [skillReference(client)]) {
2360
+ const skills = [...new Set(references)].sort();
1535
2361
  return [
1536
2362
  INSTRUCTIONS_START,
1537
2363
  "## Wot UI Agent Instructions",
1538
2364
  "",
1539
- `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.`,
2365
+ `Before generating or modifying wot-ui component code, ${skills.length === 1 ? `read the project Skill at \`${skills[0]}\`` : `read the project Skill for your client at ${skills.map((skill) => `\`${skill}\``).join(" or ")}`} and query the configured \`wot-ui\` MCP server for version-accurate APIs and examples.`,
1540
2366
  "",
1541
2367
  INSTRUCTIONS_END
1542
2368
  ].join("\n");
@@ -1566,11 +2392,11 @@ async function listFiles(root, current = root) {
1566
2392
  if (!await exists(current)) return [];
1567
2393
  const entries = await readdir(current, { withFileTypes: true });
1568
2394
  const files = [];
1569
- for (const entry of entries) {
1570
- if (entry.name === ".DS_Store") continue;
1571
- const path = join(current, entry.name);
1572
- if (entry.isDirectory()) files.push(...await listFiles(root, path));
1573
- else if (entry.isFile()) files.push(relative(root, path));
2395
+ for (const entry$2 of entries) {
2396
+ if (entry$2.name === ".DS_Store") continue;
2397
+ const path = join(current, entry$2.name);
2398
+ if (entry$2.isDirectory()) files.push(...await listFiles(root, path));
2399
+ else if (entry$2.isFile()) files.push(relative(root, path));
1574
2400
  }
1575
2401
  return files.sort();
1576
2402
  }
@@ -1636,10 +2462,19 @@ async function planSkillRemove(projectDir, client) {
1636
2462
  requiresConfirmation: changes.length > 0
1637
2463
  };
1638
2464
  }
1639
- async function planInstructions(projectDir, client, remove) {
2465
+ async function planInstructions(projectDir, client, remove, clients = [client]) {
1640
2466
  const path = getInstructionsTarget(projectDir, client);
1641
2467
  const before = await readOptional(path);
1642
- const after = remove ? removeManagedInstructions(before ?? "") : updateManagedInstructions(before ?? "", createAgentInstructions(client));
2468
+ const range = findManagedInstructionsRange(before ?? "");
2469
+ const block = range ? before.slice(range.start, range.end) : "";
2470
+ const candidates = MCP_CLIENT_IDS.filter((candidate) => getInstructionsTarget(projectDir, candidate) === path);
2471
+ const existing = candidates.filter((candidate) => block.includes(`\`${skillReference(candidate)}\``)).map(skillReference);
2472
+ const selected = candidates.filter((candidate) => clients.includes(candidate)).map(skillReference);
2473
+ const references = remove ? existing.filter((reference) => !selected.includes(reference)) : [...existing, ...selected];
2474
+ let after = before ?? "";
2475
+ if (!remove || references.length > 0 && existing.some((reference) => selected.includes(reference))) after = updateManagedInstructions(after, createAgentInstructions(client, references));
2476
+ else if (references.length === 0) after = removeManagedInstructions(after);
2477
+ const removeBlock = remove && references.length === 0;
1643
2478
  return {
1644
2479
  id: `${client}-instructions-${remove ? "remove" : "install"}`,
1645
2480
  summary: `${remove ? "Remove" : "Install"} managed Agent instructions for ${client}`,
@@ -1649,8 +2484,8 @@ async function planInstructions(projectDir, client, remove) {
1649
2484
  allowedRoot: projectDir,
1650
2485
  before,
1651
2486
  after,
1652
- reason: `${remove ? "Remove" : "Update"} only the open-wot managed instructions block`,
1653
- preview: [remove ? "- open-wot managed instructions block" : "+ open-wot managed instructions block"]
2487
+ reason: `${removeBlock ? "Remove" : "Update"} only the open-wot managed instructions block`,
2488
+ preview: [removeBlock ? "- open-wot managed instructions block" : remove ? "~ remove selected Skill references; keep other clients" : "+ open-wot managed instructions block"]
1654
2489
  }],
1655
2490
  warnings: [],
1656
2491
  requiresConfirmation: (before ?? "") !== after
@@ -1658,19 +2493,29 @@ async function planInstructions(projectDir, client, remove) {
1658
2493
  }
1659
2494
  async function planAgentInit(options) {
1660
2495
  const context = createDetectContext(options);
2496
+ await assertSkillLocation(options, context.cwd);
1661
2497
  const plans = [];
1662
2498
  if (options.capabilities.includes("mcp")) plans.push(...await planMcpInstall(options.client, options));
1663
2499
  if (options.capabilities.includes("skill")) plans.push(await planSkillInstall(context.cwd, options.client));
1664
- if (options.capabilities.includes("instructions")) plans.push(await planInstructions(context.cwd, options.client, false));
2500
+ if (options.capabilities.includes("instructions")) plans.push(await planInstructions(context.cwd, options.client, false, options.instructionClients));
1665
2501
  return mergeChangePlans(`Initialize wot-ui Agent integration for ${options.client}`, plans);
1666
2502
  }
1667
2503
  async function planAgentRemove(options) {
1668
2504
  const context = createDetectContext(options);
2505
+ await assertSkillLocation(options, context.cwd);
1669
2506
  const plans = [];
1670
2507
  if (options.capabilities.includes("mcp")) plans.push(...await planMcpRemove(options.client, options));
1671
2508
  if (options.capabilities.includes("skill")) plans.push(await planSkillRemove(context.cwd, options.client));
1672
- if (options.capabilities.includes("instructions")) plans.push(await planInstructions(context.cwd, options.client, true));
1673
- return mergeChangePlans(`Remove wot-ui Agent integration for ${options.client}`, plans);
2509
+ if (options.capabilities.includes("instructions")) plans.push(await planInstructions(context.cwd, options.client, true, options.instructionClients));
2510
+ const plan$1 = mergeChangePlans(`Remove wot-ui Agent integration for ${options.client}`, plans);
2511
+ if (options.client === "deepseek" && options.capabilities.some((capability) => capability !== "mcp")) plan$1.warnings.push("Project Skills and AGENTS.md instructions are shared with other agents; removing them affects those agents too.");
2512
+ if ([
2513
+ "qoder",
2514
+ "qoder-cli",
2515
+ "codebuddy",
2516
+ "codebuddy-cli"
2517
+ ].includes(options.client) && options.capabilities.some((capability) => capability !== "mcp")) plan$1.warnings.push("Project Skills and instructions may be shared by IDE, CLI, or other agents; removing them affects those clients too.");
2518
+ return plan$1;
1674
2519
  }
1675
2520
  async function inspectAgentSkill(projectDir, client) {
1676
2521
  const source = resolveBundledWotSkill();
@@ -1690,7 +2535,8 @@ async function inspectAgentInstructions(projectDir, client) {
1690
2535
  const instructions = await readOptional(getInstructionsTarget(projectDir, client));
1691
2536
  if (!instructions) return { installed: false };
1692
2537
  try {
1693
- return { installed: Boolean(findManagedInstructionsRange(instructions)) };
2538
+ const range = findManagedInstructionsRange(instructions);
2539
+ return { installed: Boolean(range && instructions.slice(range.start, range.end).includes(`\`${skillReference(client)}\``)) };
1694
2540
  } catch {
1695
2541
  return { installed: false };
1696
2542
  }
@@ -2091,6 +2937,7 @@ async function inspectCapabilities(client, capabilities, options) {
2091
2937
  capabilities.includes("instructions") ? inspectAgentInstructions(projectDir, client) : Promise.resolve(void 0)
2092
2938
  ]);
2093
2939
  const mcp = mcpStates[0];
2940
+ const skillProblem = capabilities.includes("skill") ? await agentSkillLocationProblem(projectDir, client) : void 0;
2094
2941
  return {
2095
2942
  client,
2096
2943
  capabilities: {
@@ -2099,9 +2946,10 @@ async function inspectCapabilities(client, capabilities, options) {
2099
2946
  state: mcp ? toPublicClientConfigState(mcp) : void 0
2100
2947
  } : { status: "skipped" },
2101
2948
  skill: capabilities.includes("skill") ? {
2102
- status: skill?.matches ? "pass" : "fail",
2949
+ status: skill?.matches && !skillProblem ? "pass" : "fail",
2103
2950
  installed: skill?.installed,
2104
- matches: skill?.matches
2951
+ matches: Boolean(skill?.matches) && !skillProblem,
2952
+ ...skillProblem ? { problem: skillProblem } : {}
2105
2953
  } : { status: "skipped" },
2106
2954
  instructions: capabilities.includes("instructions") ? {
2107
2955
  status: instructions?.installed ? "pass" : "fail",
@@ -2122,35 +2970,43 @@ function reportHasWarning(report) {
2122
2970
  function formatCapabilityReport(report) {
2123
2971
  const state = reportFailed(report) ? "incomplete" : reportNeedsAction(report) ? "action-required" : reportHasWarning(report) ? "warning" : "ok";
2124
2972
  const capabilities = Object.entries(report.capabilities).map(([name$1, capability]) => `${name$1}=${capability.status}`).join(" ");
2125
- return `${state} ${report.client}: ${capabilities}`;
2973
+ const problems = Object.values(report.capabilities).flatMap((capability) => typeof capability.problem === "string" ? [` ${capability.problem}`] : []);
2974
+ return [`${state} ${report.client}: ${capabilities}`, ...problems].join("\n");
2126
2975
  }
2127
2976
  function registerAgentCommand(program) {
2128
2977
  const agent = program.command("agent").description("Connect AI agents to wot-ui MCP and Skills");
2129
2978
  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 () => {
2130
2979
  const clients = await resolveClients(options);
2131
2980
  const capabilities = parseCapabilities(options.with);
2132
- const plan = mergeChangePlans("Initialize wot-ui Agent integration", await Promise.all(clients.map((client) => planAgentInit({
2981
+ const plans = await Promise.all(clients.map((client) => planSelectedClient(parseClient$1(options.client), client, () => planAgentInit({
2133
2982
  client,
2983
+ instructionClients: clients,
2134
2984
  capabilities,
2135
2985
  cwd: options.cwd,
2136
2986
  scope: parseScope$1(options.scope),
2137
2987
  pin: options.pin
2138
2988
  }))));
2989
+ await reconcileCodebuddyConfigPlans(plans, createPlanContext({
2990
+ cwd: options.cwd,
2991
+ scope: parseScope$1(options.scope),
2992
+ pin: options.pin
2993
+ }));
2994
+ const plan$1 = mergeChangePlans("Initialize wot-ui Agent integration", plans);
2139
2995
  if (!options.dryRun) {
2140
- if (!await confirmChangePlan(plan, {
2996
+ if (!await confirmChangePlan(plan$1, {
2141
2997
  yes: options.yes,
2142
2998
  cwd: options.cwd
2143
2999
  })) {
2144
3000
  writeLine("Cancelled.");
2145
3001
  return;
2146
3002
  }
2147
- await applyChangePlan(plan);
3003
+ await applyChangePlan(plan$1);
2148
3004
  }
2149
3005
  if (options.format === "json") writeJson({
2150
3006
  dryRun: Boolean(options.dryRun),
2151
- plan: toPublicChangePlan(plan, options.cwd)
3007
+ plan: toPublicChangePlan(plan$1, options.cwd)
2152
3008
  });
2153
- else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
3009
+ else writeLine(`${formatChangePlan(plan$1, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
2154
3010
  }));
2155
3011
  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 () => {
2156
3012
  const clients = await detectMcpClients({ cwd: options.cwd });
@@ -2201,28 +3057,29 @@ function registerAgentCommand(program) {
2201
3057
  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 () => {
2202
3058
  const clients = await resolveClients(options);
2203
3059
  const capabilities = parseCapabilities(options.with);
2204
- const plan = mergeChangePlans("Remove wot-ui Agent integration", await Promise.all(clients.map((client) => planAgentRemove({
3060
+ const plan$1 = mergeChangePlans("Remove wot-ui Agent integration", await Promise.all(clients.map((client) => planSelectedClient(parseClient$1(options.client), client, () => planAgentRemove({
2205
3061
  client,
3062
+ instructionClients: clients,
2206
3063
  capabilities,
2207
3064
  cwd: options.cwd,
2208
3065
  scope: parseScope$1(options.scope),
2209
3066
  pin: options.pin
2210
- }))));
3067
+ })))));
2211
3068
  if (!options.dryRun) {
2212
- if (!await confirmChangePlan(plan, {
3069
+ if (!await confirmChangePlan(plan$1, {
2213
3070
  yes: options.yes,
2214
3071
  cwd: options.cwd
2215
3072
  })) {
2216
3073
  writeLine("Cancelled.");
2217
3074
  return;
2218
3075
  }
2219
- await applyChangePlan(plan);
3076
+ await applyChangePlan(plan$1);
2220
3077
  }
2221
3078
  if (options.format === "json") writeJson({
2222
3079
  dryRun: Boolean(options.dryRun),
2223
- plan: toPublicChangePlan(plan, options.cwd)
3080
+ plan: toPublicChangePlan(plan$1, options.cwd)
2224
3081
  });
2225
- else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
3082
+ else writeLine(`${formatChangePlan(plan$1, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
2226
3083
  }));
2227
3084
  }
2228
3085
 
@@ -2238,19 +3095,19 @@ function registerChangelogCommand(program) {
2238
3095
  if (versionOrComponent && /^v?\d/.test(versionOrComponent)) versionFilter = versionOrComponent;
2239
3096
  else if (versionOrComponent) componentFilter = versionOrComponent;
2240
3097
  if (componentArg) componentFilter = componentArg;
2241
- const entries = (metadata.changelog ?? []).filter((entry) => {
2242
- const versionMatches = versionFilter ? entry.version === versionFilter || `v${entry.version}` === versionFilter : true;
2243
- const componentMatches = componentFilter ? (entry.components ?? []).some((component) => component.toLowerCase() === componentFilter.toLowerCase()) : true;
3098
+ const entries = (metadata.changelog ?? []).filter((entry$2) => {
3099
+ const versionMatches = versionFilter ? entry$2.version === versionFilter || `v${entry$2.version}` === versionFilter : true;
3100
+ const componentMatches = componentFilter ? (entry$2.components ?? []).some((component) => component.toLowerCase() === componentFilter.toLowerCase()) : true;
2244
3101
  return versionMatches && componentMatches;
2245
3102
  });
2246
3103
  if (query.format === "json") {
2247
3104
  writeJson({ entries });
2248
3105
  return;
2249
3106
  }
2250
- writeLine(entries.flatMap((entry) => [
2251
- `${entry.version}${entry.date ? ` (${entry.date})` : ""}`,
2252
- entry.summary,
2253
- ...entry.highlights.map((item) => `- ${item}`),
3107
+ writeLine(entries.flatMap((entry$2) => [
3108
+ `${entry$2.version}${entry$2.date ? ` (${entry$2.date})` : ""}`,
3109
+ entry$2.summary,
3110
+ ...entry$2.highlights.map((item) => `- ${item}`),
2254
3111
  ""
2255
3112
  ]).join("\n").trim());
2256
3113
  } catch (error) {
@@ -2527,7 +3384,7 @@ async function runSafely(options, action) {
2527
3384
  }
2528
3385
  }
2529
3386
  async function startServer() {
2530
- const { startMcpServer } = await import("./server-TCmh-5rB.mjs");
3387
+ const { startMcpServer } = await import("./server-CxOzENpT.mjs");
2531
3388
  await startMcpServer();
2532
3389
  }
2533
3390
  function registerMcpCommand(program) {
@@ -2536,26 +3393,26 @@ function registerMcpCommand(program) {
2536
3393
  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 () => {
2537
3394
  const client = parseClient(options.client);
2538
3395
  const scope = parseScope(options.scope);
2539
- const plan = mergeChangePlans("Initialize wot-ui MCP integration", await planMcpInstall(client, {
3396
+ const plan$1 = mergeChangePlans("Initialize wot-ui MCP integration", await planMcpInstall(client, {
2540
3397
  cwd: options.cwd,
2541
3398
  scope,
2542
3399
  pin: options.pin
2543
3400
  }));
2544
3401
  if (!options.dryRun) {
2545
- if (!await confirmChangePlan(plan, {
3402
+ if (!await confirmChangePlan(plan$1, {
2546
3403
  yes: options.yes,
2547
3404
  cwd: options.cwd
2548
3405
  })) {
2549
3406
  writeLine("Cancelled.");
2550
3407
  return;
2551
3408
  }
2552
- await applyChangePlan(plan);
3409
+ await applyChangePlan(plan$1);
2553
3410
  }
2554
3411
  if (outputFormat(options.format) === "json") writeJson({
2555
3412
  dryRun: Boolean(options.dryRun),
2556
- plan: toPublicChangePlan(plan, options.cwd)
3413
+ plan: toPublicChangePlan(plan$1, options.cwd)
2557
3414
  });
2558
- else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
3415
+ else writeLine(`${formatChangePlan(plan$1, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
2559
3416
  }));
2560
3417
  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 () => {
2561
3418
  const detections = await detectMcpClients({ cwd: options.cwd });
@@ -2574,26 +3431,26 @@ function registerMcpCommand(program) {
2574
3431
  if (states.some((state) => !state.matches)) process.exitCode = 1;
2575
3432
  }));
2576
3433
  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 () => {
2577
- const plan = mergeChangePlans("Remove wot-ui MCP integration", await planMcpRemove(parseClient(options.client), {
3434
+ const plan$1 = mergeChangePlans("Remove wot-ui MCP integration", await planMcpRemove(parseClient(options.client), {
2578
3435
  cwd: options.cwd,
2579
3436
  scope: parseScope(options.scope),
2580
3437
  pin: options.pin
2581
3438
  }));
2582
3439
  if (!options.dryRun) {
2583
- if (!await confirmChangePlan(plan, {
3440
+ if (!await confirmChangePlan(plan$1, {
2584
3441
  yes: options.yes,
2585
3442
  cwd: options.cwd
2586
3443
  })) {
2587
3444
  writeLine("Cancelled.");
2588
3445
  return;
2589
3446
  }
2590
- await applyChangePlan(plan);
3447
+ await applyChangePlan(plan$1);
2591
3448
  }
2592
3449
  if (outputFormat(options.format) === "json") writeJson({
2593
3450
  dryRun: Boolean(options.dryRun),
2594
- plan: toPublicChangePlan(plan, options.cwd)
3451
+ plan: toPublicChangePlan(plan$1, options.cwd)
2595
3452
  });
2596
- else writeLine(`${formatChangePlan(plan, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
3453
+ else writeLine(`${formatChangePlan(plan$1, options.cwd)}${options.dryRun ? "\nDry run: no files were changed." : ""}`);
2597
3454
  }));
2598
3455
  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 () => {
2599
3456
  const reports = await doctorMcpClients(parseClient(options.client), {