@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.
- package/CHANGELOG.md +38 -0
- package/README.md +83 -4
- package/dist/{chunk-COE6ZXMP.js → chunk-RDY3H2LE.js} +7 -6
- package/dist/chunk-RDY3H2LE.js.map +1 -0
- package/dist/plugin/index.js +795 -8
- package/dist/plugin/index.js.map +1 -1
- package/dist/provider/index.d.ts +7 -0
- package/dist/provider/index.js +6 -2
- package/dist/provider/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-COE6ZXMP.js.map +0 -1
package/dist/plugin/index.js
CHANGED
|
@@ -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-
|
|
15
|
+
} from "../chunk-RDY3H2LE.js";
|
|
15
16
|
|
|
16
17
|
// src/plugin/index.ts
|
|
17
|
-
import { rmSync as
|
|
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:
|
|
242
|
-
|
|
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
|
-
|
|
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
|
}
|