@stablekernel/opencode-cursor 0.7.0 → 0.7.1-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.
@@ -5,18 +5,135 @@ import {
5
5
  clearSubagentBridge,
6
6
  fingerprintApiKey,
7
7
  loadCursorSdk,
8
+ pluginLog,
8
9
  removeSystemRule,
9
10
  resolveControls,
10
11
  resolveCursorApiKey,
11
12
  setLogBridge,
12
13
  setSubagentBridge,
13
14
  streamAgentTurn
14
- } from "../chunk-COE6ZXMP.js";
15
+ } from "../chunk-RDY3H2LE.js";
15
16
 
16
17
  // src/plugin/index.ts
17
- import { rmSync as rmSync2 } from "fs";
18
+ import { rmSync as rmSync3 } from "fs";
18
19
  import semver2 from "semver";
19
20
 
21
+ // src/model-limits.ts
22
+ var MODEL_CONTEXT_LIMITS = {
23
+ "auto-smart": 2e5,
24
+ "claude-fable-5": 3e5,
25
+ "claude-haiku-4-5": 2e5,
26
+ "claude-opus-4-5": 2e5,
27
+ "claude-opus-4-6": 2e5,
28
+ "claude-opus-4-7": 3e5,
29
+ "claude-opus-4-8": 3e5,
30
+ "claude-opus-5": 3e5,
31
+ "claude-sonnet-4": 2e5,
32
+ "claude-sonnet-4-5": 2e5,
33
+ "claude-sonnet-4-6": 2e5,
34
+ "claude-sonnet-5": 2e5,
35
+ "composer-2": 2e5,
36
+ "composer-2.5": 2e5,
37
+ "default": 2e5,
38
+ "gemini-2.5-flash": 2e5,
39
+ "gemini-3-flash": 2e5,
40
+ "gemini-3.1-pro": 2e5,
41
+ "gemini-3.5-flash": 2e5,
42
+ "gemini-3.6-flash": 2e5,
43
+ "glm-5.2": 2e5,
44
+ "gpt-5-mini": 272e3,
45
+ "gpt-5.1": 272e3,
46
+ "gpt-5.2": 272e3,
47
+ "gpt-5.3-codex": 272e3,
48
+ "gpt-5.4": 272e3,
49
+ "gpt-5.4-mini": 272e3,
50
+ "gpt-5.4-nano": 272e3,
51
+ "gpt-5.5": 272e3,
52
+ "gpt-5.6-luna": 272e3,
53
+ "gpt-5.6-sol": 272e3,
54
+ "gpt-5.6-terra": 272e3,
55
+ "grok-4.5": 256e3
56
+ };
57
+ var DEFAULT_CONTEXT_LIMIT = 2e5;
58
+ function resolveContextLimit(modelId) {
59
+ let best;
60
+ let bestLen = 0;
61
+ for (const [prefix, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
62
+ if (modelId.startsWith(prefix) && prefix.length > bestLen) {
63
+ best = limit;
64
+ bestLen = prefix.length;
65
+ }
66
+ }
67
+ return best ?? DEFAULT_CONTEXT_LIMIT;
68
+ }
69
+ var MODEL_COST = {
70
+ "auto-smart": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
71
+ "claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
72
+ "claude-haiku-4-5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
73
+ "claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
74
+ "claude-opus-4-6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
75
+ "claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
76
+ "claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
77
+ "claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
78
+ "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
79
+ "claude-sonnet-4-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
80
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
81
+ "claude-sonnet-5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
82
+ "composer-2": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
83
+ "composer-2.5": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
84
+ "default": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
85
+ "gemini-2.5-flash": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 },
86
+ "gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 },
87
+ "gemini-3.1-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
88
+ "gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
89
+ "gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
90
+ "glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
91
+ "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 },
92
+ "gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
93
+ "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
94
+ "gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
95
+ "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
96
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
97
+ "gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0 },
98
+ "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
99
+ "gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
100
+ "gpt-5.6-sol": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
101
+ "gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
102
+ "grok-4.5": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
103
+ };
104
+ var DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
105
+ function resolveCost(modelId) {
106
+ let best;
107
+ let bestLen = 0;
108
+ for (const [prefix, cost] of Object.entries(MODEL_COST)) {
109
+ if (modelId.startsWith(prefix) && prefix.length > bestLen) {
110
+ best = cost;
111
+ bestLen = prefix.length;
112
+ }
113
+ }
114
+ return best ?? DEFAULT_COST;
115
+ }
116
+ var MODEL_OUTPUT_LIMITS = {
117
+ "claude-opus-4-7": 64e3,
118
+ "claude-opus-4-8": 64e3,
119
+ "claude-opus-5": 64e3,
120
+ "claude-fable-5": 64e3,
121
+ "gpt-5.5": 64e3,
122
+ "gpt-5.6-sol": 64e3
123
+ };
124
+ var DEFAULT_OUTPUT_LIMIT = 32e3;
125
+ function resolveOutputLimit(modelId) {
126
+ let best;
127
+ let bestLen = 0;
128
+ for (const [prefix, limit] of Object.entries(MODEL_OUTPUT_LIMITS)) {
129
+ if (modelId.startsWith(prefix) && prefix.length > bestLen) {
130
+ best = limit;
131
+ bestLen = prefix.length;
132
+ }
133
+ }
134
+ return best ?? DEFAULT_OUTPUT_LIMIT;
135
+ }
136
+
20
137
  // src/model-cache.ts
21
138
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
22
139
  import { homedir, tmpdir } from "os";
@@ -200,6 +317,7 @@ function toOpencodeModels(items) {
200
317
  const out = {};
201
318
  for (const item of items) {
202
319
  const params = defaultModelParams(item);
320
+ const cost = resolveCost(item.id);
203
321
  out[item.id] = {
204
322
  id: item.id,
205
323
  name: item.displayName || item.id,
@@ -208,7 +326,17 @@ function toOpencodeModels(items) {
208
326
  temperature: false,
209
327
  tool_call: true,
210
328
  variants: buildModelVariants(item),
211
- options: Object.keys(params).length > 0 ? { params } : {}
329
+ options: Object.keys(params).length > 0 ? { params } : {},
330
+ limit: {
331
+ context: resolveContextLimit(item.id),
332
+ output: resolveOutputLimit(item.id)
333
+ },
334
+ cost: {
335
+ input: cost.input,
336
+ output: cost.output,
337
+ cache_read: cost.cacheRead,
338
+ cache_write: cost.cacheWrite
339
+ }
212
340
  };
213
341
  }
214
342
  return out;
@@ -238,8 +366,11 @@ function buildModelV2Map(items) {
238
366
  output: { text: true, audio: false, image: false, video: false, pdf: false },
239
367
  interleaved: false
240
368
  },
241
- cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
242
- limit: { context: 2e5, output: 32e3 },
369
+ cost: (() => {
370
+ const c = resolveCost(item.id);
371
+ return { input: c.input, output: c.output, cache: { read: c.cacheRead, write: c.cacheWrite } };
372
+ })(),
373
+ limit: { context: resolveContextLimit(item.id), output: resolveOutputLimit(item.id) },
243
374
  status: "active",
244
375
  options: Object.keys(params).length > 0 ? { params } : {},
245
376
  headers: {},
@@ -404,6 +535,10 @@ async function runDelegate(params) {
404
535
  modelSelection,
405
536
  mode,
406
537
  cwd: params.cwd,
538
+ // Default to the "project" settings layer so the delegate picks up
539
+ // `.cursor/skills/` and other project-level config from its cwd. An
540
+ // explicit `settingSources` from the caller overrides this default.
541
+ settingSources: params.settingSources ?? ["project"],
407
542
  ...params.sandbox !== void 0 ? { sandbox: params.sandbox } : {},
408
543
  ...params.agentId ? { resumeAgentId: params.agentId } : {}
409
544
  });
@@ -565,6 +700,11 @@ function buildCursorTools(deps) {
565
700
  prompt: args.prompt,
566
701
  model: args.model,
567
702
  cwd: args.additionalCwds?.length ? [baseCwd, ...args.additionalCwds] : baseCwd,
703
+ // Honour the user's project settings layer so delegated turns pick
704
+ // up `.cursor/skills/` from the delegate's cwd. The delegate defaults
705
+ // to ["project"] on its own, so this is only needed when the user
706
+ // explicitly configured settingSources on the provider.
707
+ settingSources: ["project"],
568
708
  ...args.mode ? { mode: args.mode } : {},
569
709
  ...args.thinking ? { thinking: args.thinking } : {},
570
710
  ...args.sandbox !== void 0 ? { sandbox: args.sandbox } : {},
@@ -645,7 +785,7 @@ function clearVersionCache() {
645
785
  }
646
786
  }
647
787
  function getLocalVersion() {
648
- if (true) return "0.7.0";
788
+ if (true) return "0.7.1-next.0";
649
789
  try {
650
790
  const require2 = createRequire(import.meta.url);
651
791
  const pkg = require2("../package.json");
@@ -699,6 +839,589 @@ async function getLatestVersion() {
699
839
  return latest;
700
840
  }
701
841
 
842
+ // src/provider/skill-mirror.ts
843
+ import {
844
+ mkdirSync as mkdirSync3,
845
+ writeFileSync as writeFileSync3,
846
+ readFileSync as readFileSync4,
847
+ existsSync as existsSync2,
848
+ rmSync as rmSync2,
849
+ readdirSync as readdirSync2,
850
+ statSync as statSync2,
851
+ copyFileSync,
852
+ realpathSync as realpathSync2
853
+ } from "fs";
854
+ import { join as join4, relative as relative2, dirname as dirname2 } from "path";
855
+
856
+ // src/plugin/skill-discovery.ts
857
+ import {
858
+ readdirSync,
859
+ readFileSync as readFileSync3,
860
+ statSync,
861
+ existsSync,
862
+ realpathSync
863
+ } from "fs";
864
+ import { join as join3, relative, dirname, resolve as resolvePath, isAbsolute } from "path";
865
+ import { homedir as homedir3 } from "os";
866
+ import { execSync } from "child_process";
867
+ function parseFrontmatter(content) {
868
+ if (!content.startsWith("---")) return {};
869
+ const end = content.indexOf("\n---", 3);
870
+ if (end === -1) return {};
871
+ const frontmatter = content.slice(3, end);
872
+ const result = {};
873
+ for (const line of frontmatter.split(/\r?\n/)) {
874
+ const trimmed = line.trim();
875
+ if (!trimmed || trimmed.startsWith("#")) continue;
876
+ const colon = trimmed.indexOf(":");
877
+ if (colon === -1) continue;
878
+ const key = trimmed.slice(0, colon).trim();
879
+ let value = trimmed.slice(colon + 1).trim();
880
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
881
+ value = value.slice(1, -1);
882
+ }
883
+ if (key === "name") result.name = value;
884
+ else if (key === "description") result.description = value;
885
+ }
886
+ return result;
887
+ }
888
+ var SKILL_DIR_NAMES = ["skill", "skills"];
889
+ var EXTERNAL_DIR_NAMES = [".claude", ".agents"];
890
+ function worktreeRoot(cwd) {
891
+ try {
892
+ const root = execSync("git rev-parse --show-toplevel", {
893
+ cwd,
894
+ encoding: "utf8",
895
+ stdio: ["pipe", "pipe", "pipe"],
896
+ timeout: 3e3
897
+ }).trim();
898
+ return root || cwd;
899
+ } catch {
900
+ return cwd;
901
+ }
902
+ }
903
+ function* walkUp(start, stop) {
904
+ let current = start;
905
+ while (current) {
906
+ yield current;
907
+ if (current === stop) break;
908
+ const parent = dirname(current);
909
+ if (parent === current) break;
910
+ current = parent;
911
+ }
912
+ }
913
+ function scanSkillDir(dir) {
914
+ if (!existsSync(dir)) return [];
915
+ let entries;
916
+ try {
917
+ entries = readdirSync(dir, { withFileTypes: true });
918
+ } catch {
919
+ return [];
920
+ }
921
+ const found = [];
922
+ for (const entry of entries) {
923
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
924
+ const skillDir = join3(dir, entry.name);
925
+ if (!existsSync(join3(skillDir, "SKILL.md"))) continue;
926
+ found.push({ id: entry.name, sourceDir: skillDir });
927
+ }
928
+ return found;
929
+ }
930
+ function entryKind(entry, fullPath) {
931
+ if (entry.isDirectory()) return "dir";
932
+ if (entry.isFile()) return "file";
933
+ if (!entry.isSymbolicLink()) return "other";
934
+ try {
935
+ const target = statSync(fullPath);
936
+ if (target.isDirectory()) return "dir";
937
+ if (target.isFile()) return "file";
938
+ } catch {
939
+ }
940
+ return "other";
941
+ }
942
+ function collectFiles(sourceDir) {
943
+ const files = [];
944
+ const visited = /* @__PURE__ */ new Set();
945
+ function walk(dir, base) {
946
+ let realDir;
947
+ try {
948
+ realDir = realpathSync(dir);
949
+ } catch {
950
+ return;
951
+ }
952
+ if (visited.has(realDir)) return;
953
+ visited.add(realDir);
954
+ let entries;
955
+ try {
956
+ entries = readdirSync(dir, { withFileTypes: true });
957
+ } catch {
958
+ return;
959
+ }
960
+ for (const entry of entries) {
961
+ const fullPath = join3(dir, entry.name);
962
+ const relPath = relative(base, fullPath);
963
+ if (entry.name === "SKILL.md") continue;
964
+ const kind = entryKind(entry, fullPath);
965
+ if (kind === "dir") {
966
+ walk(fullPath, base);
967
+ } else if (kind === "file") {
968
+ files.push(relPath);
969
+ }
970
+ }
971
+ }
972
+ walk(sourceDir, sourceDir);
973
+ return files;
974
+ }
975
+ function loadSkill(id, sourceDir) {
976
+ const skillMdPath = join3(sourceDir, "SKILL.md");
977
+ let content;
978
+ try {
979
+ content = readFileSync3(skillMdPath, "utf8");
980
+ } catch {
981
+ return void 0;
982
+ }
983
+ const fm = parseFrontmatter(content);
984
+ if (!fm.name || !fm.description) return void 0;
985
+ return {
986
+ id,
987
+ name: fm.name,
988
+ description: fm.description,
989
+ sourceDir,
990
+ files: collectFiles(sourceDir)
991
+ };
992
+ }
993
+ function expandSkillPath(raw, cwd, home) {
994
+ const trimmed = raw.trim();
995
+ if (!trimmed) return void 0;
996
+ if (trimmed.startsWith("~/")) return join3(home, trimmed.slice(2));
997
+ if (isAbsolute(trimmed)) return trimmed;
998
+ return resolvePath(cwd, trimmed);
999
+ }
1000
+ function discoverSkills(cwd, extraPaths) {
1001
+ const home = homedir3();
1002
+ const xdgConfig = process.env["XDG_CONFIG_HOME"] || join3(home, ".config");
1003
+ const stop = worktreeRoot(cwd);
1004
+ const scanRoots = [];
1005
+ for (const ancestor of walkUp(cwd, stop)) {
1006
+ for (const sub of SKILL_DIR_NAMES) {
1007
+ scanRoots.push(join3(ancestor, ".opencode", sub));
1008
+ }
1009
+ }
1010
+ for (const ancestor of walkUp(cwd, stop)) {
1011
+ for (const ext of EXTERNAL_DIR_NAMES) {
1012
+ scanRoots.push(join3(ancestor, ext, "skills"));
1013
+ }
1014
+ }
1015
+ for (const sub of SKILL_DIR_NAMES) {
1016
+ scanRoots.push(join3(xdgConfig, "opencode", sub));
1017
+ }
1018
+ for (const ext of EXTERNAL_DIR_NAMES) {
1019
+ scanRoots.push(join3(home, ext, "skills"));
1020
+ }
1021
+ const tildeOpencode = join3(home, ".opencode");
1022
+ if (existsSync(tildeOpencode)) {
1023
+ for (const sub of SKILL_DIR_NAMES) {
1024
+ scanRoots.push(join3(tildeOpencode, sub));
1025
+ }
1026
+ }
1027
+ if (extraPaths) {
1028
+ for (const raw of extraPaths) {
1029
+ const expanded = expandSkillPath(raw, cwd, home);
1030
+ if (!expanded) continue;
1031
+ if (!existsSync(expanded)) continue;
1032
+ scanRoots.push(expanded);
1033
+ }
1034
+ }
1035
+ const byId = /* @__PURE__ */ new Map();
1036
+ for (const dir of scanRoots) {
1037
+ const found = scanSkillDir(dir);
1038
+ for (const { id, sourceDir } of found) {
1039
+ if (byId.has(id)) continue;
1040
+ const skill = loadSkill(id, sourceDir);
1041
+ if (skill) byId.set(id, skill);
1042
+ }
1043
+ }
1044
+ return Array.from(byId.values());
1045
+ }
1046
+ function wildcardMatch(pattern, value) {
1047
+ if (pattern === "*") return true;
1048
+ if (!pattern.includes("*")) return pattern === value;
1049
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1050
+ return new RegExp(`^${regex}$`).test(value);
1051
+ }
1052
+ function resolveMapPermission(skillPerm, skillId) {
1053
+ if (typeof skillPerm === "string") {
1054
+ return skillPerm;
1055
+ }
1056
+ if (typeof skillPerm !== "object" || skillPerm === null) return void 0;
1057
+ const map = skillPerm;
1058
+ let action;
1059
+ for (const [pattern, value] of Object.entries(map)) {
1060
+ if (wildcardMatch(pattern, skillId)) {
1061
+ action = value;
1062
+ }
1063
+ }
1064
+ return action;
1065
+ }
1066
+ function resolveRuleArrayPermission(rules, skillId) {
1067
+ let action;
1068
+ for (const rule of rules) {
1069
+ if (rule.permission !== "skill") continue;
1070
+ if (wildcardMatch(rule.pattern, skillId)) {
1071
+ action = rule.action;
1072
+ }
1073
+ }
1074
+ return action;
1075
+ }
1076
+ function filterSkills(skills, config, options) {
1077
+ const include = options?.include ?? [];
1078
+ const exclude = options?.exclude ?? [];
1079
+ const matchesAny = (patterns, id) => patterns.some((pattern) => wildcardMatch(pattern, id));
1080
+ const permission = config?.permission;
1081
+ const mapSkillPerm = permission?.["skill"];
1082
+ const ruleArray = Array.isArray(permission?.["permission"]) ? permission["permission"] : void 0;
1083
+ const v2Ruleset = Array.isArray(config?.permission) ? config.permission : void 0;
1084
+ const permitted = [];
1085
+ const withheld = [];
1086
+ for (const skill of skills) {
1087
+ if (matchesAny(exclude, skill.id)) {
1088
+ withheld.push({ id: skill.id, reason: "excluded by plugin options" });
1089
+ continue;
1090
+ }
1091
+ if (include.length > 0 && matchesAny(include, skill.id)) {
1092
+ permitted.push(skill);
1093
+ continue;
1094
+ }
1095
+ if (include.length > 0 && !matchesAny(include, skill.id)) {
1096
+ withheld.push({
1097
+ id: skill.id,
1098
+ reason: "not in plugin include list"
1099
+ });
1100
+ continue;
1101
+ }
1102
+ let action;
1103
+ if (v2Ruleset) {
1104
+ action = resolveRuleArrayPermission(v2Ruleset, skill.id);
1105
+ }
1106
+ if (action === void 0 && ruleArray) {
1107
+ action = resolveRuleArrayPermission(ruleArray, skill.id);
1108
+ }
1109
+ if (action === void 0 && mapSkillPerm !== void 0) {
1110
+ action = resolveMapPermission(mapSkillPerm, skill.id);
1111
+ }
1112
+ if (action === void 0 || action === "allow") {
1113
+ permitted.push(skill);
1114
+ } else if (action === "deny") {
1115
+ withheld.push({ id: skill.id, reason: "denied by permission config" });
1116
+ } else if (action === "ask") {
1117
+ withheld.push({
1118
+ id: skill.id,
1119
+ reason: "ask-permissioned skills are withheld (the ask prompt can't cross the Cursor boundary)"
1120
+ });
1121
+ }
1122
+ }
1123
+ return { skills: permitted, withheld };
1124
+ }
1125
+ function resolveSkills(cwd, config, options) {
1126
+ const skillsConfig = config;
1127
+ const extraPaths = skillsConfig?.skills?.paths;
1128
+ let discovered;
1129
+ try {
1130
+ discovered = discoverSkills(cwd, extraPaths);
1131
+ } catch {
1132
+ discovered = [];
1133
+ }
1134
+ return filterSkills(discovered, config, options);
1135
+ }
1136
+ function skillSetHash(skills) {
1137
+ const parts = skills.map((s2) => {
1138
+ const files = ["SKILL.md", ...s2.files].map((file) => {
1139
+ try {
1140
+ const stat = statSync(join3(s2.sourceDir, file));
1141
+ return `${file}:${stat.mtimeMs}:${stat.size}`;
1142
+ } catch {
1143
+ return `${file}:missing`;
1144
+ }
1145
+ });
1146
+ files.sort();
1147
+ return `${s2.id}:${s2.sourceDir}:${files.join(",")}`;
1148
+ });
1149
+ parts.sort();
1150
+ return parts.join("|");
1151
+ }
1152
+
1153
+ // src/provider/skill-mirror.ts
1154
+ var SKILLS_DIR = join4(".cursor", "skills");
1155
+ var IGNORE_FILE = ".gitignore";
1156
+ var SENTINEL = "generated: opencode-cursor";
1157
+ var MAX_FILE_BYTES = 1048576;
1158
+ var MAX_TOTAL_BYTES = 10485760;
1159
+ function isGenerated(content) {
1160
+ if (!content.startsWith("---")) return false;
1161
+ const end = content.indexOf("\n---", 3);
1162
+ const frontmatter = end === -1 ? content : content.slice(0, end);
1163
+ return frontmatter.split(/\r?\n/).includes(SENTINEL);
1164
+ }
1165
+ function stampSentinel(content) {
1166
+ if (!content.startsWith("---")) {
1167
+ return `---
1168
+ ${SENTINEL}
1169
+ ---
1170
+
1171
+ ${content}`;
1172
+ }
1173
+ const end = content.indexOf("\n---", 3);
1174
+ if (end === -1) {
1175
+ return `---
1176
+ ${SENTINEL}
1177
+ ${content.slice(3)}`;
1178
+ }
1179
+ const frontmatter = content.slice(3, end);
1180
+ if (frontmatter.includes(SENTINEL)) return content;
1181
+ return `---${frontmatter}
1182
+ ${SENTINEL}
1183
+ ${content.slice(end)}`;
1184
+ }
1185
+ function copyTree(srcDir, destDir, skillId, maxBytes, warn) {
1186
+ let bytes = 0;
1187
+ const skipped = [];
1188
+ const visited = /* @__PURE__ */ new Set();
1189
+ function walk(src, dest) {
1190
+ let realSrc;
1191
+ try {
1192
+ realSrc = realpathSync2(src);
1193
+ } catch {
1194
+ return;
1195
+ }
1196
+ if (visited.has(realSrc)) return;
1197
+ visited.add(realSrc);
1198
+ let entries;
1199
+ try {
1200
+ entries = readdirSync2(src, { withFileTypes: true });
1201
+ } catch {
1202
+ return;
1203
+ }
1204
+ for (const entry of entries) {
1205
+ const srcPath = join4(src, entry.name);
1206
+ const destPath = join4(dest, entry.name);
1207
+ const kind = entryKind(entry, srcPath);
1208
+ if (kind === "dir") {
1209
+ walk(srcPath, destPath);
1210
+ } else if (kind === "file") {
1211
+ if (entry.name === "SKILL.md") continue;
1212
+ try {
1213
+ const size = statSync2(srcPath).size;
1214
+ if (size > MAX_FILE_BYTES) {
1215
+ skipped.push(relative2(srcDir, srcPath));
1216
+ warn(
1217
+ `Skill "${skillId}": skipped oversized file "${relative2(srcDir, srcPath)}" (${size} bytes > ${MAX_FILE_BYTES} limit). The rest of the skill is still mirrored.`
1218
+ );
1219
+ continue;
1220
+ }
1221
+ if (bytes + size > maxBytes) {
1222
+ skipped.push(relative2(srcDir, srcPath));
1223
+ warn(
1224
+ `Skill "${skillId}": skipped file "${relative2(srcDir, srcPath)}" because the mirror's ${MAX_TOTAL_BYTES}-byte total size limit would be exceeded.`
1225
+ );
1226
+ continue;
1227
+ }
1228
+ mkdirSync3(dirname2(destPath), { recursive: true });
1229
+ copyFileSync(srcPath, destPath);
1230
+ bytes += size;
1231
+ } catch {
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ walk(srcDir, destDir);
1237
+ return { bytes, skipped };
1238
+ }
1239
+ function writeIfChanged(path, content) {
1240
+ const existing = existsSync2(path) ? readFileSync4(path, "utf8") : void 0;
1241
+ if (existing === content) return false;
1242
+ mkdirSync3(dirname2(path), { recursive: true });
1243
+ writeFileSync3(path, content, "utf8");
1244
+ return true;
1245
+ }
1246
+ function ensureGitIgnored(dir, skillIds) {
1247
+ const path = join4(dir, IGNORE_FILE);
1248
+ const existing = existsSync2(path) ? readFileSync4(path, "utf8") : "";
1249
+ const lines = existing.split(/\r?\n/);
1250
+ const missing = [...skillIds, IGNORE_FILE].filter(
1251
+ (entry) => !lines.includes(entry)
1252
+ );
1253
+ if (missing.length === 0) return;
1254
+ const prefix = existing && !existing.endsWith("\n") ? `${existing}
1255
+ ` : existing;
1256
+ writeFileSync3(path, `${prefix}${missing.join("\n")}
1257
+ `, "utf8");
1258
+ }
1259
+ function writeSkillMirror(cwd, skills, warn) {
1260
+ if (skills.length === 0) {
1261
+ try {
1262
+ pruneStale(cwd, /* @__PURE__ */ new Set(), warn);
1263
+ } catch {
1264
+ }
1265
+ return "empty";
1266
+ }
1267
+ const dir = join4(cwd, SKILLS_DIR);
1268
+ let totalBytes = 0;
1269
+ let wroteAny = false;
1270
+ let partial = false;
1271
+ const mirroredIds = [];
1272
+ for (const skill of skills) {
1273
+ if (totalBytes >= MAX_TOTAL_BYTES) {
1274
+ warn(
1275
+ `Skill mirror: total size limit (${MAX_TOTAL_BYTES} bytes) reached; skipping remaining skills: ${skills.filter((s2) => !mirroredIds.includes(s2.id)).map((s2) => s2.id).join(", ")}`
1276
+ );
1277
+ break;
1278
+ }
1279
+ const skillDir = join4(dir, skill.id);
1280
+ const skillMdPath = join4(skillDir, "SKILL.md");
1281
+ if (existsSync2(skillMdPath)) {
1282
+ try {
1283
+ const existing = readFileSync4(skillMdPath, "utf8");
1284
+ if (!isGenerated(existing)) {
1285
+ warn(
1286
+ `.cursor/skills/${skill.id}/SKILL.md exists but was not generated by opencode-cursor; leaving it untouched and skipping this skill.`
1287
+ );
1288
+ continue;
1289
+ }
1290
+ } catch {
1291
+ }
1292
+ }
1293
+ try {
1294
+ const sourceContent = readFileSync4(
1295
+ join4(skill.sourceDir, "SKILL.md"),
1296
+ "utf8"
1297
+ );
1298
+ const stamped = stampSentinel(sourceContent);
1299
+ const skillMdBytes = Buffer.byteLength(stamped);
1300
+ if (totalBytes + skillMdBytes > MAX_TOTAL_BYTES) {
1301
+ warn(
1302
+ `Skill mirror: total size limit (${MAX_TOTAL_BYTES} bytes) reached; skipping skill "${skill.id}" and all remaining skills.`
1303
+ );
1304
+ partial = true;
1305
+ break;
1306
+ }
1307
+ const changed = writeIfChanged(skillMdPath, stamped);
1308
+ if (changed) wroteAny = true;
1309
+ totalBytes += skillMdBytes;
1310
+ const { bytes, skipped } = copyTree(
1311
+ skill.sourceDir,
1312
+ skillDir,
1313
+ skill.id,
1314
+ MAX_TOTAL_BYTES - totalBytes,
1315
+ warn
1316
+ );
1317
+ totalBytes += bytes;
1318
+ if (skipped.length > 0) partial = true;
1319
+ mirroredIds.push(skill.id);
1320
+ } catch (error) {
1321
+ partial = true;
1322
+ warn(
1323
+ `Failed to mirror skill "${skill.id}": ${error instanceof Error ? error.message : String(error)}. This skill will be unavailable this turn.`
1324
+ );
1325
+ }
1326
+ }
1327
+ try {
1328
+ const staleRemoved = pruneStale(
1329
+ cwd,
1330
+ new Set(mirroredIds),
1331
+ warn
1332
+ );
1333
+ if (staleRemoved) wroteAny = true;
1334
+ } catch {
1335
+ }
1336
+ if (mirroredIds.length > 0) {
1337
+ try {
1338
+ ensureGitIgnored(dir, mirroredIds);
1339
+ } catch {
1340
+ }
1341
+ }
1342
+ if (partial) return "partial";
1343
+ return wroteAny ? "written" : "unchanged";
1344
+ }
1345
+ function pruneStale(cwd, currentIds, warn) {
1346
+ const dir = join4(cwd, SKILLS_DIR);
1347
+ if (!existsSync2(dir)) return false;
1348
+ let removed = false;
1349
+ let entries;
1350
+ try {
1351
+ entries = readdirSync2(dir, { withFileTypes: true });
1352
+ } catch {
1353
+ return false;
1354
+ }
1355
+ for (const entry of entries) {
1356
+ if (!entry.isDirectory()) continue;
1357
+ if (entry.name === "." || entry.name === "..") continue;
1358
+ if (currentIds.has(entry.name)) continue;
1359
+ const skillMdPath = join4(dir, entry.name, "SKILL.md");
1360
+ if (!existsSync2(skillMdPath)) continue;
1361
+ try {
1362
+ const content = readFileSync4(skillMdPath, "utf8");
1363
+ if (isGenerated(content)) {
1364
+ rmSync2(join4(dir, entry.name), { recursive: true, force: true });
1365
+ removed = true;
1366
+ }
1367
+ } catch {
1368
+ }
1369
+ }
1370
+ return removed;
1371
+ }
1372
+ function removeSkillMirror(cwd) {
1373
+ const dir = join4(cwd, SKILLS_DIR);
1374
+ if (!existsSync2(dir)) return;
1375
+ try {
1376
+ const entries = readdirSync2(dir, { withFileTypes: true });
1377
+ for (const entry of entries) {
1378
+ if (!entry.isDirectory()) continue;
1379
+ const skillMdPath = join4(dir, entry.name, "SKILL.md");
1380
+ if (!existsSync2(skillMdPath)) continue;
1381
+ try {
1382
+ if (isGenerated(readFileSync4(skillMdPath, "utf8"))) {
1383
+ rmSync2(join4(dir, entry.name), { recursive: true, force: true });
1384
+ }
1385
+ } catch {
1386
+ }
1387
+ }
1388
+ const remaining = readdirSync2(dir, { withFileTypes: true });
1389
+ const hasSentinelDir = remaining.some((e) => {
1390
+ if (!e.isDirectory()) return false;
1391
+ const p = join4(dir, e.name, "SKILL.md");
1392
+ if (!existsSync2(p)) return false;
1393
+ try {
1394
+ return isGenerated(readFileSync4(p, "utf8"));
1395
+ } catch {
1396
+ return false;
1397
+ }
1398
+ });
1399
+ if (!hasSentinelDir) {
1400
+ const ignorePath = join4(dir, IGNORE_FILE);
1401
+ if (existsSync2(ignorePath)) {
1402
+ try {
1403
+ rmSync2(ignorePath);
1404
+ } catch {
1405
+ }
1406
+ }
1407
+ }
1408
+ } catch {
1409
+ }
1410
+ }
1411
+ function buildSkillsCatalogue(skills) {
1412
+ if (skills.length === 0) return void 0;
1413
+ const lines = skills.map(
1414
+ (s2) => `- **${s2.id}**: ${s2.description}`
1415
+ );
1416
+ return [
1417
+ "<available_skills>",
1418
+ "The following skills are available. Load the matching skill from `.cursor/skills/<id>/SKILL.md` before starting relevant work:",
1419
+ "",
1420
+ ...lines,
1421
+ "</available_skills>"
1422
+ ].join("\n");
1423
+ }
1424
+
702
1425
  // src/plugin/index.ts
703
1426
  function apiKeyFromAuth(auth) {
704
1427
  return auth?.type === "api" ? auth.key : void 0;
@@ -750,6 +1473,10 @@ var CursorPlugin = async (input) => {
750
1473
  let resolvedCwd = directory ?? process.cwd();
751
1474
  let forwardMcp = true;
752
1475
  let userMcp = {};
1476
+ let forwardSkills = true;
1477
+ let skillFilterOptions;
1478
+ let lastSkillHash = "";
1479
+ let currentSkillsCatalogue = "";
753
1480
  const warnedOAuth = /* @__PURE__ */ new Set();
754
1481
  return {
755
1482
  auth: {
@@ -788,6 +1515,38 @@ var CursorPlugin = async (input) => {
788
1515
  }
789
1516
  const optionCwd = existingOptions["cwd"];
790
1517
  resolvedCwd = (typeof optionCwd === "string" ? optionCwd : void 0) ?? directory ?? process.cwd();
1518
+ forwardSkills = existingOptions["forwardSkills"] !== false;
1519
+ const skillsOpt = existingOptions["skills"];
1520
+ skillFilterOptions = skillsOpt;
1521
+ if (forwardSkills) {
1522
+ try {
1523
+ const resolved = resolveSkills(
1524
+ resolvedCwd,
1525
+ config,
1526
+ skillFilterOptions
1527
+ );
1528
+ writeSkillMirror(
1529
+ resolvedCwd,
1530
+ resolved.skills,
1531
+ (msg) => pluginLog("warn", msg)
1532
+ );
1533
+ currentSkillsCatalogue = buildSkillsCatalogue(resolved.skills) ?? "";
1534
+ lastSkillHash = skillSetHash(resolved.skills);
1535
+ if (resolved.withheld.length > 0) {
1536
+ pluginLog("warn", "skills withheld from mirror", {
1537
+ withheld: resolved.withheld.map((w) => ({
1538
+ id: w.id,
1539
+ reason: w.reason
1540
+ }))
1541
+ });
1542
+ }
1543
+ } catch (error) {
1544
+ pluginLog("warn", "skill mirror failed", {
1545
+ error: error instanceof Error ? error.message : String(error),
1546
+ impact: "skills unavailable to the Cursor agent this session"
1547
+ });
1548
+ }
1549
+ }
791
1550
  config.provider[PROVIDER_ID] = {
792
1551
  name: "Cursor",
793
1552
  npm: providerNpm(),
@@ -796,7 +1555,8 @@ var CursorPlugin = async (input) => {
796
1555
  ...existingOptions,
797
1556
  cwd: resolvedCwd,
798
1557
  ...Object.keys(mcpServers).length > 0 ? { mcpServers } : {},
799
- ...Object.keys(modelParamDefaults).length > 0 ? { modelParamDefaults } : {}
1558
+ ...Object.keys(modelParamDefaults).length > 0 ? { modelParamDefaults } : {},
1559
+ ...currentSkillsCatalogue ? { skillsCatalogue: currentSkillsCatalogue } : {}
800
1560
  },
801
1561
  models: { ...toOpencodeModels(models), ...existing.models ?? {} }
802
1562
  };
@@ -863,6 +1623,32 @@ var CursorPlugin = async (input) => {
863
1623
  } catch {
864
1624
  }
865
1625
  }
1626
+ if (forwardSkills) {
1627
+ if (client) {
1628
+ try {
1629
+ const query = directory ? { query: { directory } } : void 0;
1630
+ const cfgRes = await client.config.get(query);
1631
+ const liveConfig = cfgRes?.data;
1632
+ const resolved = resolveSkills(
1633
+ resolvedCwd,
1634
+ liveConfig,
1635
+ skillFilterOptions
1636
+ );
1637
+ const hash = skillSetHash(resolved.skills);
1638
+ if (hash !== lastSkillHash) {
1639
+ writeSkillMirror(
1640
+ resolvedCwd,
1641
+ resolved.skills,
1642
+ (msg) => pluginLog("warn", msg)
1643
+ );
1644
+ currentSkillsCatalogue = buildSkillsCatalogue(resolved.skills) ?? "";
1645
+ lastSkillHash = hash;
1646
+ }
1647
+ } catch {
1648
+ }
1649
+ }
1650
+ output.options["skillsCatalogue"] = currentSkillsCatalogue;
1651
+ }
866
1652
  },
867
1653
  tool: {
868
1654
  cursor_update_plugin: {
@@ -902,7 +1688,7 @@ var CursorPlugin = async (input) => {
902
1688
  const cachePath = PLUGIN_CACHE_PATH;
903
1689
  const removeCommand = process.platform === "win32" ? `rmdir /s /q "${cachePath}"` : `rm -rf ${cachePath}`;
904
1690
  try {
905
- rmSync2(cachePath, { recursive: true, force: true });
1691
+ rmSync3(cachePath, { recursive: true, force: true });
906
1692
  clearVersionCache();
907
1693
  return {
908
1694
  title: "cursor plugin (updated)",
@@ -952,6 +1738,7 @@ then restart opencode.`,
952
1738
  },
953
1739
  dispose: async () => {
954
1740
  removeSystemRule(resolvedCwd);
1741
+ removeSkillMirror(resolvedCwd);
955
1742
  clearSubagentBridge();
956
1743
  clearLogBridge();
957
1744
  }