@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.
@@ -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
- streamAgentTurn
15
- } from "../chunk-RDY3H2LE.js";
15
+ stampTaskPartSessionId,
16
+ streamAgentTurn,
17
+ subagentCallChildId
18
+ } from "../chunk-YIEC27VB.js";
16
19
 
17
20
  // src/plugin/index.ts
18
- import { rmSync as rmSync3 } from "fs";
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.7.1";
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 mkdirSync3,
854
- writeFileSync as writeFileSync3,
873
+ mkdirSync as mkdirSync4,
874
+ writeFileSync as writeFileSync4,
855
875
  readFileSync as readFileSync4,
856
876
  existsSync as existsSync2,
857
- rmSync as rmSync2,
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 { join as join3, relative, dirname, resolve as resolvePath, isAbsolute } from "path";
874
- 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";
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 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) {
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
- 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) {
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
- mkdirSync3(dirname2(destPath), { recursive: true });
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
- mkdirSync3(dirname2(path), { recursive: true });
1252
- writeFileSync3(path, content, "utf8");
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
- writeFileSync3(path, `${prefix}${missing.join("\n")}
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
- rmSync2(join4(dir, entry.name), { recursive: true, force: true });
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
- rmSync2(join4(dir, entry.name), { recursive: true, force: true });
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
- rmSync2(ignorePath);
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 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;
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 liveMcp = cfgRes?.data?.mcp;
2455
+ const liveConfig = cfgRes?.data;
2456
+ const liveMcp = liveConfig?.mcp;
1614
2457
  const status = statusRes?.data;
1615
2458
  if (status) {
1616
- output.options["mcpServers"] = {
2459
+ const liveToolsServer = await syncPluginTools(liveConfig);
2460
+ const liveServers = {
1617
2461
  ...userMcp,
1618
2462
  ...translateMcpServers(liveMcp, status)
1619
2463
  };
1620
- const unshareable = findUnshareableOAuthServers(
1621
- liveMcp,
1622
- status
1623
- ).filter((name) => !warnedOAuth.has(name));
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: { local: void 0, latest: void 0, status: "disabled" }
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
- rmSync3(cachePath, { recursive: true, force: true });
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
  }