@wot-ui/cli 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/data/v2.4.0.json.gz +0 -0
- package/data/v2.json.gz +0 -0
- package/data/versions.json +2 -1
- package/data/versions.json.gz +0 -0
- package/dist/index.mjs +1304 -447
- package/dist/{server-TCmh-5rB.mjs → server-725tR0x7.mjs} +1 -1
- package/dist/{update-check-DjA3lf-3.mjs → update-check-DHUhoS1o.mjs} +1 -1
- package/package.json +4 -1
- package/runtime/deepseek-project-bridge.mjs +197 -0
- package/skills/wot-ui-cli/references/overview.md +6 -2
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-
|
|
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-DHUhoS1o.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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
const preparedChanges = [];
|
|
168
|
-
const applied = [];
|
|
28
|
+
function parseDeclaration(source) {
|
|
29
|
+
let value;
|
|
169
30
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
209
|
+
configured: entry$2 !== void 0,
|
|
210
|
+
matches: !serverProblem$4 && serverMatches$2(server, context.server),
|
|
407
211
|
...server ? { server } : {},
|
|
408
|
-
...serverProblem$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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/
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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: `${
|
|
1653
|
-
preview: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
3387
|
+
const { startMcpServer } = await import("./server-725tR0x7.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), {
|