@stablekernel/opencode-cursor 0.7.1 → 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 +60 -0
- package/README.md +110 -11
- package/dist/{chunk-RDY3H2LE.js → chunk-YIEC27VB.js} +436 -12
- package/dist/chunk-YIEC27VB.js.map +1 -0
- package/dist/plugin/index.js +934 -34
- package/dist/plugin/index.js.map +1 -1
- package/dist/provider/index.js +264 -32
- package/dist/provider/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/chunk-RDY3H2LE.js.map +0 -1
package/dist/plugin/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
clearLogBridge,
|
|
5
5
|
clearSubagentBridge,
|
|
6
6
|
fingerprintApiKey,
|
|
7
|
+
linkDelegateSession,
|
|
7
8
|
loadCursorSdk,
|
|
8
9
|
pluginLog,
|
|
9
10
|
removeSystemRule,
|
|
@@ -11,11 +12,14 @@ import {
|
|
|
11
12
|
resolveCursorApiKey,
|
|
12
13
|
setLogBridge,
|
|
13
14
|
setSubagentBridge,
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
stampTaskPartSessionId,
|
|
16
|
+
streamAgentTurn,
|
|
17
|
+
subagentCallChildId
|
|
18
|
+
} from "../chunk-YIEC27VB.js";
|
|
16
19
|
|
|
17
20
|
// src/plugin/index.ts
|
|
18
|
-
import { rmSync as
|
|
21
|
+
import { rmSync as rmSync4 } from "fs";
|
|
22
|
+
import { homedir as homedir5 } from "os";
|
|
19
23
|
import semver2 from "semver";
|
|
20
24
|
|
|
21
25
|
// src/model-limits.ts
|
|
@@ -585,6 +589,7 @@ async function runDelegate(params) {
|
|
|
585
589
|
break;
|
|
586
590
|
case "reasoning-complete":
|
|
587
591
|
case "compaction":
|
|
592
|
+
case "subagent-event":
|
|
588
593
|
break;
|
|
589
594
|
case "finish":
|
|
590
595
|
if (event.text && text.length === 0) text.push(event.text);
|
|
@@ -726,6 +731,21 @@ function buildCursorTools(deps) {
|
|
|
726
731
|
const toolNote = result.toolActivity.length > 0 ? `
|
|
727
732
|
|
|
728
733
|
(${result.toolActivity.length} tool call(s)${result.toolActivity.some((t) => t.isError) ? ", some failed" : ""})` : "";
|
|
734
|
+
if (context.sessionID) {
|
|
735
|
+
const transcript = [
|
|
736
|
+
result.text || "(no text output)",
|
|
737
|
+
...result.reasoning ? [`
|
|
738
|
+
> ${result.reasoning}`] : [],
|
|
739
|
+
...result.toolActivity.length > 0 ? [`
|
|
740
|
+
(${result.toolActivity.length} tool call(s))`] : []
|
|
741
|
+
].join("\n");
|
|
742
|
+
await linkDelegateSession({
|
|
743
|
+
parentSessionID: context.sessionID,
|
|
744
|
+
title: `Cursor delegate (${args.model})`,
|
|
745
|
+
prompt: args.prompt,
|
|
746
|
+
transcript
|
|
747
|
+
});
|
|
748
|
+
}
|
|
729
749
|
return {
|
|
730
750
|
title: `Cursor delegate (${args.model})`,
|
|
731
751
|
output: (result.text || "(no text output)") + toolNote,
|
|
@@ -794,7 +814,7 @@ function clearVersionCache() {
|
|
|
794
814
|
}
|
|
795
815
|
}
|
|
796
816
|
function getLocalVersion() {
|
|
797
|
-
if (true) return "0.
|
|
817
|
+
if (true) return "0.9.0-next.0";
|
|
798
818
|
try {
|
|
799
819
|
const require2 = createRequire(import.meta.url);
|
|
800
820
|
const pkg = require2("../package.json");
|
|
@@ -850,11 +870,11 @@ async function getLatestVersion() {
|
|
|
850
870
|
|
|
851
871
|
// src/provider/skill-mirror.ts
|
|
852
872
|
import {
|
|
853
|
-
mkdirSync as
|
|
854
|
-
writeFileSync as
|
|
873
|
+
mkdirSync as mkdirSync4,
|
|
874
|
+
writeFileSync as writeFileSync4,
|
|
855
875
|
readFileSync as readFileSync4,
|
|
856
876
|
existsSync as existsSync2,
|
|
857
|
-
rmSync as
|
|
877
|
+
rmSync as rmSync3,
|
|
858
878
|
readdirSync as readdirSync2,
|
|
859
879
|
statSync as statSync2,
|
|
860
880
|
copyFileSync,
|
|
@@ -868,10 +888,20 @@ import {
|
|
|
868
888
|
readFileSync as readFileSync3,
|
|
869
889
|
statSync,
|
|
870
890
|
existsSync,
|
|
871
|
-
realpathSync
|
|
891
|
+
realpathSync,
|
|
892
|
+
mkdtempSync,
|
|
893
|
+
mkdirSync as mkdirSync3,
|
|
894
|
+
rmSync as rmSync2,
|
|
895
|
+
writeFileSync as writeFileSync3
|
|
872
896
|
} from "fs";
|
|
873
|
-
import {
|
|
874
|
-
|
|
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";
|
|
875
905
|
import { execSync } from "child_process";
|
|
876
906
|
function parseFrontmatter(content) {
|
|
877
907
|
if (!content.startsWith("---")) return {};
|
|
@@ -896,6 +926,112 @@ function parseFrontmatter(content) {
|
|
|
896
926
|
}
|
|
897
927
|
var SKILL_DIR_NAMES = ["skill", "skills"];
|
|
898
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
|
+
}
|
|
899
1035
|
function worktreeRoot(cwd) {
|
|
900
1036
|
try {
|
|
901
1037
|
const root = execSync("git rev-parse --show-toplevel", {
|
|
@@ -1006,7 +1142,39 @@ function expandSkillPath(raw, cwd, home) {
|
|
|
1006
1142
|
if (isAbsolute(trimmed)) return trimmed;
|
|
1007
1143
|
return resolvePath(cwd, trimmed);
|
|
1008
1144
|
}
|
|
1009
|
-
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) {
|
|
1010
1178
|
const home = homedir3();
|
|
1011
1179
|
const xdgConfig = process.env["XDG_CONFIG_HOME"] || join3(home, ".config");
|
|
1012
1180
|
const stop = worktreeRoot(cwd);
|
|
@@ -1041,6 +1209,29 @@ function discoverSkills(cwd, extraPaths) {
|
|
|
1041
1209
|
scanRoots.push(expanded);
|
|
1042
1210
|
}
|
|
1043
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
|
+
}
|
|
1044
1235
|
const byId = /* @__PURE__ */ new Map();
|
|
1045
1236
|
for (const dir of scanRoots) {
|
|
1046
1237
|
const found = scanSkillDir(dir);
|
|
@@ -1131,15 +1322,82 @@ function filterSkills(skills, config, options) {
|
|
|
1131
1322
|
}
|
|
1132
1323
|
return { skills: permitted, withheld };
|
|
1133
1324
|
}
|
|
1134
|
-
|
|
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) {
|
|
1135
1385
|
const skillsConfig = config;
|
|
1136
1386
|
const extraPaths = skillsConfig?.skills?.paths;
|
|
1137
1387
|
let discovered;
|
|
1138
1388
|
try {
|
|
1139
|
-
discovered = discoverSkills(cwd, extraPaths);
|
|
1389
|
+
discovered = discoverSkills(cwd, extraPaths, discoveryOptions);
|
|
1140
1390
|
} catch {
|
|
1141
1391
|
discovered = [];
|
|
1142
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
|
+
}
|
|
1143
1401
|
return filterSkills(discovered, config, options);
|
|
1144
1402
|
}
|
|
1145
1403
|
function skillSetHash(skills) {
|
|
@@ -1234,7 +1492,7 @@ function copyTree(srcDir, destDir, skillId, maxBytes, warn) {
|
|
|
1234
1492
|
);
|
|
1235
1493
|
continue;
|
|
1236
1494
|
}
|
|
1237
|
-
|
|
1495
|
+
mkdirSync4(dirname2(destPath), { recursive: true });
|
|
1238
1496
|
copyFileSync(srcPath, destPath);
|
|
1239
1497
|
bytes += size;
|
|
1240
1498
|
} catch {
|
|
@@ -1248,8 +1506,8 @@ function copyTree(srcDir, destDir, skillId, maxBytes, warn) {
|
|
|
1248
1506
|
function writeIfChanged(path, content) {
|
|
1249
1507
|
const existing = existsSync2(path) ? readFileSync4(path, "utf8") : void 0;
|
|
1250
1508
|
if (existing === content) return false;
|
|
1251
|
-
|
|
1252
|
-
|
|
1509
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1510
|
+
writeFileSync4(path, content, "utf8");
|
|
1253
1511
|
return true;
|
|
1254
1512
|
}
|
|
1255
1513
|
function ensureGitIgnored(dir, skillIds) {
|
|
@@ -1262,7 +1520,7 @@ function ensureGitIgnored(dir, skillIds) {
|
|
|
1262
1520
|
if (missing.length === 0) return;
|
|
1263
1521
|
const prefix = existing && !existing.endsWith("\n") ? `${existing}
|
|
1264
1522
|
` : existing;
|
|
1265
|
-
|
|
1523
|
+
writeFileSync4(path, `${prefix}${missing.join("\n")}
|
|
1266
1524
|
`, "utf8");
|
|
1267
1525
|
}
|
|
1268
1526
|
function writeSkillMirror(cwd, skills, warn) {
|
|
@@ -1370,7 +1628,7 @@ function pruneStale(cwd, currentIds, warn) {
|
|
|
1370
1628
|
try {
|
|
1371
1629
|
const content = readFileSync4(skillMdPath, "utf8");
|
|
1372
1630
|
if (isGenerated(content)) {
|
|
1373
|
-
|
|
1631
|
+
rmSync3(join4(dir, entry.name), { recursive: true, force: true });
|
|
1374
1632
|
removed = true;
|
|
1375
1633
|
}
|
|
1376
1634
|
} catch {
|
|
@@ -1389,7 +1647,7 @@ function removeSkillMirror(cwd) {
|
|
|
1389
1647
|
if (!existsSync2(skillMdPath)) continue;
|
|
1390
1648
|
try {
|
|
1391
1649
|
if (isGenerated(readFileSync4(skillMdPath, "utf8"))) {
|
|
1392
|
-
|
|
1650
|
+
rmSync3(join4(dir, entry.name), { recursive: true, force: true });
|
|
1393
1651
|
}
|
|
1394
1652
|
} catch {
|
|
1395
1653
|
}
|
|
@@ -1409,7 +1667,7 @@ function removeSkillMirror(cwd) {
|
|
|
1409
1667
|
const ignorePath = join4(dir, IGNORE_FILE);
|
|
1410
1668
|
if (existsSync2(ignorePath)) {
|
|
1411
1669
|
try {
|
|
1412
|
-
|
|
1670
|
+
rmSync3(ignorePath);
|
|
1413
1671
|
} catch {
|
|
1414
1672
|
}
|
|
1415
1673
|
}
|
|
@@ -1431,10 +1689,449 @@ function buildSkillsCatalogue(skills) {
|
|
|
1431
1689
|
].join("\n");
|
|
1432
1690
|
}
|
|
1433
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
|
+
|
|
1434
2120
|
// src/plugin/index.ts
|
|
1435
2121
|
function apiKeyFromAuth(auth) {
|
|
1436
2122
|
return auth?.type === "api" ? auth.key : void 0;
|
|
1437
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
|
+
}
|
|
1438
2135
|
var CursorPlugin = async (input) => {
|
|
1439
2136
|
const _latestVersionPromise = (async () => {
|
|
1440
2137
|
try {
|
|
@@ -1480,6 +2177,16 @@ var CursorPlugin = async (input) => {
|
|
|
1480
2177
|
setLogBridge({ client, directory });
|
|
1481
2178
|
}
|
|
1482
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
|
+
}
|
|
1483
2190
|
let forwardMcp = true;
|
|
1484
2191
|
let userMcp = {};
|
|
1485
2192
|
let autoCompaction = false;
|
|
@@ -1488,6 +2195,134 @@ var CursorPlugin = async (input) => {
|
|
|
1488
2195
|
let lastSkillHash = "";
|
|
1489
2196
|
let currentSkillsCatalogue = "";
|
|
1490
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
|
+
}
|
|
1491
2326
|
return {
|
|
1492
2327
|
auth: {
|
|
1493
2328
|
provider: PROVIDER_ID,
|
|
@@ -1518,7 +2353,13 @@ var CursorPlugin = async (input) => {
|
|
|
1518
2353
|
autoCompaction = existingOptions["autoCompaction"] === true;
|
|
1519
2354
|
forwardMcp = existingOptions["forwardMcp"] !== false;
|
|
1520
2355
|
userMcp = existingOptions["mcpServers"] ?? {};
|
|
1521
|
-
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;
|
|
1522
2363
|
const modelParamDefaults = {};
|
|
1523
2364
|
for (const item of models) {
|
|
1524
2365
|
const params = defaultModelParams(item);
|
|
@@ -1534,7 +2375,8 @@ var CursorPlugin = async (input) => {
|
|
|
1534
2375
|
const resolved = resolveSkills(
|
|
1535
2376
|
resolvedCwd,
|
|
1536
2377
|
config,
|
|
1537
|
-
skillFilterOptions
|
|
2378
|
+
skillFilterOptions,
|
|
2379
|
+
pluginSkillSources
|
|
1538
2380
|
);
|
|
1539
2381
|
writeSkillMirror(
|
|
1540
2382
|
resolvedCwd,
|
|
@@ -1610,17 +2452,22 @@ var CursorPlugin = async (input) => {
|
|
|
1610
2452
|
client.config.get(),
|
|
1611
2453
|
client.mcp.status(query)
|
|
1612
2454
|
]);
|
|
1613
|
-
const
|
|
2455
|
+
const liveConfig = cfgRes?.data;
|
|
2456
|
+
const liveMcp = liveConfig?.mcp;
|
|
1614
2457
|
const status = statusRes?.data;
|
|
1615
2458
|
if (status) {
|
|
1616
|
-
|
|
2459
|
+
const liveToolsServer = await syncPluginTools(liveConfig);
|
|
2460
|
+
const liveServers = {
|
|
1617
2461
|
...userMcp,
|
|
1618
2462
|
...translateMcpServers(liveMcp, status)
|
|
1619
2463
|
};
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
2464
|
+
if (liveToolsServer) {
|
|
2465
|
+
liveServers["opencode-plugin-tools"] = liveToolsServer;
|
|
2466
|
+
}
|
|
2467
|
+
output.options["mcpServers"] = liveServers;
|
|
2468
|
+
const unshareable = findUnshareableOAuthServers(liveMcp, status).filter(
|
|
2469
|
+
(name) => !warnedOAuth.has(name)
|
|
2470
|
+
);
|
|
1624
2471
|
if (unshareable.length > 0) {
|
|
1625
2472
|
for (const name of unshareable) warnedOAuth.add(name);
|
|
1626
2473
|
const plural = unshareable.length > 1;
|
|
@@ -1636,6 +2483,23 @@ var CursorPlugin = async (input) => {
|
|
|
1636
2483
|
}
|
|
1637
2484
|
} catch {
|
|
1638
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
|
+
}
|
|
1639
2503
|
}
|
|
1640
2504
|
if (forwardSkills) {
|
|
1641
2505
|
if (client) {
|
|
@@ -1643,10 +2507,18 @@ var CursorPlugin = async (input) => {
|
|
|
1643
2507
|
const query = directory ? { query: { directory } } : void 0;
|
|
1644
2508
|
const cfgRes = await client.config.get(query);
|
|
1645
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
|
+
}
|
|
1646
2517
|
const resolved = resolveSkills(
|
|
1647
2518
|
resolvedCwd,
|
|
1648
2519
|
liveConfig,
|
|
1649
|
-
skillFilterOptions
|
|
2520
|
+
skillFilterOptions,
|
|
2521
|
+
{ ...pluginSkillSources, liveSkills }
|
|
1650
2522
|
);
|
|
1651
2523
|
const hash = skillSetHash(resolved.skills);
|
|
1652
2524
|
if (hash !== lastSkillHash) {
|
|
@@ -1664,6 +2536,30 @@ var CursorPlugin = async (input) => {
|
|
|
1664
2536
|
output.options["skillsCatalogue"] = currentSkillsCatalogue;
|
|
1665
2537
|
}
|
|
1666
2538
|
},
|
|
2539
|
+
// Stamp the child session id on the RUNNING `task` part. The provider
|
|
2540
|
+
// creates the child session when the Cursor subagent starts and
|
|
2541
|
+
// publishes call→child on the bridge registry; when opencode's
|
|
2542
|
+
// processor lands the task part (`message.part.updated`), patch it
|
|
2543
|
+
// (`part.update`, the native `ctx.metadata` equivalent) so the TUI
|
|
2544
|
+
// card carries `state.metadata.sessionId` from the start — matching
|
|
2545
|
+
// the native task tool, which publishes the id at execute time. The
|
|
2546
|
+
// processor emits a running-state part update for every streamed
|
|
2547
|
+
// tool part, so this fires early; the stamp is idempotent.
|
|
2548
|
+
event: async (input2) => {
|
|
2549
|
+
const evt = input2.event;
|
|
2550
|
+
if (evt.type !== "message.part.updated") return;
|
|
2551
|
+
const part = evt.properties.part;
|
|
2552
|
+
if (!part || part.type !== "tool" || part.tool !== "task") return;
|
|
2553
|
+
const childId = subagentCallChildId(part.callID);
|
|
2554
|
+
if (!childId) return;
|
|
2555
|
+
void stampTaskPartSessionId({
|
|
2556
|
+
sessionID: part.sessionID,
|
|
2557
|
+
messageID: part.messageID,
|
|
2558
|
+
partID: part.id,
|
|
2559
|
+
part,
|
|
2560
|
+
childId
|
|
2561
|
+
});
|
|
2562
|
+
},
|
|
1667
2563
|
tool: {
|
|
1668
2564
|
cursor_update_plugin: {
|
|
1669
2565
|
description: "Check if the @stablekernel/opencode-cursor plugin is up to date and update it if not. Call this when the user asks to update, upgrade, or refresh the cursor plugin. Clears the cached install so opencode fetches the latest version on next launch.",
|
|
@@ -1673,7 +2569,11 @@ var CursorPlugin = async (input) => {
|
|
|
1673
2569
|
return {
|
|
1674
2570
|
title: "cursor plugin (checks disabled)",
|
|
1675
2571
|
output: "Update checks are disabled (CI or NO_UPDATE_NOTIFIER is set).",
|
|
1676
|
-
metadata: {
|
|
2572
|
+
metadata: {
|
|
2573
|
+
local: void 0,
|
|
2574
|
+
latest: void 0,
|
|
2575
|
+
status: "disabled"
|
|
2576
|
+
}
|
|
1677
2577
|
};
|
|
1678
2578
|
}
|
|
1679
2579
|
const local = getLocalVersion();
|
|
@@ -1702,7 +2602,7 @@ var CursorPlugin = async (input) => {
|
|
|
1702
2602
|
const cachePath = PLUGIN_CACHE_PATH;
|
|
1703
2603
|
const removeCommand = process.platform === "win32" ? `rmdir /s /q "${cachePath}"` : `rm -rf ${cachePath}`;
|
|
1704
2604
|
try {
|
|
1705
|
-
|
|
2605
|
+
rmSync4(cachePath, { recursive: true, force: true });
|
|
1706
2606
|
clearVersionCache();
|
|
1707
2607
|
return {
|
|
1708
2608
|
title: "cursor plugin (updated)",
|
|
@@ -1731,9 +2631,7 @@ then restart opencode.`,
|
|
|
1731
2631
|
args: {},
|
|
1732
2632
|
execute: async () => {
|
|
1733
2633
|
const result = await discoverModels({ forceRefresh: true });
|
|
1734
|
-
const lines = result.models.map(
|
|
1735
|
-
(m) => `- ${m.id} \u2014 ${m.displayName}`
|
|
1736
|
-
);
|
|
2634
|
+
const lines = result.models.map((m) => `- ${m.id} \u2014 ${m.displayName}`);
|
|
1737
2635
|
const header = result.source === "live" ? `Refreshed ${result.models.length} Cursor models (live):` : `Could not fetch live models (${result.source}). ${result.warning ?? ""}`.trim();
|
|
1738
2636
|
return {
|
|
1739
2637
|
title: `Cursor models (${result.source})`,
|
|
@@ -1753,6 +2651,8 @@ then restart opencode.`,
|
|
|
1753
2651
|
dispose: async () => {
|
|
1754
2652
|
removeSystemRule(resolvedCwd);
|
|
1755
2653
|
removeSkillMirror(resolvedCwd);
|
|
2654
|
+
await pluginToolsBridge?.close();
|
|
2655
|
+
pluginToolsBridge = void 0;
|
|
1756
2656
|
clearSubagentBridge();
|
|
1757
2657
|
clearLogBridge();
|
|
1758
2658
|
}
|