@wrongstack/core 0.310.1 → 0.313.1
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/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/project-server.js +84 -4868
- package/dist/chunk-2BUUIMQB.js +225 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2FQL4PDC.js +292 -0
- package/dist/chunk-2KKODIQU.js +2256 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3E244UDA.js +356 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-4LJJYBAU.js +388 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-4XV6IR37.js +187 -0
- package/dist/chunk-4ZHTKJHM.js +183 -0
- package/dist/chunk-52RTVH4M.js +361 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5DSMEVA4.js +931 -0
- package/dist/chunk-5RP3QTRL.js +1223 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-62V2AFVG.js +422 -0
- package/dist/chunk-65RDIOR2.js +2470 -0
- package/dist/chunk-6AKOTGLM.js +597 -0
- package/dist/chunk-7ZHZE5EO.js +28 -0
- package/dist/chunk-A455ATZB.js +140 -0
- package/dist/chunk-AD7ZFZCS.js +46 -0
- package/dist/chunk-AI4LLQ2U.js +856 -0
- package/dist/chunk-ASX6AW36.js +789 -0
- package/dist/chunk-AT4V4XGI.js +3522 -0
- package/dist/chunk-AWNQ6ED6.js +311 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-BAKVJLEV.js +417 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-CB2HFKTP.js +9 -0
- package/dist/chunk-CE4ZOCOI.js +1640 -0
- package/dist/chunk-CQQL4ZOQ.js +151 -0
- package/dist/chunk-DEIQMBD2.js +65 -0
- package/dist/chunk-E6I5RXVK.js +58 -0
- package/dist/chunk-EFEHDQGY.js +1589 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-EPC3GZD4.js +308 -0
- package/dist/chunk-FAYRX2AD.js +13 -0
- package/dist/chunk-FFSX2QDQ.js +362 -0
- package/dist/chunk-FHAMQS7Q.js +5856 -0
- package/dist/chunk-FIV4IJL7.js +2263 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FSWCWSTS.js +395 -0
- package/dist/chunk-FW5LDDMJ.js +16464 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5AAKEEZ.js +228 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GAZEWQEZ.js +231 -0
- package/dist/chunk-GFJOMSHG.js +125 -0
- package/dist/chunk-GFXZTHNI.js +158 -0
- package/dist/chunk-GM54GJBU.js +960 -0
- package/dist/chunk-GPQ3NJPR.js +900 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-H5E77KUJ.js +15 -0
- package/dist/chunk-H72H4XDU.js +1023 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-I45KAW2N.js +5020 -0
- package/dist/chunk-IQ35HMQL.js +1396 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-JJPMWPB6.js +109 -0
- package/dist/chunk-JTHKOYWU.js +663 -0
- package/dist/chunk-JVGPR3TS.js +365 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K3G6JBUC.js +5232 -0
- package/dist/chunk-K4YUNKU7.js +589 -0
- package/dist/chunk-KBZGE6LN.js +710 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KI4L2DUL.js +560 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KSJ5UTP7.js +154 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LZDDLNCZ.js +279 -0
- package/dist/chunk-M2WF6AXG.js +115 -0
- package/dist/chunk-M7BS2K6U.js +1 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NFAYHPRH.js +323 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-O74MJHXU.js +187 -0
- package/dist/chunk-OJZEX73F.js +112 -0
- package/dist/chunk-PAAWLLZX.js +74 -0
- package/dist/chunk-PFXFQQRT.js +339 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-QRTG4XTI.js +188 -0
- package/dist/chunk-QVOVMXJR.js +1351 -0
- package/dist/chunk-RCQYT2I4.js +757 -0
- package/dist/chunk-RD6DEE36.js +1178 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RPOA7IAE.js +150 -0
- package/dist/chunk-SN2DQRL6.js +1172 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SQP64RCS.js +5391 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-T3KGZBU5.js +3171 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TYW2GI5I.js +1077 -0
- package/dist/chunk-UIWONWVG.js +4959 -0
- package/dist/chunk-VB3H5WWB.js +151 -0
- package/dist/chunk-W4A6QA3D.js +258 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WEBMSQP2.js +123 -0
- package/dist/chunk-WOEZV3Q4.js +333 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-WZO23AWO.js +2779 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-XQ2XGIBF.js +2086 -0
- package/dist/chunk-Y2GE5TSI.js +355 -0
- package/dist/chunk-YBUJCZ57.js +351 -0
- package/dist/chunk-YNCVSFGF.js +149 -0
- package/dist/chunk-YUPMXOGP.js +282 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZEHUCJHY.js +129 -0
- package/dist/chunk-ZYU2YOJ6.js +1496 -0
- package/dist/chunk-ZZ4V3WIZ.js +163 -0
- package/dist/coordination/agents/index.js +145 -6260
- package/dist/coordination/index.js +365 -34323
- package/dist/coordination/mailbox-project-server.js +48 -3220
- package/dist/coordination/mailbox-types.d.ts +13 -1
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/coordination/sqlite-mailbox.d.ts +2 -1
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12479
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/design/index.js +50 -1087
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1599 -21887
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +112 -5139
- package/dist/hq/protocol.js +21 -668
- package/dist/index.js +1410 -92655
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
- package/dist/kernel/events.d.ts +16 -7
- package/dist/kernel/index.js +14 -992
- package/dist/models/index.js +40 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/index.js +17 -279
- package/dist/plugin/index.js +584 -14835
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry-R23UBE7H.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/review-finding-store-DKAV3AS6.js +21 -0
- package/dist/review-report-store-OWY3EC3N.js +19 -0
- package/dist/security/index.js +114 -2202
- package/dist/session-catalog/index.js +36 -2103
- package/dist/session-catalog/project-server.js +42 -1435
- package/dist/skills/index.js +35 -1578
- package/dist/storage/index.js +182 -15600
- package/dist/tasking/index.js +10 -676
- package/dist/tools/index.js +52 -9589
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/tools.d.ts +2 -2
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1341
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/session-markers.js +8 -144
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +269 -5685
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/wiring/proxy-rewrite.d.ts +92 -4
- package/dist/wiring/proxy-rewrite.js +20 -61
- package/dist/worktree/index.js +6 -887
- package/package.json +4 -4
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildWin32CmdShimInvocation
|
|
3
|
+
} from "./chunk-LHOA5DWA.js";
|
|
4
|
+
import {
|
|
5
|
+
SKILL_LIMITS
|
|
6
|
+
} from "./chunk-XEBHSJLT.js";
|
|
7
|
+
import {
|
|
8
|
+
atomicWrite
|
|
9
|
+
} from "./chunk-7ZHZE5EO.js";
|
|
10
|
+
import {
|
|
11
|
+
isValidSkillNameFormat,
|
|
12
|
+
parseSkillFrontmatter,
|
|
13
|
+
validateSkillName
|
|
14
|
+
} from "./chunk-WEBMSQP2.js";
|
|
15
|
+
import {
|
|
16
|
+
ERROR_CODES,
|
|
17
|
+
FetchError,
|
|
18
|
+
FsError,
|
|
19
|
+
ParseError,
|
|
20
|
+
WrongStackError
|
|
21
|
+
} from "./chunk-WOEZV3Q4.js";
|
|
22
|
+
import {
|
|
23
|
+
toErrorMessage
|
|
24
|
+
} from "./chunk-TBRL2RCC.js";
|
|
25
|
+
import {
|
|
26
|
+
expectDefined
|
|
27
|
+
} from "./chunk-PFZO3C6H.js";
|
|
28
|
+
|
|
29
|
+
// src/skills/registry/github-direct-adapter.ts
|
|
30
|
+
var githubDirectAdapter = {
|
|
31
|
+
id: "github",
|
|
32
|
+
displayName: "GitHub (direct)",
|
|
33
|
+
async search(_query, _opts = {}) {
|
|
34
|
+
return { adapterId: "github", results: [], hasMore: false };
|
|
35
|
+
},
|
|
36
|
+
resolveInstallRef(registryId) {
|
|
37
|
+
const trimmed = registryId.trim();
|
|
38
|
+
if (!trimmed.includes("/")) {
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/skills/registry/skills-sh-adapter.ts
|
|
46
|
+
var DEFAULT_SKILLS_SH_URL = "https://skills.sh";
|
|
47
|
+
var SEARCH_TIMEOUT_MS = 15e3;
|
|
48
|
+
var MAX_PAGE_SIZE = 50;
|
|
49
|
+
var defaultFetcher = async (url) => {
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(url, {
|
|
53
|
+
signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
54
|
+
headers: {
|
|
55
|
+
Accept: "application/json",
|
|
56
|
+
"User-Agent": "wrongstack-skill-installer"
|
|
57
|
+
},
|
|
58
|
+
redirect: "follow"
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new FetchError({
|
|
62
|
+
message: `Network error querying skill registry: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
status: 0,
|
|
64
|
+
context: { url, op: "skills.sh.search" },
|
|
65
|
+
cause: err
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw new FetchError({
|
|
70
|
+
message: `Skill registry returned ${res.status} ${res.statusText}`,
|
|
71
|
+
status: res.status,
|
|
72
|
+
context: { url, op: "skills.sh.search" }
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return await res.json();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new ParseError({
|
|
79
|
+
message: `Skill registry response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
80
|
+
source: "skills.sh.search",
|
|
81
|
+
context: { url },
|
|
82
|
+
cause: err
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function createSkillsShAdapter(opts = {}) {
|
|
87
|
+
const baseUrl = (opts.baseUrl ?? DEFAULT_SKILLS_SH_URL).replace(/\/+$/, "");
|
|
88
|
+
const fetcher = opts.fetcher ?? defaultFetcher;
|
|
89
|
+
const id = "skills.sh";
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
displayName: "skills.sh",
|
|
93
|
+
async search(query, sopts = {}) {
|
|
94
|
+
const page = Math.max(1, sopts.page ?? 1);
|
|
95
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, sopts.pageSize ?? 20));
|
|
96
|
+
const q = query.trim();
|
|
97
|
+
if (!q) return { adapterId: id, results: [], hasMore: false };
|
|
98
|
+
const url = `${baseUrl}/api/skills?query=${encodeURIComponent(q)}&page=${page}&pageSize=${pageSize}&sortBy=installs`;
|
|
99
|
+
const raw = await fetcher(url);
|
|
100
|
+
const results = parseResults(raw, url);
|
|
101
|
+
return {
|
|
102
|
+
adapterId: id,
|
|
103
|
+
results,
|
|
104
|
+
hasMore: results.length === pageSize
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
resolveInstallRef(registryId) {
|
|
108
|
+
const trimmed = registryId.trim();
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
throw new ParseError({
|
|
111
|
+
message: "Empty skills.sh registry id.",
|
|
112
|
+
source: "skills.sh.resolveInstallRef"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const atIdx = trimmed.indexOf("@");
|
|
116
|
+
const repoPart = atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
|
|
117
|
+
const segs = repoPart.split("/").filter(Boolean);
|
|
118
|
+
if (segs.length !== 2) {
|
|
119
|
+
throw new ParseError({
|
|
120
|
+
message: `Invalid skills.sh id "${registryId}". Expected "<owner>/<repo>" or "<owner>/<repo>@<ref>".`,
|
|
121
|
+
source: "skills.sh.resolveInstallRef",
|
|
122
|
+
context: { registryId }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return trimmed;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function parseResults(raw, url) {
|
|
130
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
131
|
+
throw new ParseError({
|
|
132
|
+
message: "Skill registry response was not a JSON object.",
|
|
133
|
+
source: "skills.sh.search",
|
|
134
|
+
context: { url }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const results = raw.results;
|
|
138
|
+
if (!Array.isArray(results)) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const out = [];
|
|
142
|
+
for (let i = 0; i < results.length; i++) {
|
|
143
|
+
const entry = results[i];
|
|
144
|
+
if (!entry || typeof entry !== "object") continue;
|
|
145
|
+
const name = strField(entry, "name") ?? strField(entry, "slug");
|
|
146
|
+
const description = strField(entry, "description") ?? "";
|
|
147
|
+
const owner = strField(entry, "owner") ?? strField(entry, "author");
|
|
148
|
+
const repo = strField(entry, "repo") ?? strField(entry, "repository");
|
|
149
|
+
if (!name || !owner || !repo) continue;
|
|
150
|
+
const ref = strField(entry, "ref") ?? strField(entry, "branch") ?? strField(entry, "tag");
|
|
151
|
+
const installRef = ref ? `${owner}/${repo}@${ref}` : `${owner}/${repo}`;
|
|
152
|
+
out.push({
|
|
153
|
+
id: strField(entry, "id") ?? `${owner}/${repo}`,
|
|
154
|
+
name,
|
|
155
|
+
description,
|
|
156
|
+
author: owner,
|
|
157
|
+
installs: numField(entry, "installs") ?? numField(entry, "installCount"),
|
|
158
|
+
stars: numField(entry, "stars") ?? numField(entry, "starCount"),
|
|
159
|
+
securityScore: numField(entry, "securityScore") ?? numField(entry, "score"),
|
|
160
|
+
updatedAt: strField(entry, "updatedAt") ?? strField(entry, "updated_at"),
|
|
161
|
+
installRef
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function strField(rec, key) {
|
|
167
|
+
const v = rec[key];
|
|
168
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
169
|
+
}
|
|
170
|
+
function numField(rec, key) {
|
|
171
|
+
const v = rec[key];
|
|
172
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
173
|
+
if (typeof v === "string" && /^\d+(\.\d+)?$/.test(v.trim())) {
|
|
174
|
+
const n = Number(v);
|
|
175
|
+
if (Number.isFinite(n)) return n;
|
|
176
|
+
}
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/skills/skill-generator.ts
|
|
181
|
+
import { spawn } from "node:child_process";
|
|
182
|
+
import * as fs from "node:fs/promises";
|
|
183
|
+
import * as path from "node:path";
|
|
184
|
+
async function validateSkillNameAvailable(name, loader) {
|
|
185
|
+
const formatViolations = validateSkillName(name);
|
|
186
|
+
const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
|
|
187
|
+
const formatOk = formatViolations.length === 0;
|
|
188
|
+
const collisionBlocks = conflicts.some((c) => c.source === "project" || c.source === "user");
|
|
189
|
+
return {
|
|
190
|
+
ok: formatOk && !collisionBlocks,
|
|
191
|
+
formatViolations,
|
|
192
|
+
conflicts
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function generateSkillSkeleton(opts) {
|
|
196
|
+
const name = opts.name.trim();
|
|
197
|
+
if (!isValidSkillNameFormat(name)) {
|
|
198
|
+
throw new WrongStackError({
|
|
199
|
+
message: `Cannot generate skeleton: invalid skill name "${name}". Names must be kebab-case (a-z0-9 and hyphens), \u226464 chars.`,
|
|
200
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
201
|
+
subsystem: "general",
|
|
202
|
+
context: { name }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const description = opts.description.trim();
|
|
206
|
+
const triggers = (opts.triggerKeywords ?? []).map((k) => k.trim()).filter(Boolean).map((k) => `"${k}"`).join(", ");
|
|
207
|
+
const triggerLine = triggers ? `
|
|
208
|
+
Triggers: user says ${triggers}.` : "";
|
|
209
|
+
const version = opts.version?.trim() || "1.0.0";
|
|
210
|
+
const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
211
|
+
return `---
|
|
212
|
+
name: ${name}
|
|
213
|
+
description: |
|
|
214
|
+
${description || `Use this skill when <trigger situation>.`}${triggerLine}
|
|
215
|
+
version: ${version}
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
# ${title}
|
|
219
|
+
|
|
220
|
+
## Overview
|
|
221
|
+
|
|
222
|
+
${description || "One-line description of what this skill does."}
|
|
223
|
+
|
|
224
|
+
## Rules
|
|
225
|
+
|
|
226
|
+
1. Rule one
|
|
227
|
+
2. Rule two
|
|
228
|
+
|
|
229
|
+
## Patterns
|
|
230
|
+
|
|
231
|
+
### Do
|
|
232
|
+
|
|
233
|
+
\`\`\`ts
|
|
234
|
+
// good example
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
### Don't
|
|
238
|
+
|
|
239
|
+
\`\`\`ts
|
|
240
|
+
// bad example
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
## Skills in scope
|
|
244
|
+
|
|
245
|
+
- \`other-skill\` \u2014 for delegation when this skill needs help
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
function extractSkillFromPrompt(prompt) {
|
|
249
|
+
const text = prompt.trim();
|
|
250
|
+
if (!text) {
|
|
251
|
+
return { suggestedName: "", description: "", body: "", triggerKeywords: [] };
|
|
252
|
+
}
|
|
253
|
+
const headingMatch = text.match(/^#{1,6}\s+(.+?)\s*$/m);
|
|
254
|
+
const firstLine = text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? text;
|
|
255
|
+
const titleSource = headingMatch?.[1] ?? firstLine.replace(/^#+\s*/, "");
|
|
256
|
+
const suggestedName = toKebab(titleSource).slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN);
|
|
257
|
+
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
258
|
+
const description = paragraphs[0]?.replace(/^#{1,6}\s+/m, "").trim() ?? titleSource;
|
|
259
|
+
const body = paragraphs.slice(1).join("\n\n");
|
|
260
|
+
const quoted = [...text.matchAll(/"([^"]{2,40})"/g)].map((m) => m[1]).filter((q) => typeof q === "string").map((q) => q.toLowerCase());
|
|
261
|
+
const titleWords = titleSource.split(/\W+/).map((w) => w.toLowerCase()).filter((w) => w.length > 3 && !STOPWORDS.has(w));
|
|
262
|
+
const triggerKeywords = dedupe([...quoted, ...titleWords]).slice(0, 8);
|
|
263
|
+
return { suggestedName, description, body, triggerKeywords };
|
|
264
|
+
}
|
|
265
|
+
async function openInEditor(filePath, env = process.env) {
|
|
266
|
+
const editor = env["VISUAL"]?.trim() && env["VISUAL"] || env["EDITOR"]?.trim() && env["EDITOR"] || defaultEditor();
|
|
267
|
+
if (!editor) {
|
|
268
|
+
throw new WrongStackError({
|
|
269
|
+
message: "No editor found. Set the EDITOR or VISUAL environment variable (e.g. `export EDITOR=code` or `export EDITOR=vim`).",
|
|
270
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
271
|
+
subsystem: "general",
|
|
272
|
+
context: { filePath }
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const shell = process.platform === "win32";
|
|
276
|
+
const parts = editor.split(/\s+/).filter(Boolean);
|
|
277
|
+
if (parts.length === 0) {
|
|
278
|
+
throw new WrongStackError({
|
|
279
|
+
message: "Resolved editor command is empty.",
|
|
280
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
281
|
+
subsystem: "general",
|
|
282
|
+
context: { filePath }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const editorArgs = [...parts.slice(1), filePath];
|
|
286
|
+
const child = shell ? (() => {
|
|
287
|
+
const inv = buildWin32CmdShimInvocation(parts[0], editorArgs);
|
|
288
|
+
return spawn(inv.command, inv.args, {
|
|
289
|
+
stdio: "ignore",
|
|
290
|
+
detached: true,
|
|
291
|
+
windowsVerbatimArguments: inv.windowsVerbatimArguments,
|
|
292
|
+
// Suppresses the console flash the cmd.exe wrapper would otherwise
|
|
293
|
+
// show before the editor appears. Repo convention — see
|
|
294
|
+
// `core/tests/architecture/spawn-convention.test.ts`.
|
|
295
|
+
windowsHide: true
|
|
296
|
+
});
|
|
297
|
+
})() : spawn(parts[0], editorArgs, {
|
|
298
|
+
stdio: "ignore",
|
|
299
|
+
detached: true,
|
|
300
|
+
windowsHide: true
|
|
301
|
+
});
|
|
302
|
+
child.unref();
|
|
303
|
+
}
|
|
304
|
+
async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
305
|
+
const fm = parseSkillFrontmatter(body);
|
|
306
|
+
const name = fm.name;
|
|
307
|
+
if (!name || !isValidSkillNameFormat(name)) {
|
|
308
|
+
throw new WrongStackError({
|
|
309
|
+
message: "Skeleton body has no valid `name` in its frontmatter.",
|
|
310
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
311
|
+
subsystem: "general"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const skillDir = path.join(skillsDir, name);
|
|
315
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
316
|
+
if (!opts.overwrite) {
|
|
317
|
+
try {
|
|
318
|
+
await fs.access(skillFile);
|
|
319
|
+
throw new WrongStackError({
|
|
320
|
+
message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
|
|
321
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
322
|
+
subsystem: "general",
|
|
323
|
+
context: { skillFile }
|
|
324
|
+
});
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err instanceof WrongStackError) throw err;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
330
|
+
await fs.writeFile(skillFile, body, "utf8");
|
|
331
|
+
return skillFile;
|
|
332
|
+
}
|
|
333
|
+
function bodyLineAdvisory(body) {
|
|
334
|
+
const lines = body.split("\n").length;
|
|
335
|
+
return { lines, over: lines > SKILL_LIMITS.SKILL_BODY_LINE_LIMIT };
|
|
336
|
+
}
|
|
337
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
338
|
+
"the",
|
|
339
|
+
"and",
|
|
340
|
+
"for",
|
|
341
|
+
"with",
|
|
342
|
+
"that",
|
|
343
|
+
"this",
|
|
344
|
+
"from",
|
|
345
|
+
"your",
|
|
346
|
+
"have",
|
|
347
|
+
"will",
|
|
348
|
+
"are",
|
|
349
|
+
"was",
|
|
350
|
+
"were",
|
|
351
|
+
"been",
|
|
352
|
+
"into",
|
|
353
|
+
"when",
|
|
354
|
+
"use",
|
|
355
|
+
"skill",
|
|
356
|
+
"about"
|
|
357
|
+
]);
|
|
358
|
+
function toKebab(s) {
|
|
359
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN) || "skill";
|
|
360
|
+
}
|
|
361
|
+
function dedupe(arr) {
|
|
362
|
+
return [...new Set(arr)];
|
|
363
|
+
}
|
|
364
|
+
function defaultEditor() {
|
|
365
|
+
if (process.platform === "win32") return "notepad";
|
|
366
|
+
if (process.platform === "darwin") return "open";
|
|
367
|
+
return void 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/skills/github-fetcher.ts
|
|
371
|
+
import { createWriteStream } from "node:fs";
|
|
372
|
+
import * as fs2 from "node:fs/promises";
|
|
373
|
+
import * as os from "node:os";
|
|
374
|
+
import * as path2 from "node:path";
|
|
375
|
+
import { Readable } from "node:stream";
|
|
376
|
+
import { pipeline } from "node:stream/promises";
|
|
377
|
+
import { createGunzip } from "node:zlib";
|
|
378
|
+
function parseSkillRef(input) {
|
|
379
|
+
const trimmed = input.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
|
|
380
|
+
const atIdx = trimmed.indexOf("@");
|
|
381
|
+
let refPath;
|
|
382
|
+
let ref;
|
|
383
|
+
if (atIdx > 0) {
|
|
384
|
+
refPath = trimmed.slice(0, atIdx);
|
|
385
|
+
ref = trimmed.slice(atIdx + 1);
|
|
386
|
+
} else {
|
|
387
|
+
refPath = trimmed;
|
|
388
|
+
ref = "main";
|
|
389
|
+
}
|
|
390
|
+
const parts = refPath.split("/").filter(Boolean);
|
|
391
|
+
if (parts.length < 2) {
|
|
392
|
+
throw new WrongStackError({
|
|
393
|
+
message: `Invalid skill reference "${input}". Expected format: user/repo or user/repo@ref`,
|
|
394
|
+
code: ERROR_CODES.UNKNOWN,
|
|
395
|
+
subsystem: "general",
|
|
396
|
+
context: { input }
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return { owner: expectDefined(parts[0]), repo: expectDefined(parts[1]), ref };
|
|
400
|
+
}
|
|
401
|
+
async function writeBoundedGzipStream(source, outputPath, maxBytes, maxCompressedBytes = SKILL_LIMITS.MAX_TARBALL_SIZE) {
|
|
402
|
+
let compressedBytes = 0;
|
|
403
|
+
let writtenBytes = 0;
|
|
404
|
+
await pipeline(
|
|
405
|
+
source,
|
|
406
|
+
async function* (chunks) {
|
|
407
|
+
for await (const chunk of chunks) {
|
|
408
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
409
|
+
compressedBytes += buffer.byteLength;
|
|
410
|
+
if (compressedBytes > maxCompressedBytes) {
|
|
411
|
+
throw new WrongStackError({
|
|
412
|
+
message: `Compressed tarball too large. Max: ${maxCompressedBytes / 1024 / 1024}MB`,
|
|
413
|
+
code: ERROR_CODES.UNKNOWN,
|
|
414
|
+
subsystem: "general",
|
|
415
|
+
context: { compressedBytes, maxBytes: maxCompressedBytes }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
yield buffer;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
createGunzip(),
|
|
422
|
+
async function* (chunks) {
|
|
423
|
+
for await (const chunk of chunks) {
|
|
424
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
425
|
+
writtenBytes += buffer.byteLength;
|
|
426
|
+
if (writtenBytes > maxBytes) {
|
|
427
|
+
throw new WrongStackError({
|
|
428
|
+
message: `Uncompressed tarball too large. Max: ${maxBytes / 1024 / 1024}MB`,
|
|
429
|
+
code: ERROR_CODES.UNKNOWN,
|
|
430
|
+
subsystem: "general",
|
|
431
|
+
context: { uncompressedBytes: writtenBytes, maxBytes }
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
yield buffer;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
createWriteStream(outputPath, { flags: "wx" })
|
|
438
|
+
);
|
|
439
|
+
return writtenBytes;
|
|
440
|
+
}
|
|
441
|
+
function resolveGitHubToken(env = process.env) {
|
|
442
|
+
const raw = env["WRONGSTACK_GITHUB_TOKEN"] ?? env["GITHUB_TOKEN"] ?? env["GH_TOKEN"];
|
|
443
|
+
if (!raw) return void 0;
|
|
444
|
+
const trimmed = raw.trim();
|
|
445
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
446
|
+
}
|
|
447
|
+
async function downloadGitHubTarball(parsed) {
|
|
448
|
+
const url = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/tarball/${parsed.ref}`;
|
|
449
|
+
const token = resolveGitHubToken();
|
|
450
|
+
const headers = {
|
|
451
|
+
Accept: "application/vnd.github+json",
|
|
452
|
+
"User-Agent": "wrongstack-skill-installer"
|
|
453
|
+
};
|
|
454
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
455
|
+
let response;
|
|
456
|
+
try {
|
|
457
|
+
response = await fetch(url, {
|
|
458
|
+
signal: AbortSignal.timeout(3e4),
|
|
459
|
+
headers,
|
|
460
|
+
redirect: "follow"
|
|
461
|
+
});
|
|
462
|
+
} catch (err) {
|
|
463
|
+
throw new FetchError({
|
|
464
|
+
message: `Network error fetching ${parsed.owner}/${parsed.repo}: ${err instanceof Error ? err.message : String(err)}`,
|
|
465
|
+
status: 0,
|
|
466
|
+
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, op: "tarball" },
|
|
467
|
+
cause: err
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
if (response.status === 404) {
|
|
472
|
+
throw new WrongStackError({
|
|
473
|
+
message: `Repository not found: ${parsed.owner}/${parsed.repo}` + (parsed.ref !== "main" ? ` (ref: ${parsed.ref})` : "") + (token ? "" : ". If this is a private repo, set GITHUB_TOKEN (or GH_TOKEN)."),
|
|
474
|
+
code: ERROR_CODES.UNKNOWN,
|
|
475
|
+
subsystem: "general",
|
|
476
|
+
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, status: 404 }
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
if (response.status === 403) {
|
|
480
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
481
|
+
const isRateLimited = remaining === "0";
|
|
482
|
+
throw new WrongStackError({
|
|
483
|
+
message: isRateLimited ? `GitHub API rate limit exceeded. Set GITHUB_TOKEN (or GH_TOKEN) to use the higher authenticated limit.` : `Access denied: ${parsed.owner}/${parsed.repo}. The repository may be private (set GITHUB_TOKEN to install private repos) or rate-limited.`,
|
|
484
|
+
code: ERROR_CODES.UNKNOWN,
|
|
485
|
+
subsystem: "general",
|
|
486
|
+
context: {
|
|
487
|
+
owner: parsed.owner,
|
|
488
|
+
repo: parsed.repo,
|
|
489
|
+
status: 403,
|
|
490
|
+
rateLimited: isRateLimited,
|
|
491
|
+
hasToken: Boolean(token)
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
throw new WrongStackError({
|
|
496
|
+
message: `GitHub API error (${response.status}): ${response.statusText}`,
|
|
497
|
+
code: ERROR_CODES.UNKNOWN,
|
|
498
|
+
subsystem: "general",
|
|
499
|
+
context: { owner: parsed.owner, repo: parsed.repo, status: response.status }
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
const contentLength = response.headers.get("content-length");
|
|
503
|
+
if (contentLength && Number.parseInt(contentLength, 10) > SKILL_LIMITS.MAX_TARBALL_SIZE) {
|
|
504
|
+
throw new WrongStackError({
|
|
505
|
+
message: `Tarball too large (${(Number.parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Max: ${SKILL_LIMITS.MAX_TARBALL_SIZE / 1024 / 1024}MB`,
|
|
506
|
+
code: ERROR_CODES.UNKNOWN,
|
|
507
|
+
subsystem: "general",
|
|
508
|
+
context: {
|
|
509
|
+
owner: parsed.owner,
|
|
510
|
+
repo: parsed.repo,
|
|
511
|
+
contentLengthBytes: Number.parseInt(contentLength, 10),
|
|
512
|
+
maxBytes: SKILL_LIMITS.MAX_TARBALL_SIZE
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const tempDir = await fs2.mkdtemp(path2.join(os.tmpdir(), "wskill-"));
|
|
517
|
+
try {
|
|
518
|
+
if (!response.body) {
|
|
519
|
+
throw new WrongStackError({
|
|
520
|
+
message: "Empty response body from GitHub API",
|
|
521
|
+
code: ERROR_CODES.UNKNOWN,
|
|
522
|
+
subsystem: "general",
|
|
523
|
+
context: { owner: parsed.owner, repo: parsed.repo }
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
527
|
+
const tarPath = path2.join(tempDir, ".wrongstack-download.tar");
|
|
528
|
+
await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
|
|
529
|
+
const tarBuf = await fs2.readFile(tarPath);
|
|
530
|
+
try {
|
|
531
|
+
await extractTar(tarBuf, tempDir);
|
|
532
|
+
} finally {
|
|
533
|
+
await fs2.rm(tarPath, { force: true });
|
|
534
|
+
}
|
|
535
|
+
return { tempDir };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
await fs2.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
538
|
+
});
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function readTarSize(buf, start) {
|
|
543
|
+
const first = buf[start] ?? 0;
|
|
544
|
+
if ((first & 128) !== 0) {
|
|
545
|
+
if (first === 255) {
|
|
546
|
+
throw new Error("tar: negative base-256 size field");
|
|
547
|
+
}
|
|
548
|
+
let value = first & 127;
|
|
549
|
+
for (let i = start + 1; i < start + 12; i++) {
|
|
550
|
+
value = value * 256 + (buf[i] ?? 0);
|
|
551
|
+
if (!Number.isSafeInteger(value)) {
|
|
552
|
+
throw new Error("tar: base-256 size exceeds the safe integer range");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
const text = readTarString(buf, start, 12).trim();
|
|
558
|
+
if (text === "") return 0;
|
|
559
|
+
if (!/^[0-7]+$/.test(text)) {
|
|
560
|
+
throw new Error(`tar: unparseable size field ${JSON.stringify(text)}`);
|
|
561
|
+
}
|
|
562
|
+
const parsed = Number.parseInt(text, 8);
|
|
563
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
564
|
+
throw new Error("tar: size field out of range");
|
|
565
|
+
}
|
|
566
|
+
return parsed;
|
|
567
|
+
}
|
|
568
|
+
function readTarString(buf, start, maxLen) {
|
|
569
|
+
let end = start;
|
|
570
|
+
while (end < start + maxLen && end < buf.length && buf[end] !== 0) {
|
|
571
|
+
end++;
|
|
572
|
+
}
|
|
573
|
+
return buf.subarray(start, end).toString("utf8");
|
|
574
|
+
}
|
|
575
|
+
function stripTopDir(p) {
|
|
576
|
+
const idx = p.indexOf("/");
|
|
577
|
+
if (idx === -1) return "";
|
|
578
|
+
return p.slice(idx + 1);
|
|
579
|
+
}
|
|
580
|
+
async function extractTar(buf, destDir) {
|
|
581
|
+
let offset = 0;
|
|
582
|
+
while (offset + 512 <= buf.length) {
|
|
583
|
+
const header = buf.subarray(offset, offset + 512);
|
|
584
|
+
if (header.every((b) => b === 0)) break;
|
|
585
|
+
const name = readTarString(buf, offset, 100);
|
|
586
|
+
const prefix = readTarString(buf, offset + 345, 155);
|
|
587
|
+
const size = readTarSize(buf, offset + 124);
|
|
588
|
+
const typeflag = buf[offset + 156] ?? 0;
|
|
589
|
+
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
590
|
+
const relPath = stripTopDir(fullPath);
|
|
591
|
+
if (relPath && relPath !== "." && relPath !== "..") {
|
|
592
|
+
const destPath = path2.join(destDir, relPath);
|
|
593
|
+
const resolvedDest = path2.resolve(destPath);
|
|
594
|
+
const resolvedRoot = path2.resolve(destDir);
|
|
595
|
+
if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path2.sep)) {
|
|
596
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (typeflag === 53 || typeflag === 0) {
|
|
600
|
+
if (relPath.endsWith("/") || typeflag === 53) {
|
|
601
|
+
await fs2.mkdir(destPath, { recursive: true });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
|
|
605
|
+
const dir = path2.dirname(destPath);
|
|
606
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
607
|
+
const dataStart = offset + 512;
|
|
608
|
+
const dataEnd = dataStart + size;
|
|
609
|
+
if (dataEnd > buf.length) break;
|
|
610
|
+
await fs2.writeFile(destPath, buf.subarray(dataStart, dataEnd));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/skills/manifest-store.ts
|
|
618
|
+
import * as fs3 from "node:fs/promises";
|
|
619
|
+
import * as path3 from "node:path";
|
|
620
|
+
var SkillManifestStore = class {
|
|
621
|
+
manifestPath;
|
|
622
|
+
cache;
|
|
623
|
+
constructor(manifestPath) {
|
|
624
|
+
this.manifestPath = manifestPath;
|
|
625
|
+
}
|
|
626
|
+
async read() {
|
|
627
|
+
if (this.cache) return this.cache;
|
|
628
|
+
try {
|
|
629
|
+
const raw = await fs3.readFile(this.manifestPath, "utf8");
|
|
630
|
+
const data = JSON.parse(raw);
|
|
631
|
+
if (!Array.isArray(data.skills)) {
|
|
632
|
+
this.cache = { skills: [] };
|
|
633
|
+
} else {
|
|
634
|
+
this.cache = data;
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
this.cache = { skills: [] };
|
|
638
|
+
}
|
|
639
|
+
return this.cache;
|
|
640
|
+
}
|
|
641
|
+
async write(data) {
|
|
642
|
+
const dir = path3.dirname(this.manifestPath);
|
|
643
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
644
|
+
await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
|
|
645
|
+
this.cache = data;
|
|
646
|
+
}
|
|
647
|
+
async addEntry(entry) {
|
|
648
|
+
const data = await this.read();
|
|
649
|
+
data.skills = data.skills.filter((s) => !(s.name === entry.name && s.scope === entry.scope));
|
|
650
|
+
data.skills.push(entry);
|
|
651
|
+
await this.write(data);
|
|
652
|
+
}
|
|
653
|
+
async removeEntry(name, scope) {
|
|
654
|
+
const data = await this.read();
|
|
655
|
+
const before = data.skills.length;
|
|
656
|
+
data.skills = data.skills.filter((s) => !(s.name === name && s.scope === scope));
|
|
657
|
+
if (data.skills.length === before) return false;
|
|
658
|
+
await this.write(data);
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
async findByName(name) {
|
|
662
|
+
const data = await this.read();
|
|
663
|
+
return data.skills.filter((s) => s.name === name);
|
|
664
|
+
}
|
|
665
|
+
async findBySource(source) {
|
|
666
|
+
const data = await this.read();
|
|
667
|
+
return data.skills.filter((s) => s.source === source);
|
|
668
|
+
}
|
|
669
|
+
async listAll() {
|
|
670
|
+
const data = await this.read();
|
|
671
|
+
return data.skills;
|
|
672
|
+
}
|
|
673
|
+
/** Invalidate the in-memory cache (e.g. after external file changes). */
|
|
674
|
+
invalidateCache() {
|
|
675
|
+
this.cache = void 0;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/skills/skill-installer.ts
|
|
680
|
+
import * as fs4 from "node:fs/promises";
|
|
681
|
+
import * as path4 from "node:path";
|
|
682
|
+
function isInside(resolved, destDir) {
|
|
683
|
+
const root = path4.resolve(destDir);
|
|
684
|
+
return resolved === root || resolved.startsWith(root + path4.sep);
|
|
685
|
+
}
|
|
686
|
+
var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
|
|
687
|
+
var SkillInstaller = class {
|
|
688
|
+
opts;
|
|
689
|
+
manifest;
|
|
690
|
+
adapters;
|
|
691
|
+
constructor(opts) {
|
|
692
|
+
this.opts = opts;
|
|
693
|
+
this.manifest = new SkillManifestStore(opts.manifestPath);
|
|
694
|
+
this.adapters = opts.registryAdapters?.length ? opts.registryAdapters : [githubDirectAdapter];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Install skills from a skill reference.
|
|
698
|
+
*
|
|
699
|
+
* Accepts two ref formats:
|
|
700
|
+
* - `user/repo[@ref]` — direct GitHub install (original path)
|
|
701
|
+
* - `<adapterId>:<registryId>` — registry-resolved (e.g.
|
|
702
|
+
* `skills.sh:owner/repo@v1`); the
|
|
703
|
+
* adapter resolves it to a `user/repo`
|
|
704
|
+
* ref and the install proceeds as above.
|
|
705
|
+
*
|
|
706
|
+
* Supports both single-skill repos (SKILL.md at root) and multi-skill repos
|
|
707
|
+
* (skills/ subdirectory). The manifest records the GitHub source as
|
|
708
|
+
* `github:owner/repo` (so `/skill-update` keeps working) plus the originating
|
|
709
|
+
* registry in `registryFrom` when the install came through a registry.
|
|
710
|
+
*/
|
|
711
|
+
async install(refInput, opts) {
|
|
712
|
+
const resolved = this.resolveRef(refInput);
|
|
713
|
+
const parsed = parseSkillRef(resolved.installRef);
|
|
714
|
+
const scope = opts?.global ? "user" : "project";
|
|
715
|
+
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
716
|
+
const source = `github:${parsed.owner}/${parsed.repo}`;
|
|
717
|
+
this.opts.log?.(
|
|
718
|
+
resolved.fromRegistry ? `Resolving ${refInput} \u2192 ${resolved.installRef} (${resolved.adapterId}), downloading...` : `Downloading ${parsed.owner}/${parsed.repo}@${parsed.ref}...`
|
|
719
|
+
);
|
|
720
|
+
const { tempDir } = await downloadGitHubTarball(parsed);
|
|
721
|
+
try {
|
|
722
|
+
const skills = await this.detectSkills(tempDir);
|
|
723
|
+
if (skills.length === 0) {
|
|
724
|
+
throw new WrongStackError({
|
|
725
|
+
message: "No skills found in repository. Expected SKILL.md at root or skills/ subdirectory.",
|
|
726
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
727
|
+
subsystem: "general",
|
|
728
|
+
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref }
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const results = [];
|
|
732
|
+
for (const skill of skills) {
|
|
733
|
+
const existing = await this.manifest.findByName(skill.name);
|
|
734
|
+
const existingInScope = existing.find((e) => e.scope === scope);
|
|
735
|
+
if (existingInScope) {
|
|
736
|
+
this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
|
|
737
|
+
await this.removeSkillFiles(skill.name, scope);
|
|
738
|
+
}
|
|
739
|
+
const destDir = path4.join(targetDir, skill.name);
|
|
740
|
+
await fs4.mkdir(destDir, { recursive: true });
|
|
741
|
+
const copiedFiles = [];
|
|
742
|
+
for (const file of skill.files) {
|
|
743
|
+
const srcPath = path4.join(skill.baseDir, file);
|
|
744
|
+
const destPath = path4.join(destDir, file);
|
|
745
|
+
const resolved2 = path4.resolve(destPath);
|
|
746
|
+
if (!isInside(resolved2, destDir)) {
|
|
747
|
+
throw new FsError({
|
|
748
|
+
message: `Path traversal detected in skill file: ${file}`,
|
|
749
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
750
|
+
path: destPath,
|
|
751
|
+
context: { reason: "path_traversal", skillName: skill.name }
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
const stat2 = await fs4.stat(srcPath);
|
|
755
|
+
if (stat2.size > MAX_SKILL_FILE_SIZE) {
|
|
756
|
+
throw new FsError({
|
|
757
|
+
message: `Skill file "${file}" is too large (${(stat2.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
|
|
758
|
+
code: ERROR_CODES.FS_WRITE_FAILED,
|
|
759
|
+
path: srcPath,
|
|
760
|
+
context: { skillName: skill.name, fileSize: stat2.size, maxSize: MAX_SKILL_FILE_SIZE }
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
await fs4.mkdir(path4.dirname(destPath), { recursive: true });
|
|
764
|
+
await fs4.copyFile(srcPath, destPath);
|
|
765
|
+
copiedFiles.push(file);
|
|
766
|
+
}
|
|
767
|
+
const entry = {
|
|
768
|
+
name: skill.name,
|
|
769
|
+
source,
|
|
770
|
+
ref: parsed.ref,
|
|
771
|
+
scope,
|
|
772
|
+
projectHash: scope === "project" ? this.opts.projectHash : void 0,
|
|
773
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
774
|
+
files: copiedFiles,
|
|
775
|
+
// When the install came through a registry (e.g. skills.sh), record
|
|
776
|
+
// which adapter + registry id resolved to this GitHub repo, so the
|
|
777
|
+
// source is traceable. `source` stays `github:owner/repo` for
|
|
778
|
+
// backward compat with `/skill-update`.
|
|
779
|
+
...resolved.fromRegistry ? { registryFrom: { adapterId: resolved.adapterId, registryId: resolved.registryId } } : {}
|
|
780
|
+
};
|
|
781
|
+
await this.manifest.addEntry(entry);
|
|
782
|
+
results.push({
|
|
783
|
+
name: skill.name,
|
|
784
|
+
path: destDir,
|
|
785
|
+
scope,
|
|
786
|
+
source,
|
|
787
|
+
ref: parsed.ref,
|
|
788
|
+
skillCount: 1
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
this.invalidateLoaderCache();
|
|
792
|
+
return results;
|
|
793
|
+
} finally {
|
|
794
|
+
await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Import skills from a local directory (e.g. `.claude/skills`) into the
|
|
800
|
+
* project or user skills dir, optionally as symlinks. Used by `/skill-import`
|
|
801
|
+
* to take ownership of foreign skills so they can be edited/committed.
|
|
802
|
+
* Each direct subdirectory containing a valid `SKILL.md` is copied verbatim.
|
|
803
|
+
*/
|
|
804
|
+
async importFromDir(srcDir, opts) {
|
|
805
|
+
const scope = opts?.global ? "user" : "project";
|
|
806
|
+
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
807
|
+
let entries;
|
|
808
|
+
try {
|
|
809
|
+
entries = await fs4.readdir(srcDir, { withFileTypes: true });
|
|
810
|
+
} catch {
|
|
811
|
+
throw new WrongStackError({
|
|
812
|
+
message: `Source directory not found or not readable: ${srcDir}`,
|
|
813
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
814
|
+
subsystem: "general",
|
|
815
|
+
context: { srcDir }
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const results = [];
|
|
819
|
+
for (const e of entries) {
|
|
820
|
+
if (!await entryIsDirectory(srcDir, e)) continue;
|
|
821
|
+
const skillMdPath = path4.join(srcDir, e.name, "SKILL.md");
|
|
822
|
+
let content;
|
|
823
|
+
try {
|
|
824
|
+
content = await fs4.readFile(skillMdPath, "utf8");
|
|
825
|
+
} catch {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const fm = parseSkillFrontmatter(content);
|
|
829
|
+
if (!fm.name || !fm.description || !isValidSkillNameFormat(fm.name)) continue;
|
|
830
|
+
const existing = await this.manifest.findByName(fm.name);
|
|
831
|
+
if (existing.find((x) => x.scope === scope)) {
|
|
832
|
+
await this.removeSkillFiles(fm.name, scope);
|
|
833
|
+
}
|
|
834
|
+
const destDir = path4.join(targetDir, fm.name);
|
|
835
|
+
await fs4.mkdir(destDir, { recursive: true });
|
|
836
|
+
const srcSkillDir = path4.join(srcDir, e.name);
|
|
837
|
+
const files = await collectFiles(srcSkillDir, srcSkillDir);
|
|
838
|
+
const copiedFiles = [];
|
|
839
|
+
for (const file of files) {
|
|
840
|
+
const srcPath = path4.join(srcSkillDir, file);
|
|
841
|
+
const destPath = path4.join(destDir, file);
|
|
842
|
+
const resolved = path4.resolve(destPath);
|
|
843
|
+
if (!isInside(resolved, destDir)) {
|
|
844
|
+
throw new FsError({
|
|
845
|
+
message: `Path traversal detected in skill file: ${file}`,
|
|
846
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
847
|
+
path: destPath,
|
|
848
|
+
context: { reason: "path_traversal", skillName: fm.name }
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
await fs4.mkdir(path4.dirname(destPath), { recursive: true });
|
|
852
|
+
if (opts?.link) {
|
|
853
|
+
try {
|
|
854
|
+
await fs4.symlink(srcPath, destPath);
|
|
855
|
+
} catch (err) {
|
|
856
|
+
await fs4.copyFile(srcPath, destPath);
|
|
857
|
+
this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
await fs4.copyFile(srcPath, destPath);
|
|
861
|
+
}
|
|
862
|
+
copiedFiles.push(file);
|
|
863
|
+
}
|
|
864
|
+
await this.manifest.addEntry({
|
|
865
|
+
name: fm.name,
|
|
866
|
+
source: `import:${srcDir}`,
|
|
867
|
+
ref: "-",
|
|
868
|
+
scope,
|
|
869
|
+
projectHash: scope === "project" ? this.opts.projectHash : void 0,
|
|
870
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
871
|
+
files: copiedFiles
|
|
872
|
+
});
|
|
873
|
+
results.push({
|
|
874
|
+
name: fm.name,
|
|
875
|
+
path: destDir,
|
|
876
|
+
scope,
|
|
877
|
+
source: `import:${srcDir}`,
|
|
878
|
+
ref: "-",
|
|
879
|
+
skillCount: 1
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
this.invalidateLoaderCache();
|
|
883
|
+
return results;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Update installed skills.
|
|
887
|
+
* - No args: update all
|
|
888
|
+
* - Name: update that specific skill
|
|
889
|
+
* - Name + newRef: update to a different ref
|
|
890
|
+
*/
|
|
891
|
+
async update(nameOrRef, _opts) {
|
|
892
|
+
const result = { updated: [], unchanged: [], errors: [] };
|
|
893
|
+
const allEntries = await this.manifest.listAll();
|
|
894
|
+
let targets;
|
|
895
|
+
if (nameOrRef) {
|
|
896
|
+
const byName = allEntries.filter((e) => e.name === nameOrRef);
|
|
897
|
+
if (byName.length > 0) {
|
|
898
|
+
targets = byName;
|
|
899
|
+
} else {
|
|
900
|
+
try {
|
|
901
|
+
const parsed = parseSkillRef(nameOrRef);
|
|
902
|
+
const source = `github:${parsed.owner}/${parsed.repo}`;
|
|
903
|
+
targets = allEntries.filter((e) => e.source === source);
|
|
904
|
+
if (targets.length === 0) {
|
|
905
|
+
result.errors.push({
|
|
906
|
+
name: nameOrRef,
|
|
907
|
+
error: `No installed skills found matching "${nameOrRef}"`
|
|
908
|
+
});
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
result.errors.push({
|
|
913
|
+
name: nameOrRef,
|
|
914
|
+
error: `Invalid reference: ${nameOrRef}`
|
|
915
|
+
});
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} else {
|
|
920
|
+
targets = allEntries;
|
|
921
|
+
}
|
|
922
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
923
|
+
for (const entry of targets) {
|
|
924
|
+
const key = `${entry.source}@${entry.ref}`;
|
|
925
|
+
if (!bySource.has(key)) bySource.set(key, []);
|
|
926
|
+
bySource.get(key)?.push(entry);
|
|
927
|
+
}
|
|
928
|
+
for (const [, entries] of bySource) {
|
|
929
|
+
const first = expectDefined(entries[0]);
|
|
930
|
+
const scope = first.scope;
|
|
931
|
+
const isGlobal = scope === "user";
|
|
932
|
+
try {
|
|
933
|
+
const sourceRepo = first.source.replace("github:", "");
|
|
934
|
+
let refToInstall = first.ref;
|
|
935
|
+
if (nameOrRef && !allEntries.find((e) => e.name === nameOrRef)) {
|
|
936
|
+
try {
|
|
937
|
+
const parsed = parseSkillRef(nameOrRef);
|
|
938
|
+
refToInstall = parsed.ref;
|
|
939
|
+
} catch {
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
this.opts.log?.(`Updating ${first.source}@${refToInstall}...`);
|
|
943
|
+
const results = await this.install(`${sourceRepo}@${refToInstall}`, { global: isGlobal });
|
|
944
|
+
for (const r of results) {
|
|
945
|
+
result.updated.push({
|
|
946
|
+
name: r.name,
|
|
947
|
+
oldRef: first.ref,
|
|
948
|
+
newRef: refToInstall
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
} catch (err) {
|
|
952
|
+
const msg = toErrorMessage(err);
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
result.errors.push({ name: entry.name, error: msg });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Uninstall a skill by name.
|
|
962
|
+
*/
|
|
963
|
+
async uninstall(name, opts) {
|
|
964
|
+
const scope = opts?.global ? "user" : "project";
|
|
965
|
+
const entries = await this.manifest.findByName(name);
|
|
966
|
+
const entry = entries.find((e) => e.scope === scope);
|
|
967
|
+
if (!entry) {
|
|
968
|
+
throw new WrongStackError({
|
|
969
|
+
message: `Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`,
|
|
970
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
971
|
+
subsystem: "general",
|
|
972
|
+
context: { skillName: name, scope }
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
await this.removeSkillFiles(name, scope);
|
|
976
|
+
await this.manifest.removeEntry(name, scope);
|
|
977
|
+
this.invalidateLoaderCache();
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* List all installed skills from the manifest.
|
|
981
|
+
*/
|
|
982
|
+
async listInstalled() {
|
|
983
|
+
return this.manifest.listAll();
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Search across all configured registry adapters. Results from each adapter
|
|
987
|
+
* are merged (deduplicated by `installRef`, adapters earlier in the list win
|
|
988
|
+
* conflicts). Adapters that don't support search (e.g. github-direct)
|
|
989
|
+
* contribute nothing.
|
|
990
|
+
*/
|
|
991
|
+
async search(query, opts) {
|
|
992
|
+
const settled = await Promise.allSettled(this.adapters.map((a) => a.search(query, opts)));
|
|
993
|
+
const perAdapter = [];
|
|
994
|
+
const errors = [];
|
|
995
|
+
settled.forEach((s, i) => {
|
|
996
|
+
if (s.status === "fulfilled") perAdapter.push(s.value);
|
|
997
|
+
else {
|
|
998
|
+
const id = this.adapters[i]?.id ?? "?";
|
|
999
|
+
errors.push(`${id}: ${toErrorMessage(s.reason)}`);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
if (errors.length > 0) {
|
|
1003
|
+
this.opts.log?.(`Some registries failed during search: ${errors.join("; ")}`);
|
|
1004
|
+
}
|
|
1005
|
+
return dedupeSearchResults(perAdapter);
|
|
1006
|
+
}
|
|
1007
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
1008
|
+
/**
|
|
1009
|
+
* Detect skills in an extracted repository.
|
|
1010
|
+
* Returns an array of detected skills with their files.
|
|
1011
|
+
*/
|
|
1012
|
+
async detectSkills(baseDir) {
|
|
1013
|
+
const results = [];
|
|
1014
|
+
const rootSkillMd = path4.join(baseDir, "SKILL.md");
|
|
1015
|
+
try {
|
|
1016
|
+
await fs4.access(rootSkillMd);
|
|
1017
|
+
const content = await fs4.readFile(rootSkillMd, "utf8");
|
|
1018
|
+
const fm = parseSkillFrontmatter(content);
|
|
1019
|
+
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
1020
|
+
results.push({
|
|
1021
|
+
name: fm.name,
|
|
1022
|
+
baseDir,
|
|
1023
|
+
files: ["SKILL.md"]
|
|
1024
|
+
});
|
|
1025
|
+
return results;
|
|
1026
|
+
}
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
const skillsDir = path4.join(baseDir, "skills");
|
|
1030
|
+
try {
|
|
1031
|
+
const entries = await fs4.readdir(skillsDir, { withFileTypes: true });
|
|
1032
|
+
for (const entry of entries) {
|
|
1033
|
+
if (!entry.isDirectory()) continue;
|
|
1034
|
+
const skillFile = path4.join(skillsDir, entry.name, "SKILL.md");
|
|
1035
|
+
try {
|
|
1036
|
+
const content = await fs4.readFile(skillFile, "utf8");
|
|
1037
|
+
const fm = parseSkillFrontmatter(content);
|
|
1038
|
+
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
1039
|
+
const skillDir = path4.join(skillsDir, entry.name);
|
|
1040
|
+
const files = await collectFiles(skillDir, skillDir);
|
|
1041
|
+
results.push({
|
|
1042
|
+
name: fm.name,
|
|
1043
|
+
baseDir: skillDir,
|
|
1044
|
+
files
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
return results;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Remove all files for an installed skill.
|
|
1056
|
+
*/
|
|
1057
|
+
/**
|
|
1058
|
+
* Delete an installed skill's directory.
|
|
1059
|
+
*
|
|
1060
|
+
* The containment check is not ceremonial: this is a `recursive: true,
|
|
1061
|
+
* force: true` delete whose target is built from a skill NAME taken from
|
|
1062
|
+
* manifest frontmatter. `path.join` normalises `..`, so a crafted name
|
|
1063
|
+
* escapes `targetDir` and takes the recursive delete with it. This file
|
|
1064
|
+
* already had `isInside` and applied it on the copy path but not here.
|
|
1065
|
+
*
|
|
1066
|
+
* The strict-subdirectory requirement matters separately: `isInside` allows
|
|
1067
|
+
* equality by design, and an empty or `.` name resolves `skillDir` to
|
|
1068
|
+
* `targetDir` itself — deleting every installed skill (WS-097).
|
|
1069
|
+
*/
|
|
1070
|
+
async removeSkillFiles(name, scope) {
|
|
1071
|
+
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
1072
|
+
const root = path4.resolve(targetDir);
|
|
1073
|
+
const skillDir = path4.resolve(path4.join(targetDir, name));
|
|
1074
|
+
if (skillDir === root || !isInside(skillDir, root)) {
|
|
1075
|
+
throw new FsError({
|
|
1076
|
+
message: `Refusing to delete skill files outside the skills directory: ${name}`,
|
|
1077
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1078
|
+
path: skillDir,
|
|
1079
|
+
context: { reason: "path_traversal", skillName: name, scope }
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
await fs4.rm(skillDir, { recursive: true, force: true });
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Invalidate the skill loader's cache so newly installed skills appear.
|
|
1086
|
+
*/
|
|
1087
|
+
invalidateLoaderCache() {
|
|
1088
|
+
const loader = this.opts.skillLoader;
|
|
1089
|
+
if (loader && typeof loader.invalidateCache === "function") {
|
|
1090
|
+
loader.invalidateCache();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Resolve an install ref, dispatching registry-prefixed refs to the matching
|
|
1095
|
+
* adapter. Returns the concrete `user/repo[@ref]` install ref plus provenance
|
|
1096
|
+
* (which adapter resolved it, if any).
|
|
1097
|
+
*/
|
|
1098
|
+
resolveRef(refInput) {
|
|
1099
|
+
const trimmed = refInput.trim();
|
|
1100
|
+
const colonIdx = trimmed.indexOf(":");
|
|
1101
|
+
if (colonIdx > 0) {
|
|
1102
|
+
const maybeAdapterId = trimmed.slice(0, colonIdx);
|
|
1103
|
+
const adapter = this.adapters.find((a) => a.id === maybeAdapterId);
|
|
1104
|
+
if (adapter) {
|
|
1105
|
+
const registryId = trimmed.slice(colonIdx + 1);
|
|
1106
|
+
const installRef = adapter.resolveInstallRef(registryId);
|
|
1107
|
+
return { installRef, fromRegistry: true, adapterId: adapter.id, registryId };
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return { installRef: trimmed, fromRegistry: false, adapterId: "", registryId: "" };
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
async function entryIsDirectory(dir, entry) {
|
|
1114
|
+
if (entry.isDirectory()) return true;
|
|
1115
|
+
if (entry.isSymbolicLink()) {
|
|
1116
|
+
try {
|
|
1117
|
+
return (await fs4.stat(path4.join(dir, entry.name))).isDirectory();
|
|
1118
|
+
} catch {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
async function collectFiles(dir, baseDir) {
|
|
1125
|
+
const results = [];
|
|
1126
|
+
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1127
|
+
for (const entry of entries) {
|
|
1128
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1129
|
+
const relPath = path4.relative(baseDir, fullPath);
|
|
1130
|
+
if (entry.isDirectory()) {
|
|
1131
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1132
|
+
results.push(...await collectFiles(fullPath, baseDir));
|
|
1133
|
+
} else {
|
|
1134
|
+
results.push(relPath);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return results;
|
|
1138
|
+
}
|
|
1139
|
+
function dedupeSearchResults(perAdapter) {
|
|
1140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1141
|
+
const out = [];
|
|
1142
|
+
for (const block of perAdapter) {
|
|
1143
|
+
const kept = [];
|
|
1144
|
+
for (const r of block.results) {
|
|
1145
|
+
const key = r.installRef;
|
|
1146
|
+
if (seen.has(key)) continue;
|
|
1147
|
+
seen.add(key);
|
|
1148
|
+
kept.push(r);
|
|
1149
|
+
}
|
|
1150
|
+
if (kept.length > 0 || block.results.length > 0) {
|
|
1151
|
+
out.push({ ...block, results: kept });
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export {
|
|
1158
|
+
githubDirectAdapter,
|
|
1159
|
+
DEFAULT_SKILLS_SH_URL,
|
|
1160
|
+
createSkillsShAdapter,
|
|
1161
|
+
validateSkillNameAvailable,
|
|
1162
|
+
generateSkillSkeleton,
|
|
1163
|
+
extractSkillFromPrompt,
|
|
1164
|
+
openInEditor,
|
|
1165
|
+
writeSkeletonSkill,
|
|
1166
|
+
bodyLineAdvisory,
|
|
1167
|
+
parseSkillRef,
|
|
1168
|
+
downloadGitHubTarball,
|
|
1169
|
+
SkillManifestStore,
|
|
1170
|
+
SkillInstaller
|
|
1171
|
+
};
|
|
1172
|
+
//# sourceMappingURL=chunk-SN2DQRL6.js.map
|