@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.
@@ -18,7 +18,8 @@ import {
18
18
  } from "../chunk-YIEC27VB.js";
19
19
 
20
20
  // src/plugin/index.ts
21
- import { rmSync as rmSync3 } from "fs";
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.8.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 mkdirSync3,
873
- writeFileSync as writeFileSync3,
873
+ mkdirSync as mkdirSync4,
874
+ writeFileSync as writeFileSync4,
874
875
  readFileSync as readFileSync4,
875
876
  existsSync as existsSync2,
876
- rmSync as rmSync2,
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 { join as join3, relative, dirname, resolve as resolvePath, isAbsolute } from "path";
893
- import { homedir as homedir3 } from "os";
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 discoverSkills(cwd, extraPaths) {
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
- function resolveSkills(cwd, config, options) {
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
- mkdirSync3(dirname2(destPath), { recursive: true });
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
- mkdirSync3(dirname2(path), { recursive: true });
1271
- writeFileSync3(path, content, "utf8");
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
- writeFileSync3(path, `${prefix}${missing.join("\n")}
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
- rmSync2(join4(dir, entry.name), { recursive: true, force: true });
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
- rmSync2(join4(dir, entry.name), { recursive: true, force: true });
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
- rmSync2(ignorePath);
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 mcpServers = forwardMcp ? { ...userMcp, ...translateMcpServers(config.mcp) } : userMcp;
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 liveMcp = cfgRes?.data?.mcp;
2455
+ const liveConfig = cfgRes?.data;
2456
+ const liveMcp = liveConfig?.mcp;
1633
2457
  const status = statusRes?.data;
1634
2458
  if (status) {
1635
- output.options["mcpServers"] = {
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
- rmSync3(cachePath, { recursive: true, force: true });
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
  }