@stablekernel/opencode-cursor 0.8.0 → 0.9.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +110 -11
- package/dist/plugin/index.js +880 -24
- package/dist/plugin/index.js.map +1 -1
- package/dist/sidecar/plugin-tools-mcp.d.ts +189 -0
- package/dist/sidecar/plugin-tools-mcp.js +163 -0
- package/dist/sidecar/plugin-tools-mcp.js.map +1 -0
- package/package.json +7 -7
package/dist/plugin/index.js
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from "../chunk-YIEC27VB.js";
|
|
19
19
|
|
|
20
20
|
// src/plugin/index.ts
|
|
21
|
-
import { rmSync as
|
|
21
|
+
import { rmSync as rmSync4 } from "fs";
|
|
22
|
+
import { homedir as homedir5 } from "os";
|
|
22
23
|
import semver2 from "semver";
|
|
23
24
|
|
|
24
25
|
// src/model-limits.ts
|
|
@@ -813,7 +814,7 @@ function clearVersionCache() {
|
|
|
813
814
|
}
|
|
814
815
|
}
|
|
815
816
|
function getLocalVersion() {
|
|
816
|
-
if (true) return "0.
|
|
817
|
+
if (true) return "0.9.0-next.0";
|
|
817
818
|
try {
|
|
818
819
|
const require2 = createRequire(import.meta.url);
|
|
819
820
|
const pkg = require2("../package.json");
|
|
@@ -869,11 +870,11 @@ async function getLatestVersion() {
|
|
|
869
870
|
|
|
870
871
|
// src/provider/skill-mirror.ts
|
|
871
872
|
import {
|
|
872
|
-
mkdirSync as
|
|
873
|
-
writeFileSync as
|
|
873
|
+
mkdirSync as mkdirSync4,
|
|
874
|
+
writeFileSync as writeFileSync4,
|
|
874
875
|
readFileSync as readFileSync4,
|
|
875
876
|
existsSync as existsSync2,
|
|
876
|
-
rmSync as
|
|
877
|
+
rmSync as rmSync3,
|
|
877
878
|
readdirSync as readdirSync2,
|
|
878
879
|
statSync as statSync2,
|
|
879
880
|
copyFileSync,
|
|
@@ -887,10 +888,20 @@ import {
|
|
|
887
888
|
readFileSync as readFileSync3,
|
|
888
889
|
statSync,
|
|
889
890
|
existsSync,
|
|
890
|
-
realpathSync
|
|
891
|
+
realpathSync,
|
|
892
|
+
mkdtempSync,
|
|
893
|
+
mkdirSync as mkdirSync3,
|
|
894
|
+
rmSync as rmSync2,
|
|
895
|
+
writeFileSync as writeFileSync3
|
|
891
896
|
} from "fs";
|
|
892
|
-
import {
|
|
893
|
-
|
|
897
|
+
import {
|
|
898
|
+
join as join3,
|
|
899
|
+
relative,
|
|
900
|
+
dirname,
|
|
901
|
+
resolve as resolvePath,
|
|
902
|
+
isAbsolute
|
|
903
|
+
} from "path";
|
|
904
|
+
import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
|
|
894
905
|
import { execSync } from "child_process";
|
|
895
906
|
function parseFrontmatter(content) {
|
|
896
907
|
if (!content.startsWith("---")) return {};
|
|
@@ -915,6 +926,112 @@ function parseFrontmatter(content) {
|
|
|
915
926
|
}
|
|
916
927
|
var SKILL_DIR_NAMES = ["skill", "skills"];
|
|
917
928
|
var EXTERNAL_DIR_NAMES = [".claude", ".agents"];
|
|
929
|
+
var FILE_PLUGIN_DIR_NAMES = ["plugin", "plugins"];
|
|
930
|
+
var PLUGIN_SKILL_DIR_NAMES = ["skills", "skill"];
|
|
931
|
+
function opencodePackagesRoot(home = homedir3()) {
|
|
932
|
+
if (process.platform === "win32") {
|
|
933
|
+
return join3(
|
|
934
|
+
process.env.LocalAppData ?? join3(home, "AppData", "Local"),
|
|
935
|
+
"opencode",
|
|
936
|
+
"cache",
|
|
937
|
+
"packages"
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
return join3(
|
|
941
|
+
process.env.XDG_CACHE_HOME ?? join3(home, ".cache"),
|
|
942
|
+
"opencode",
|
|
943
|
+
"packages"
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
function pluginCacheSkillDirs(entry) {
|
|
947
|
+
const dirs = [];
|
|
948
|
+
const visited = /* @__PURE__ */ new Set();
|
|
949
|
+
function scanNodeModules(nodeModules) {
|
|
950
|
+
let entries;
|
|
951
|
+
try {
|
|
952
|
+
entries = readdirSync(nodeModules, { withFileTypes: true });
|
|
953
|
+
} catch {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
for (const ent of entries) {
|
|
957
|
+
if (ent.name === ".bin") continue;
|
|
958
|
+
const fullPath = join3(nodeModules, ent.name);
|
|
959
|
+
if (entryKind(ent, fullPath) !== "dir") continue;
|
|
960
|
+
if (ent.name.startsWith("@")) {
|
|
961
|
+
scanNodeModules(fullPath);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
for (const skillName of SKILL_DIR_NAMES) {
|
|
965
|
+
const candidate = join3(fullPath, skillName);
|
|
966
|
+
if (existsSync(candidate)) dirs.push(candidate);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function findNodeModules(dir, depth) {
|
|
971
|
+
if (depth > 5) return;
|
|
972
|
+
let realDir;
|
|
973
|
+
try {
|
|
974
|
+
realDir = realpathSync(dir);
|
|
975
|
+
} catch {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (visited.has(realDir)) return;
|
|
979
|
+
visited.add(realDir);
|
|
980
|
+
const nm = join3(dir, "node_modules");
|
|
981
|
+
if (existsSync(nm)) {
|
|
982
|
+
scanNodeModules(nm);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
let entries;
|
|
986
|
+
try {
|
|
987
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
988
|
+
} catch {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
for (const ent of entries) {
|
|
992
|
+
const fullPath = join3(dir, ent.name);
|
|
993
|
+
if (entryKind(ent, fullPath) === "dir") {
|
|
994
|
+
findNodeModules(fullPath, depth + 1);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
findNodeModules(entry, 0);
|
|
999
|
+
return dirs;
|
|
1000
|
+
}
|
|
1001
|
+
function pluginCacheEntries(root) {
|
|
1002
|
+
let entries;
|
|
1003
|
+
try {
|
|
1004
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
1005
|
+
} catch {
|
|
1006
|
+
return [];
|
|
1007
|
+
}
|
|
1008
|
+
const skip = /* @__PURE__ */ new Set([
|
|
1009
|
+
"node_modules",
|
|
1010
|
+
"package.json",
|
|
1011
|
+
"package-lock.json",
|
|
1012
|
+
"bun.lock",
|
|
1013
|
+
"bun.lockb"
|
|
1014
|
+
]);
|
|
1015
|
+
const out = [];
|
|
1016
|
+
for (const ent of entries) {
|
|
1017
|
+
if (skip.has(ent.name)) continue;
|
|
1018
|
+
const full = join3(root, ent.name);
|
|
1019
|
+
if (entryKind(ent, full) !== "dir") continue;
|
|
1020
|
+
out.push(full);
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
function discoverPluginSkillDirs(cacheRoot, home) {
|
|
1025
|
+
const root = cacheRoot ?? opencodePackagesRoot(home);
|
|
1026
|
+
if (!existsSync(root)) return [];
|
|
1027
|
+
const dirs = [];
|
|
1028
|
+
for (const entry of pluginCacheEntries(root)) {
|
|
1029
|
+
for (const skillDir of pluginCacheSkillDirs(entry)) {
|
|
1030
|
+
dirs.push(skillDir);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return dirs;
|
|
1034
|
+
}
|
|
918
1035
|
function worktreeRoot(cwd) {
|
|
919
1036
|
try {
|
|
920
1037
|
const root = execSync("git rev-parse --show-toplevel", {
|
|
@@ -1025,7 +1142,39 @@ function expandSkillPath(raw, cwd, home) {
|
|
|
1025
1142
|
if (isAbsolute(trimmed)) return trimmed;
|
|
1026
1143
|
return resolvePath(cwd, trimmed);
|
|
1027
1144
|
}
|
|
1028
|
-
function
|
|
1145
|
+
function discoverFilePluginSkillDirs(roots) {
|
|
1146
|
+
const dirs = [];
|
|
1147
|
+
for (const root of roots) {
|
|
1148
|
+
for (const sub of FILE_PLUGIN_DIR_NAMES) {
|
|
1149
|
+
const pluginDir = join3(root, sub);
|
|
1150
|
+
if (!existsSync(pluginDir)) continue;
|
|
1151
|
+
for (const skillName of PLUGIN_SKILL_DIR_NAMES) {
|
|
1152
|
+
const skillDir = join3(pluginDir, skillName);
|
|
1153
|
+
if (existsSync(skillDir)) dirs.push(skillDir);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return dirs;
|
|
1158
|
+
}
|
|
1159
|
+
function resolvePluginSkillSources(cwd) {
|
|
1160
|
+
const home = homedir3();
|
|
1161
|
+
const cacheRoot = opencodePackagesRoot(home);
|
|
1162
|
+
let filePluginRoots = [];
|
|
1163
|
+
try {
|
|
1164
|
+
const candidates = [];
|
|
1165
|
+
const start = cwd ?? process.cwd();
|
|
1166
|
+
const stop = worktreeRoot(start);
|
|
1167
|
+
for (const ancestor of walkUp(start, stop)) {
|
|
1168
|
+
candidates.push(join3(ancestor, ".opencode"));
|
|
1169
|
+
}
|
|
1170
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] || join3(home, ".config");
|
|
1171
|
+
candidates.push(join3(xdgConfig, "opencode"), home);
|
|
1172
|
+
filePluginRoots = discoverFilePluginSkillDirs(candidates).map((dir) => dirname(dirname(dir))).filter((v, i, arr) => arr.indexOf(v) === i);
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
return { cacheRoot, filePluginRoots };
|
|
1176
|
+
}
|
|
1177
|
+
function discoverSkills(cwd, extraPaths, options) {
|
|
1029
1178
|
const home = homedir3();
|
|
1030
1179
|
const xdgConfig = process.env["XDG_CONFIG_HOME"] || join3(home, ".config");
|
|
1031
1180
|
const stop = worktreeRoot(cwd);
|
|
@@ -1060,6 +1209,29 @@ function discoverSkills(cwd, extraPaths) {
|
|
|
1060
1209
|
scanRoots.push(expanded);
|
|
1061
1210
|
}
|
|
1062
1211
|
}
|
|
1212
|
+
for (const dir of discoverPluginSkillDirs(options?.cacheRoot, home)) {
|
|
1213
|
+
scanRoots.push(dir);
|
|
1214
|
+
}
|
|
1215
|
+
const filePluginRoots = options?.filePluginRoots;
|
|
1216
|
+
if (filePluginRoots) {
|
|
1217
|
+
for (const dir of discoverFilePluginSkillDirs(filePluginRoots)) {
|
|
1218
|
+
scanRoots.push(dir);
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
for (const ancestor of walkUp(cwd, stop)) {
|
|
1222
|
+
for (const dir of discoverFilePluginSkillDirs([
|
|
1223
|
+
join3(ancestor, ".opencode")
|
|
1224
|
+
])) {
|
|
1225
|
+
scanRoots.push(dir);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
for (const dir of discoverFilePluginSkillDirs([
|
|
1229
|
+
join3(xdgConfig, "opencode"),
|
|
1230
|
+
home
|
|
1231
|
+
])) {
|
|
1232
|
+
scanRoots.push(dir);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1063
1235
|
const byId = /* @__PURE__ */ new Map();
|
|
1064
1236
|
for (const dir of scanRoots) {
|
|
1065
1237
|
const found = scanSkillDir(dir);
|
|
@@ -1150,15 +1322,82 @@ function filterSkills(skills, config, options) {
|
|
|
1150
1322
|
}
|
|
1151
1323
|
return { skills: permitted, withheld };
|
|
1152
1324
|
}
|
|
1153
|
-
|
|
1325
|
+
var liveScratchRoot;
|
|
1326
|
+
var liveScratchCleanupRegistered = false;
|
|
1327
|
+
function liveScratchDir() {
|
|
1328
|
+
if (!liveScratchRoot) {
|
|
1329
|
+
liveScratchRoot = mkdtempSync(join3(tmpdir3(), "opencode-cursor-skills-"));
|
|
1330
|
+
if (!liveScratchCleanupRegistered) {
|
|
1331
|
+
liveScratchCleanupRegistered = true;
|
|
1332
|
+
const rootAtExit = liveScratchRoot;
|
|
1333
|
+
process.once("exit", () => {
|
|
1334
|
+
rmSync2(rootAtExit, { recursive: true, force: true });
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return liveScratchRoot;
|
|
1339
|
+
}
|
|
1340
|
+
function liveSkillsToDiscovered(live) {
|
|
1341
|
+
const out = [];
|
|
1342
|
+
for (const skill of live) {
|
|
1343
|
+
if (!skill.location || !skill.name) continue;
|
|
1344
|
+
const sourceDir = skill.location.endsWith("SKILL.md") ? dirname(skill.location) : skill.location;
|
|
1345
|
+
if (existsSync(join3(sourceDir, "SKILL.md"))) {
|
|
1346
|
+
const loaded2 = loadSkill(skill.name, sourceDir);
|
|
1347
|
+
if (loaded2) out.push(loaded2);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
if (!skill.content) continue;
|
|
1351
|
+
const scratchDir = join3(liveScratchDir(), skill.name);
|
|
1352
|
+
const scratchMd = join3(scratchDir, "SKILL.md");
|
|
1353
|
+
let needsWrite = true;
|
|
1354
|
+
if (existsSync(scratchMd)) {
|
|
1355
|
+
try {
|
|
1356
|
+
const existing = readFileSync3(scratchMd, "utf8");
|
|
1357
|
+
if (existing === renderLiveSkillMd(skill)) needsWrite = false;
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (needsWrite) {
|
|
1362
|
+
try {
|
|
1363
|
+
mkdirSync3(scratchDir, { recursive: true });
|
|
1364
|
+
writeFileSync3(scratchMd, renderLiveSkillMd(skill), "utf8");
|
|
1365
|
+
} catch {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const loaded = loadSkill(skill.name, scratchDir);
|
|
1370
|
+
if (loaded) out.push(loaded);
|
|
1371
|
+
}
|
|
1372
|
+
return out;
|
|
1373
|
+
}
|
|
1374
|
+
function renderLiveSkillMd(skill) {
|
|
1375
|
+
const escaped = (skill.description ?? "").replace(/"/g, '\\"');
|
|
1376
|
+
const body = skill.content ?? "";
|
|
1377
|
+
const separator = body.startsWith("\n") ? "" : "\n";
|
|
1378
|
+
return `---
|
|
1379
|
+
name: ${skill.name}
|
|
1380
|
+
description: "${escaped}"
|
|
1381
|
+
---
|
|
1382
|
+
${separator}${body}`;
|
|
1383
|
+
}
|
|
1384
|
+
function resolveSkills(cwd, config, options, discoveryOptions) {
|
|
1154
1385
|
const skillsConfig = config;
|
|
1155
1386
|
const extraPaths = skillsConfig?.skills?.paths;
|
|
1156
1387
|
let discovered;
|
|
1157
1388
|
try {
|
|
1158
|
-
discovered = discoverSkills(cwd, extraPaths);
|
|
1389
|
+
discovered = discoverSkills(cwd, extraPaths, discoveryOptions);
|
|
1159
1390
|
} catch {
|
|
1160
1391
|
discovered = [];
|
|
1161
1392
|
}
|
|
1393
|
+
if (discoveryOptions?.liveSkills?.length) {
|
|
1394
|
+
const seen = new Set(discovered.map((s2) => s2.id));
|
|
1395
|
+
for (const skill of liveSkillsToDiscovered(discoveryOptions.liveSkills)) {
|
|
1396
|
+
if (seen.has(skill.id)) continue;
|
|
1397
|
+
seen.add(skill.id);
|
|
1398
|
+
discovered.push(skill);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1162
1401
|
return filterSkills(discovered, config, options);
|
|
1163
1402
|
}
|
|
1164
1403
|
function skillSetHash(skills) {
|
|
@@ -1253,7 +1492,7 @@ function copyTree(srcDir, destDir, skillId, maxBytes, warn) {
|
|
|
1253
1492
|
);
|
|
1254
1493
|
continue;
|
|
1255
1494
|
}
|
|
1256
|
-
|
|
1495
|
+
mkdirSync4(dirname2(destPath), { recursive: true });
|
|
1257
1496
|
copyFileSync(srcPath, destPath);
|
|
1258
1497
|
bytes += size;
|
|
1259
1498
|
} catch {
|
|
@@ -1267,8 +1506,8 @@ function copyTree(srcDir, destDir, skillId, maxBytes, warn) {
|
|
|
1267
1506
|
function writeIfChanged(path, content) {
|
|
1268
1507
|
const existing = existsSync2(path) ? readFileSync4(path, "utf8") : void 0;
|
|
1269
1508
|
if (existing === content) return false;
|
|
1270
|
-
|
|
1271
|
-
|
|
1509
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1510
|
+
writeFileSync4(path, content, "utf8");
|
|
1272
1511
|
return true;
|
|
1273
1512
|
}
|
|
1274
1513
|
function ensureGitIgnored(dir, skillIds) {
|
|
@@ -1281,7 +1520,7 @@ function ensureGitIgnored(dir, skillIds) {
|
|
|
1281
1520
|
if (missing.length === 0) return;
|
|
1282
1521
|
const prefix = existing && !existing.endsWith("\n") ? `${existing}
|
|
1283
1522
|
` : existing;
|
|
1284
|
-
|
|
1523
|
+
writeFileSync4(path, `${prefix}${missing.join("\n")}
|
|
1285
1524
|
`, "utf8");
|
|
1286
1525
|
}
|
|
1287
1526
|
function writeSkillMirror(cwd, skills, warn) {
|
|
@@ -1389,7 +1628,7 @@ function pruneStale(cwd, currentIds, warn) {
|
|
|
1389
1628
|
try {
|
|
1390
1629
|
const content = readFileSync4(skillMdPath, "utf8");
|
|
1391
1630
|
if (isGenerated(content)) {
|
|
1392
|
-
|
|
1631
|
+
rmSync3(join4(dir, entry.name), { recursive: true, force: true });
|
|
1393
1632
|
removed = true;
|
|
1394
1633
|
}
|
|
1395
1634
|
} catch {
|
|
@@ -1408,7 +1647,7 @@ function removeSkillMirror(cwd) {
|
|
|
1408
1647
|
if (!existsSync2(skillMdPath)) continue;
|
|
1409
1648
|
try {
|
|
1410
1649
|
if (isGenerated(readFileSync4(skillMdPath, "utf8"))) {
|
|
1411
|
-
|
|
1650
|
+
rmSync3(join4(dir, entry.name), { recursive: true, force: true });
|
|
1412
1651
|
}
|
|
1413
1652
|
} catch {
|
|
1414
1653
|
}
|
|
@@ -1428,7 +1667,7 @@ function removeSkillMirror(cwd) {
|
|
|
1428
1667
|
const ignorePath = join4(dir, IGNORE_FILE);
|
|
1429
1668
|
if (existsSync2(ignorePath)) {
|
|
1430
1669
|
try {
|
|
1431
|
-
|
|
1670
|
+
rmSync3(ignorePath);
|
|
1432
1671
|
} catch {
|
|
1433
1672
|
}
|
|
1434
1673
|
}
|
|
@@ -1450,10 +1689,449 @@ function buildSkillsCatalogue(skills) {
|
|
|
1450
1689
|
].join("\n");
|
|
1451
1690
|
}
|
|
1452
1691
|
|
|
1692
|
+
// src/plugin/plugin-tool-registry.ts
|
|
1693
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1694
|
+
import { join as join5 } from "path";
|
|
1695
|
+
import { createRequire as createRequire2 } from "module";
|
|
1696
|
+
import { pathToFileURL } from "url";
|
|
1697
|
+
import { homedir as homedir4 } from "os";
|
|
1698
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
1699
|
+
var SELF_SPECS = /* @__PURE__ */ new Set([
|
|
1700
|
+
"@stablekernel/opencode-cursor",
|
|
1701
|
+
"@stablekernel/opencode-cursor@latest"
|
|
1702
|
+
]);
|
|
1703
|
+
function parsePluginSpec(spec) {
|
|
1704
|
+
const trimmed = spec.trim();
|
|
1705
|
+
if (!trimmed) return void 0;
|
|
1706
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~/") || /\.[cm]?[jt]sx?$/.test(trimmed)) {
|
|
1707
|
+
const expanded = trimmed.startsWith("~/") ? join5(homedir4(), trimmed.slice(2)) : trimmed;
|
|
1708
|
+
return { kind: "path", path: expanded };
|
|
1709
|
+
}
|
|
1710
|
+
const at = trimmed.lastIndexOf("@");
|
|
1711
|
+
if (at > 0) {
|
|
1712
|
+
const name = trimmed.slice(0, at);
|
|
1713
|
+
const version = trimmed.slice(at + 1);
|
|
1714
|
+
if (version.startsWith("git+")) {
|
|
1715
|
+
return { kind: "git", name, raw: trimmed };
|
|
1716
|
+
}
|
|
1717
|
+
if (version.includes("://")) {
|
|
1718
|
+
return { kind: "unsupported", raw: trimmed };
|
|
1719
|
+
}
|
|
1720
|
+
return { kind: "npm", name, version };
|
|
1721
|
+
}
|
|
1722
|
+
return { kind: "npm", name: trimmed };
|
|
1723
|
+
}
|
|
1724
|
+
function resolveCacheEntry(cacheRoot, parsed) {
|
|
1725
|
+
if (parsed.kind === "unsupported") return void 0;
|
|
1726
|
+
if (parsed.kind === "git") {
|
|
1727
|
+
const parts = parsed.raw.split("/");
|
|
1728
|
+
let current = cacheRoot;
|
|
1729
|
+
for (const part of parts) {
|
|
1730
|
+
const candidate = join5(current, part);
|
|
1731
|
+
if (!existsSync3(candidate)) return void 0;
|
|
1732
|
+
current = candidate;
|
|
1733
|
+
}
|
|
1734
|
+
return current;
|
|
1735
|
+
}
|
|
1736
|
+
const { name, version } = parsed;
|
|
1737
|
+
const candidates = [
|
|
1738
|
+
version ? join5(cacheRoot, `${name}@${version}`) : void 0,
|
|
1739
|
+
join5(cacheRoot, `${name}@latest`),
|
|
1740
|
+
join5(cacheRoot, name)
|
|
1741
|
+
].filter((c) => Boolean(c));
|
|
1742
|
+
for (const candidate of candidates) {
|
|
1743
|
+
if (existsSync3(candidate)) return candidate;
|
|
1744
|
+
}
|
|
1745
|
+
return void 0;
|
|
1746
|
+
}
|
|
1747
|
+
function resolvePackageMain(cacheEntry, pkgDir, name) {
|
|
1748
|
+
const tryRequire = (baseDir) => {
|
|
1749
|
+
try {
|
|
1750
|
+
const req = createRequire2(join5(baseDir, "noop.js"));
|
|
1751
|
+
return req.resolve(name);
|
|
1752
|
+
} catch {
|
|
1753
|
+
return void 0;
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
return tryRequire(pkgDir) ?? tryRequire(cacheEntry);
|
|
1757
|
+
}
|
|
1758
|
+
function packageRoot(cacheEntry, name) {
|
|
1759
|
+
const direct = join5(cacheEntry, "node_modules", name);
|
|
1760
|
+
if (existsSync3(direct)) return direct;
|
|
1761
|
+
const nm = join5(cacheEntry, "node_modules");
|
|
1762
|
+
if (!existsSync3(nm)) return void 0;
|
|
1763
|
+
let entries;
|
|
1764
|
+
try {
|
|
1765
|
+
entries = readdirSync3(nm, { withFileTypes: true });
|
|
1766
|
+
} catch {
|
|
1767
|
+
return void 0;
|
|
1768
|
+
}
|
|
1769
|
+
for (const ent of entries) {
|
|
1770
|
+
if (ent.name === ".bin") continue;
|
|
1771
|
+
const full = join5(nm, ent.name);
|
|
1772
|
+
let isDir = ent.isDirectory();
|
|
1773
|
+
if (!isDir && ent.isSymbolicLink()) {
|
|
1774
|
+
try {
|
|
1775
|
+
isDir = statSync3(full).isDirectory();
|
|
1776
|
+
} catch {
|
|
1777
|
+
isDir = false;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (isDir) return full;
|
|
1781
|
+
}
|
|
1782
|
+
return void 0;
|
|
1783
|
+
}
|
|
1784
|
+
function argsToJsonSchema(args) {
|
|
1785
|
+
if (args == null || typeof args !== "object")
|
|
1786
|
+
return { type: "object", properties: {}, required: [] };
|
|
1787
|
+
const entries = Object.entries(args);
|
|
1788
|
+
const allZod = entries.length > 0 && entries.every(([, v]) => isZodType(v));
|
|
1789
|
+
if (allZod) {
|
|
1790
|
+
try {
|
|
1791
|
+
const zodLike = tool2.schema;
|
|
1792
|
+
if (typeof zodLike.toJSONSchema === "function") {
|
|
1793
|
+
const schema = zodLike.toJSONSchema(zodLike.object(args), {
|
|
1794
|
+
io: "input"
|
|
1795
|
+
});
|
|
1796
|
+
return normalizeZodSchema(schema);
|
|
1797
|
+
}
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
const properties = {};
|
|
1802
|
+
for (const [key, value] of entries) {
|
|
1803
|
+
if (typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1804
|
+
properties[key] = value;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
return { type: "object", properties, required: Object.keys(properties) };
|
|
1808
|
+
}
|
|
1809
|
+
function isZodType(value) {
|
|
1810
|
+
return typeof value === "object" && value !== null && "_zod" in value;
|
|
1811
|
+
}
|
|
1812
|
+
function normalizeZodSchema(schema) {
|
|
1813
|
+
const out = { ...schema };
|
|
1814
|
+
delete out["$schema"];
|
|
1815
|
+
return out;
|
|
1816
|
+
}
|
|
1817
|
+
function extractToolMap(hooks) {
|
|
1818
|
+
if (!hooks || typeof hooks !== "object") return void 0;
|
|
1819
|
+
const tool3 = hooks.tool;
|
|
1820
|
+
if (!tool3 || typeof tool3 !== "object") return void 0;
|
|
1821
|
+
const out = {};
|
|
1822
|
+
for (const [id, def] of Object.entries(tool3)) {
|
|
1823
|
+
if (isPluginTool(def)) out[id] = def;
|
|
1824
|
+
}
|
|
1825
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1826
|
+
}
|
|
1827
|
+
function isPluginTool(value) {
|
|
1828
|
+
return typeof value === "object" && value !== null && "args" in value && "description" in value && "execute" in value;
|
|
1829
|
+
}
|
|
1830
|
+
async function loadToolMap(modulePath, input) {
|
|
1831
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
1832
|
+
const candidates = [mod["server"], mod["default"]];
|
|
1833
|
+
for (const value of Object.values(mod)) {
|
|
1834
|
+
if (typeof value === "function" && !candidates.includes(value)) {
|
|
1835
|
+
candidates.push(value);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
for (const candidate of candidates) {
|
|
1839
|
+
if (typeof candidate !== "function") continue;
|
|
1840
|
+
let hooks;
|
|
1841
|
+
try {
|
|
1842
|
+
hooks = await candidate(input);
|
|
1843
|
+
} catch {
|
|
1844
|
+
continue;
|
|
1845
|
+
}
|
|
1846
|
+
const tools = extractToolMap(hooks);
|
|
1847
|
+
if (tools) return tools;
|
|
1848
|
+
}
|
|
1849
|
+
return void 0;
|
|
1850
|
+
}
|
|
1851
|
+
async function mirrorPluginTools(config, input, options) {
|
|
1852
|
+
const failed = {};
|
|
1853
|
+
const tools = [];
|
|
1854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1855
|
+
const specs = (config?.plugin ?? []).map(
|
|
1856
|
+
(entry) => typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : void 0
|
|
1857
|
+
).filter((s2) => typeof s2 === "string" && s2.length > 0);
|
|
1858
|
+
const cacheRoot = options?.cacheRoot ?? opencodePackagesRoot(homedir4());
|
|
1859
|
+
for (const spec of specs) {
|
|
1860
|
+
if (SELF_SPECS.has(spec)) continue;
|
|
1861
|
+
const parsed = parsePluginSpec(spec);
|
|
1862
|
+
if (!parsed) {
|
|
1863
|
+
failed[spec] = "unsupported spec format";
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
if (parsed.kind === "unsupported") {
|
|
1867
|
+
failed[spec] = "unsupported spec format (URL tarball specs are not mirrored)";
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
let modulePath;
|
|
1871
|
+
if (parsed.kind === "path") {
|
|
1872
|
+
modulePath = parsed.path.startsWith("/") ? parsed.path : void 0;
|
|
1873
|
+
if (!modulePath || !existsSync3(modulePath)) {
|
|
1874
|
+
failed[spec] = "plugin file not found";
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
} else {
|
|
1878
|
+
const entry = resolveCacheEntry(cacheRoot, parsed);
|
|
1879
|
+
if (!entry) {
|
|
1880
|
+
failed[spec] = "not found in opencode package cache";
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
const pkg = packageRoot(entry, parsed.name);
|
|
1884
|
+
if (!pkg) {
|
|
1885
|
+
failed[spec] = "package root not found in cache entry";
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
const resolved = resolvePackageMain(entry, pkg, parsed.name);
|
|
1889
|
+
if (!resolved) {
|
|
1890
|
+
failed[spec] = "package entry point not found";
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
modulePath = resolved;
|
|
1894
|
+
}
|
|
1895
|
+
if (!modulePath) {
|
|
1896
|
+
failed[spec] = "plugin module path not resolved";
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
let toolMap;
|
|
1900
|
+
try {
|
|
1901
|
+
toolMap = await loadToolMap(modulePath, input);
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
failed[spec] = error instanceof Error ? error.message : String(error);
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (!toolMap) {
|
|
1907
|
+
failed[spec] = "no tool map exported";
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
for (const [id, def] of Object.entries(toolMap)) {
|
|
1911
|
+
if (seen.has(id)) continue;
|
|
1912
|
+
if (options?.exclude?.some((p) => matchPattern(p, id))) continue;
|
|
1913
|
+
if (options?.include && options.include.length > 0 && !options.include.some((p) => matchPattern(p, id))) {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
seen.add(id);
|
|
1917
|
+
tools.push({
|
|
1918
|
+
id,
|
|
1919
|
+
description: def.description,
|
|
1920
|
+
parameters: argsToJsonSchema(def.args),
|
|
1921
|
+
execute: def.execute,
|
|
1922
|
+
sourcePlugin: spec
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return { tools, failed };
|
|
1927
|
+
}
|
|
1928
|
+
function matchPattern(pattern, value) {
|
|
1929
|
+
if (pattern === "*") return true;
|
|
1930
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
1931
|
+
const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1932
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/plugin/plugin-tools-bridge.ts
|
|
1936
|
+
import { createServer } from "http";
|
|
1937
|
+
import { randomBytes } from "crypto";
|
|
1938
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1939
|
+
import { fileURLToPath } from "url";
|
|
1940
|
+
import { execSync as execSync2 } from "child_process";
|
|
1941
|
+
function buildToolContext(args, askGate) {
|
|
1942
|
+
const controller = new AbortController();
|
|
1943
|
+
return {
|
|
1944
|
+
sessionID: args.sessionID,
|
|
1945
|
+
messageID: "cursor-plugin-tools",
|
|
1946
|
+
agent: args.agent,
|
|
1947
|
+
directory: args.directory,
|
|
1948
|
+
worktree: args.directory,
|
|
1949
|
+
abort: controller.signal,
|
|
1950
|
+
metadata: () => {
|
|
1951
|
+
},
|
|
1952
|
+
ask: async (input) => {
|
|
1953
|
+
if (!askGate) {
|
|
1954
|
+
throw new Error(
|
|
1955
|
+
"permission gate unavailable \u2014 refusing to run plugin tool without approval"
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
await askGate(input);
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function resolvePluginToolsScript() {
|
|
1963
|
+
const candidates = [
|
|
1964
|
+
"./plugin-tools-mcp.js",
|
|
1965
|
+
// importer is a chunk at dist root
|
|
1966
|
+
"../sidecar/plugin-tools-mcp.js",
|
|
1967
|
+
// importer is dist/plugin/index.js
|
|
1968
|
+
"../sidecar/plugin-tools-mcp.mjs"
|
|
1969
|
+
// importer is src/plugin/*.ts (dev/tests)
|
|
1970
|
+
];
|
|
1971
|
+
for (const candidate of candidates) {
|
|
1972
|
+
const path = fileURLToPath(new URL(candidate, import.meta.url));
|
|
1973
|
+
if (existsSync4(path)) return path;
|
|
1974
|
+
}
|
|
1975
|
+
return void 0;
|
|
1976
|
+
}
|
|
1977
|
+
function execBasename(execPath) {
|
|
1978
|
+
const base = execPath.split(/[/\\]/).pop() ?? "";
|
|
1979
|
+
return base.replace(/\.exe$/i, "").toLowerCase();
|
|
1980
|
+
}
|
|
1981
|
+
function resolvePluginToolsNodeCommand(execPath = process.execPath, lookupNode) {
|
|
1982
|
+
const name = execBasename(execPath);
|
|
1983
|
+
if (name === "node" || name === "bun") return execPath;
|
|
1984
|
+
if (lookupNode) return lookupNode() || void 0;
|
|
1985
|
+
try {
|
|
1986
|
+
const out = execSync2(
|
|
1987
|
+
process.platform === "win32" ? "where node" : "command -v node",
|
|
1988
|
+
{
|
|
1989
|
+
encoding: "utf8",
|
|
1990
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1991
|
+
}
|
|
1992
|
+
).trim();
|
|
1993
|
+
return out.split("\n")[0] || void 0;
|
|
1994
|
+
} catch {
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async function startPluginToolsBridge(options) {
|
|
1999
|
+
const scriptPath = resolvePluginToolsScript();
|
|
2000
|
+
if (!scriptPath) {
|
|
2001
|
+
pluginLog("warn", "plugin-tools MCP script not found; bridge disabled");
|
|
2002
|
+
return { close: async () => {
|
|
2003
|
+
} };
|
|
2004
|
+
}
|
|
2005
|
+
const nodePath = resolvePluginToolsNodeCommand();
|
|
2006
|
+
if (!nodePath) {
|
|
2007
|
+
pluginLog("warn", "plugin-tools MCP needs node on PATH; bridge disabled");
|
|
2008
|
+
return { close: async () => {
|
|
2009
|
+
} };
|
|
2010
|
+
}
|
|
2011
|
+
const token = randomBytes(24).toString("hex");
|
|
2012
|
+
const toolById = new Map(options.tools.map((t) => [t.id, t]));
|
|
2013
|
+
const server = createServer((req, res) => {
|
|
2014
|
+
const send = (status, body) => {
|
|
2015
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
2016
|
+
res.end(JSON.stringify(body));
|
|
2017
|
+
};
|
|
2018
|
+
const auth = req.headers["authorization"];
|
|
2019
|
+
if (auth !== `Bearer ${token}`) {
|
|
2020
|
+
send(401, { error: "unauthorized" });
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (req.method === "GET" && req.url === "/tools") {
|
|
2024
|
+
send(200, {
|
|
2025
|
+
tools: options.tools.map((t) => ({
|
|
2026
|
+
id: t.id,
|
|
2027
|
+
description: t.description,
|
|
2028
|
+
parameters: t.parameters
|
|
2029
|
+
}))
|
|
2030
|
+
});
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (req.method === "POST" && req.url === "/call") {
|
|
2034
|
+
const MAX_BODY = 10 * 1024 * 1024;
|
|
2035
|
+
let raw = "";
|
|
2036
|
+
let size = 0;
|
|
2037
|
+
req.on("data", (chunk) => {
|
|
2038
|
+
size += chunk.length;
|
|
2039
|
+
if (size > MAX_BODY) {
|
|
2040
|
+
req.destroy();
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
raw += chunk;
|
|
2044
|
+
});
|
|
2045
|
+
req.on("end", async () => {
|
|
2046
|
+
if (size > MAX_BODY) return;
|
|
2047
|
+
let body;
|
|
2048
|
+
try {
|
|
2049
|
+
body = JSON.parse(raw);
|
|
2050
|
+
} catch {
|
|
2051
|
+
send(400, { ok: false, error: "invalid JSON body" });
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
const tool3 = body.id ? toolById.get(body.id) : void 0;
|
|
2055
|
+
if (!tool3) {
|
|
2056
|
+
send(404, { ok: false, error: `unknown tool: ${body.id}` });
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
const ctx = buildToolContext(
|
|
2061
|
+
{
|
|
2062
|
+
sessionID: options.sessionID ?? "cursor-plugin-tools",
|
|
2063
|
+
agent: options.agent ?? "cursor",
|
|
2064
|
+
directory: options.directory
|
|
2065
|
+
},
|
|
2066
|
+
options.askGate
|
|
2067
|
+
);
|
|
2068
|
+
const result = await tool3.execute(body.args ?? {}, ctx);
|
|
2069
|
+
if (typeof result === "string") {
|
|
2070
|
+
send(200, { ok: true, output: result });
|
|
2071
|
+
} else {
|
|
2072
|
+
send(200, {
|
|
2073
|
+
ok: true,
|
|
2074
|
+
title: result.title,
|
|
2075
|
+
output: result.output,
|
|
2076
|
+
metadata: result.metadata
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2081
|
+
send(200, { ok: false, error: message });
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
send(404, { error: "not found" });
|
|
2087
|
+
});
|
|
2088
|
+
await new Promise((resolve, reject) => {
|
|
2089
|
+
server.once("error", reject);
|
|
2090
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
2091
|
+
});
|
|
2092
|
+
const address = server.address();
|
|
2093
|
+
const port = typeof address === "object" && address ? address.port : void 0;
|
|
2094
|
+
if (!port) {
|
|
2095
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
2096
|
+
pluginLog(
|
|
2097
|
+
"warn",
|
|
2098
|
+
"plugin-tools control server failed to bind; bridge disabled"
|
|
2099
|
+
);
|
|
2100
|
+
return { close: async () => {
|
|
2101
|
+
} };
|
|
2102
|
+
}
|
|
2103
|
+
return {
|
|
2104
|
+
mcpServer: {
|
|
2105
|
+
type: "stdio",
|
|
2106
|
+
command: nodePath,
|
|
2107
|
+
args: [scriptPath],
|
|
2108
|
+
env: {
|
|
2109
|
+
OPENCODE_PLUGIN_TOOLS_PORT: String(port),
|
|
2110
|
+
OPENCODE_PLUGIN_TOOLS_TOKEN: token
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
close: () => new Promise((resolve) => {
|
|
2114
|
+
server.close(() => resolve());
|
|
2115
|
+
server.closeAllConnections?.();
|
|
2116
|
+
})
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1453
2120
|
// src/plugin/index.ts
|
|
1454
2121
|
function apiKeyFromAuth(auth) {
|
|
1455
2122
|
return auth?.type === "api" ? auth.key : void 0;
|
|
1456
2123
|
}
|
|
2124
|
+
async function fetchLiveSkills(client, query) {
|
|
2125
|
+
const app = client.app;
|
|
2126
|
+
if (typeof app?.skills === "function") {
|
|
2127
|
+
return app.skills(query);
|
|
2128
|
+
}
|
|
2129
|
+
const inner = client._client;
|
|
2130
|
+
return inner?.get?.({
|
|
2131
|
+
url: "/skill",
|
|
2132
|
+
...query?.query ? { query: query.query } : {}
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
1457
2135
|
var CursorPlugin = async (input) => {
|
|
1458
2136
|
const _latestVersionPromise = (async () => {
|
|
1459
2137
|
try {
|
|
@@ -1499,6 +2177,16 @@ var CursorPlugin = async (input) => {
|
|
|
1499
2177
|
setLogBridge({ client, directory });
|
|
1500
2178
|
}
|
|
1501
2179
|
let resolvedCwd = directory ?? process.cwd();
|
|
2180
|
+
let pluginSkillSources;
|
|
2181
|
+
try {
|
|
2182
|
+
pluginSkillSources = resolvePluginSkillSources();
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
pluginLog("warn", "plugin skill source discovery failed", {
|
|
2185
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2186
|
+
impact: "plugin-bundled skills unavailable to the Cursor agent"
|
|
2187
|
+
});
|
|
2188
|
+
pluginSkillSources = void 0;
|
|
2189
|
+
}
|
|
1502
2190
|
let forwardMcp = true;
|
|
1503
2191
|
let userMcp = {};
|
|
1504
2192
|
let autoCompaction = false;
|
|
@@ -1507,6 +2195,134 @@ var CursorPlugin = async (input) => {
|
|
|
1507
2195
|
let lastSkillHash = "";
|
|
1508
2196
|
let currentSkillsCatalogue = "";
|
|
1509
2197
|
const warnedOAuth = /* @__PURE__ */ new Set();
|
|
2198
|
+
let forwardPluginTools = true;
|
|
2199
|
+
let pluginToolOptions;
|
|
2200
|
+
let mirroredTools = [];
|
|
2201
|
+
let pluginToolsBridge;
|
|
2202
|
+
let lastPermissionKey = "";
|
|
2203
|
+
let pluginToolsMcpServer;
|
|
2204
|
+
let pluginToolsWarned = false;
|
|
2205
|
+
async function syncPluginTools(config) {
|
|
2206
|
+
if (!forwardPluginTools) return void 0;
|
|
2207
|
+
try {
|
|
2208
|
+
const result = await mirrorPluginTools(config, input, pluginToolOptions);
|
|
2209
|
+
if (Object.keys(result.failed).length > 0 && !pluginToolsWarned) {
|
|
2210
|
+
pluginToolsWarned = true;
|
|
2211
|
+
pluginLog("warn", "plugin tool mirror skipped some plugins", result.failed);
|
|
2212
|
+
}
|
|
2213
|
+
if (result.tools.length === 0) {
|
|
2214
|
+
if (!Array.isArray(config?.plugin) && pluginToolsMcpServer) {
|
|
2215
|
+
return pluginToolsMcpServer;
|
|
2216
|
+
}
|
|
2217
|
+
await pluginToolsBridge?.close();
|
|
2218
|
+
pluginToolsBridge = void 0;
|
|
2219
|
+
mirroredTools = [];
|
|
2220
|
+
return void 0;
|
|
2221
|
+
}
|
|
2222
|
+
const ids = result.tools.map((t) => t.id).sort().join("|");
|
|
2223
|
+
const permKey = JSON.stringify(config?.permission ?? null);
|
|
2224
|
+
const currentIds = mirroredTools.map((t) => t.id).sort().join("|");
|
|
2225
|
+
if (ids !== currentIds || permKey !== lastPermissionKey || !pluginToolsBridge) {
|
|
2226
|
+
await pluginToolsBridge?.close();
|
|
2227
|
+
pluginToolsBridge = await startPluginToolsBridge({
|
|
2228
|
+
tools: result.tools,
|
|
2229
|
+
directory: input?.directory ?? process.cwd(),
|
|
2230
|
+
askGate: makeAskGate(config?.permission)
|
|
2231
|
+
});
|
|
2232
|
+
mirroredTools = result.tools;
|
|
2233
|
+
lastPermissionKey = permKey;
|
|
2234
|
+
}
|
|
2235
|
+
pluginToolsMcpServer = pluginToolsBridge?.mcpServer;
|
|
2236
|
+
return pluginToolsMcpServer;
|
|
2237
|
+
} catch (error) {
|
|
2238
|
+
pluginLog("warn", "plugin tool mirror failed", {
|
|
2239
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2240
|
+
});
|
|
2241
|
+
return void 0;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function makeAskGate(permissionConfig) {
|
|
2245
|
+
return async (req) => {
|
|
2246
|
+
const patterns = Array.isArray(req.patterns) && req.patterns.length > 0 ? req.patterns : ["*"];
|
|
2247
|
+
let needsAsk = false;
|
|
2248
|
+
for (const pattern of patterns) {
|
|
2249
|
+
const action = evaluatePermissionAction(
|
|
2250
|
+
permissionConfig,
|
|
2251
|
+
req.permission,
|
|
2252
|
+
pattern
|
|
2253
|
+
);
|
|
2254
|
+
if (action === "deny") {
|
|
2255
|
+
throw new Error(
|
|
2256
|
+
`permission denied for "${req.permission}" (pattern "${pattern}")`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
if (action !== "allow") needsAsk = true;
|
|
2260
|
+
}
|
|
2261
|
+
if (!needsAsk) return;
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
`permission for "${req.permission}" is set to "ask", which can't be prompted from the Cursor agent \u2014 set it to "allow" to use this tool`
|
|
2264
|
+
);
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
function evaluatePermissionAction(permissionConfig, permission, pattern) {
|
|
2268
|
+
const wildcardMatch2 = (pattern2, value) => {
|
|
2269
|
+
if (pattern2 === "*") return true;
|
|
2270
|
+
if (!pattern2.includes("*")) return pattern2 === value;
|
|
2271
|
+
const regex = pattern2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2272
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
2273
|
+
};
|
|
2274
|
+
const normalize = (value) => value === "allow" || value === "deny" || value === "ask" ? value : void 0;
|
|
2275
|
+
const home = process.env["HOME"] || homedir5();
|
|
2276
|
+
const expandPattern = (pattern2) => {
|
|
2277
|
+
if (pattern2 === "~") return home;
|
|
2278
|
+
if (pattern2.startsWith("~/")) return home + pattern2.slice(1);
|
|
2279
|
+
if (pattern2.startsWith("$HOME/")) return home + pattern2.slice(5);
|
|
2280
|
+
if (pattern2.startsWith("$HOME")) return home + pattern2.slice(5);
|
|
2281
|
+
return pattern2;
|
|
2282
|
+
};
|
|
2283
|
+
const rules = [];
|
|
2284
|
+
const pushRule = (perm, pattern2, action) => {
|
|
2285
|
+
const normalized = normalize(action);
|
|
2286
|
+
if (typeof perm !== "string" || normalized === void 0) return;
|
|
2287
|
+
rules.push({
|
|
2288
|
+
permission: perm,
|
|
2289
|
+
pattern: typeof pattern2 === "string" ? expandPattern(pattern2) : "*",
|
|
2290
|
+
action: normalized
|
|
2291
|
+
});
|
|
2292
|
+
};
|
|
2293
|
+
if (Array.isArray(permissionConfig)) {
|
|
2294
|
+
for (const rule of permissionConfig) {
|
|
2295
|
+
if (rule && typeof rule === "object") {
|
|
2296
|
+
const r = rule;
|
|
2297
|
+
pushRule(r.permission, r.pattern, r.action);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
} else if (permissionConfig && typeof permissionConfig === "object") {
|
|
2301
|
+
for (const [perm, value] of Object.entries(
|
|
2302
|
+
permissionConfig
|
|
2303
|
+
)) {
|
|
2304
|
+
const direct = normalize(value);
|
|
2305
|
+
if (direct) {
|
|
2306
|
+
pushRule(perm, "*", value);
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2310
|
+
for (const [pattern2, action] of Object.entries(
|
|
2311
|
+
value
|
|
2312
|
+
)) {
|
|
2313
|
+
pushRule(perm, pattern2, action);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
2319
|
+
const rule = rules[i];
|
|
2320
|
+
if (wildcardMatch2(rule.permission, permission) && wildcardMatch2(rule.pattern, pattern)) {
|
|
2321
|
+
return rule.action;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return "ask";
|
|
2325
|
+
}
|
|
1510
2326
|
return {
|
|
1511
2327
|
auth: {
|
|
1512
2328
|
provider: PROVIDER_ID,
|
|
@@ -1537,7 +2353,13 @@ var CursorPlugin = async (input) => {
|
|
|
1537
2353
|
autoCompaction = existingOptions["autoCompaction"] === true;
|
|
1538
2354
|
forwardMcp = existingOptions["forwardMcp"] !== false;
|
|
1539
2355
|
userMcp = existingOptions["mcpServers"] ?? {};
|
|
1540
|
-
const
|
|
2356
|
+
const baseMcpServers = forwardMcp ? { ...userMcp, ...translateMcpServers(config.mcp) } : userMcp;
|
|
2357
|
+
forwardPluginTools = existingOptions["forwardPluginTools"] !== false;
|
|
2358
|
+
pluginToolOptions = existingOptions["pluginTools"];
|
|
2359
|
+
const pluginToolsServer = await syncPluginTools(
|
|
2360
|
+
config
|
|
2361
|
+
);
|
|
2362
|
+
const mcpServers = pluginToolsServer ? { ...baseMcpServers, "opencode-plugin-tools": pluginToolsServer } : baseMcpServers;
|
|
1541
2363
|
const modelParamDefaults = {};
|
|
1542
2364
|
for (const item of models) {
|
|
1543
2365
|
const params = defaultModelParams(item);
|
|
@@ -1553,7 +2375,8 @@ var CursorPlugin = async (input) => {
|
|
|
1553
2375
|
const resolved = resolveSkills(
|
|
1554
2376
|
resolvedCwd,
|
|
1555
2377
|
config,
|
|
1556
|
-
skillFilterOptions
|
|
2378
|
+
skillFilterOptions,
|
|
2379
|
+
pluginSkillSources
|
|
1557
2380
|
);
|
|
1558
2381
|
writeSkillMirror(
|
|
1559
2382
|
resolvedCwd,
|
|
@@ -1629,13 +2452,19 @@ var CursorPlugin = async (input) => {
|
|
|
1629
2452
|
client.config.get(),
|
|
1630
2453
|
client.mcp.status(query)
|
|
1631
2454
|
]);
|
|
1632
|
-
const
|
|
2455
|
+
const liveConfig = cfgRes?.data;
|
|
2456
|
+
const liveMcp = liveConfig?.mcp;
|
|
1633
2457
|
const status = statusRes?.data;
|
|
1634
2458
|
if (status) {
|
|
1635
|
-
|
|
2459
|
+
const liveToolsServer = await syncPluginTools(liveConfig);
|
|
2460
|
+
const liveServers = {
|
|
1636
2461
|
...userMcp,
|
|
1637
2462
|
...translateMcpServers(liveMcp, status)
|
|
1638
2463
|
};
|
|
2464
|
+
if (liveToolsServer) {
|
|
2465
|
+
liveServers["opencode-plugin-tools"] = liveToolsServer;
|
|
2466
|
+
}
|
|
2467
|
+
output.options["mcpServers"] = liveServers;
|
|
1639
2468
|
const unshareable = findUnshareableOAuthServers(liveMcp, status).filter(
|
|
1640
2469
|
(name) => !warnedOAuth.has(name)
|
|
1641
2470
|
);
|
|
@@ -1654,6 +2483,23 @@ var CursorPlugin = async (input) => {
|
|
|
1654
2483
|
}
|
|
1655
2484
|
} catch {
|
|
1656
2485
|
}
|
|
2486
|
+
} else if (client && forwardPluginTools && mirroredTools.length > 0) {
|
|
2487
|
+
try {
|
|
2488
|
+
const query = directory ? { query: { directory } } : void 0;
|
|
2489
|
+
const cfgRes = await client.config.get(query);
|
|
2490
|
+
const liveConfig = cfgRes?.data;
|
|
2491
|
+
const liveToolsServer = await syncPluginTools(liveConfig);
|
|
2492
|
+
const liveServers = {
|
|
2493
|
+
...userMcp
|
|
2494
|
+
};
|
|
2495
|
+
if (liveToolsServer) {
|
|
2496
|
+
liveServers["opencode-plugin-tools"] = liveToolsServer;
|
|
2497
|
+
}
|
|
2498
|
+
if (Object.keys(liveServers).length > 0) {
|
|
2499
|
+
output.options["mcpServers"] = liveServers;
|
|
2500
|
+
}
|
|
2501
|
+
} catch {
|
|
2502
|
+
}
|
|
1657
2503
|
}
|
|
1658
2504
|
if (forwardSkills) {
|
|
1659
2505
|
if (client) {
|
|
@@ -1661,10 +2507,18 @@ var CursorPlugin = async (input) => {
|
|
|
1661
2507
|
const query = directory ? { query: { directory } } : void 0;
|
|
1662
2508
|
const cfgRes = await client.config.get(query);
|
|
1663
2509
|
const liveConfig = cfgRes?.data;
|
|
2510
|
+
let liveSkills;
|
|
2511
|
+
try {
|
|
2512
|
+
let skillsRes;
|
|
2513
|
+
skillsRes = await fetchLiveSkills(client, query);
|
|
2514
|
+
liveSkills = skillsRes?.data;
|
|
2515
|
+
} catch {
|
|
2516
|
+
}
|
|
1664
2517
|
const resolved = resolveSkills(
|
|
1665
2518
|
resolvedCwd,
|
|
1666
2519
|
liveConfig,
|
|
1667
|
-
skillFilterOptions
|
|
2520
|
+
skillFilterOptions,
|
|
2521
|
+
{ ...pluginSkillSources, liveSkills }
|
|
1668
2522
|
);
|
|
1669
2523
|
const hash = skillSetHash(resolved.skills);
|
|
1670
2524
|
if (hash !== lastSkillHash) {
|
|
@@ -1748,7 +2602,7 @@ var CursorPlugin = async (input) => {
|
|
|
1748
2602
|
const cachePath = PLUGIN_CACHE_PATH;
|
|
1749
2603
|
const removeCommand = process.platform === "win32" ? `rmdir /s /q "${cachePath}"` : `rm -rf ${cachePath}`;
|
|
1750
2604
|
try {
|
|
1751
|
-
|
|
2605
|
+
rmSync4(cachePath, { recursive: true, force: true });
|
|
1752
2606
|
clearVersionCache();
|
|
1753
2607
|
return {
|
|
1754
2608
|
title: "cursor plugin (updated)",
|
|
@@ -1797,6 +2651,8 @@ then restart opencode.`,
|
|
|
1797
2651
|
dispose: async () => {
|
|
1798
2652
|
removeSystemRule(resolvedCwd);
|
|
1799
2653
|
removeSkillMirror(resolvedCwd);
|
|
2654
|
+
await pluginToolsBridge?.close();
|
|
2655
|
+
pluginToolsBridge = void 0;
|
|
1800
2656
|
clearSubagentBridge();
|
|
1801
2657
|
clearLogBridge();
|
|
1802
2658
|
}
|