@wrongstack/core 0.313.0 → 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 -34324
- package/dist/coordination/mailbox-project-server.js +48 -3276
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12480
- 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 -21888
- 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 -92656
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- 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 -9590
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1342
- 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 +18 -0
- package/dist/wiring/proxy-rewrite.js +18 -121
- package/dist/worktree/index.js +6 -887
- package/package.json +4 -4
package/dist/skills/index.js
CHANGED
|
@@ -1,1582 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
var FOREIGN_SKILL_TOOLS = [
|
|
3
|
-
{ id: "agents", subdir: "skills" },
|
|
4
|
-
// shared store (asm / agentskills.io ecosystem)
|
|
5
|
-
{ id: "codex", subdir: "skills" },
|
|
6
|
-
// OpenAI Codex CLI
|
|
7
|
-
{ id: "gemini", subdir: "skills" },
|
|
8
|
-
// Gemini CLI
|
|
9
|
-
{ id: "cursor", subdir: "skills" },
|
|
10
|
-
// Cursor (standard subdir, aligned with skills.sh ecosystem)
|
|
11
|
-
{ id: "qwen", subdir: "skills" },
|
|
12
|
-
// Qwen Code
|
|
13
|
-
{ id: "trae", subdir: "skills" },
|
|
14
|
-
// Trae
|
|
15
|
-
{ id: "windsurf", subdir: "skills" }
|
|
16
|
-
// Windsurf
|
|
17
|
-
];
|
|
18
|
-
var KNOWN_FOREIGN_IDS = new Set(FOREIGN_SKILL_TOOLS.map((t) => t.id));
|
|
19
|
-
function resolveForeignToolIds(opt) {
|
|
20
|
-
return resolveForeignToolIdsWithWarnings(opt).ids;
|
|
21
|
-
}
|
|
22
|
-
function resolveForeignToolIdsWithWarnings(opt) {
|
|
23
|
-
if (opt === false) return { ids: [], unknownIds: [] };
|
|
24
|
-
if (Array.isArray(opt)) {
|
|
25
|
-
const ids = [];
|
|
26
|
-
const unknownIds = [];
|
|
27
|
-
for (const id of opt) {
|
|
28
|
-
if (KNOWN_FOREIGN_IDS.has(id)) ids.push(id);
|
|
29
|
-
else unknownIds.push(id);
|
|
30
|
-
}
|
|
31
|
-
return { ids, unknownIds };
|
|
32
|
-
}
|
|
33
|
-
return { ids: FOREIGN_SKILL_TOOLS.map((t) => t.id), unknownIds: [] };
|
|
34
|
-
}
|
|
35
|
-
function securityScoreToTier(score) {
|
|
36
|
-
if (score == null || Number.isNaN(score)) return "low";
|
|
37
|
-
if (score < 30) return "low";
|
|
38
|
-
if (score < 70) return "medium";
|
|
39
|
-
return "high";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/skills/frontmatter.ts
|
|
43
|
-
var SCALAR_KEYS = /* @__PURE__ */ new Set([
|
|
44
|
-
"name",
|
|
45
|
-
"description",
|
|
46
|
-
"trigger",
|
|
47
|
-
"version",
|
|
48
|
-
"license",
|
|
49
|
-
"compatibility"
|
|
50
|
-
]);
|
|
51
|
-
function parseSkillFrontmatter(raw) {
|
|
52
|
-
const text = normalizeLineEndings(raw);
|
|
53
|
-
if (!text.startsWith("---")) return {};
|
|
54
|
-
const end = text.indexOf("\n---", 4);
|
|
55
|
-
if (end === -1) return {};
|
|
56
|
-
return parseFrontmatterBlock(text.slice(4, end));
|
|
57
|
-
}
|
|
58
|
-
function stripFrontmatter(raw) {
|
|
59
|
-
const text = normalizeLineEndings(raw);
|
|
60
|
-
if (!text.startsWith("---")) return text;
|
|
61
|
-
const end = text.indexOf("\n---", 4);
|
|
62
|
-
if (end === -1) return text;
|
|
63
|
-
let body = text.slice(end + 4);
|
|
64
|
-
if (body.startsWith("\n")) body = body.slice(1);
|
|
65
|
-
return body;
|
|
66
|
-
}
|
|
67
|
-
function normalizeLineEndings(s) {
|
|
68
|
-
return s.replace(/\r\n?/g, "\n");
|
|
69
|
-
}
|
|
70
|
-
function parseFrontmatterBlock(block) {
|
|
71
|
-
const out = {};
|
|
72
|
-
const lines = block.split("\n");
|
|
73
|
-
let i = 0;
|
|
74
|
-
while (i < lines.length) {
|
|
75
|
-
const line = lines[i] ?? "";
|
|
76
|
-
const m = /^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
|
|
77
|
-
if (!m) {
|
|
78
|
-
i++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const key = m[1] ?? "";
|
|
82
|
-
const rest = (m[2] ?? "").trim();
|
|
83
|
-
if (key === "metadata") {
|
|
84
|
-
const map = {};
|
|
85
|
-
i++;
|
|
86
|
-
while (i < lines.length) {
|
|
87
|
-
const sub = lines[i] ?? "";
|
|
88
|
-
const sm = /^\s+([a-zA-Z0-9_.-]+):\s*(.*)$/.exec(sub);
|
|
89
|
-
if (!sm) break;
|
|
90
|
-
map[sm[1] ?? ""] = unquote((sm[2] ?? "").trim());
|
|
91
|
-
i++;
|
|
92
|
-
}
|
|
93
|
-
if (Object.keys(map).length > 0) out.metadata = map;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (rest === "|" || rest === ">") {
|
|
97
|
-
const collected = [];
|
|
98
|
-
i++;
|
|
99
|
-
while (i < lines.length) {
|
|
100
|
-
const sub = lines[i] ?? "";
|
|
101
|
-
if (sub === "" || sub.startsWith(" ") || sub.startsWith(" ")) {
|
|
102
|
-
collected.push(sub.replace(/^\s+/, ""));
|
|
103
|
-
i++;
|
|
104
|
-
} else break;
|
|
105
|
-
}
|
|
106
|
-
out[normalizeKey(key)] = collected.join("\n").trim();
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (key === "allowed-tools" || key === "allowedTools" || key === "required-capabilities" || key === "requiredCapabilities" || key === "required-tools" || key === "requiredTools" || key === "optional-capabilities" || key === "optionalCapabilities") {
|
|
110
|
-
const values = rest.replace(/^\[/, "").replace(/\]$/, "").split(/[\s,]+/).map(unquote).filter(Boolean);
|
|
111
|
-
out[normalizeKey(key)] = values;
|
|
112
|
-
i++;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (SCALAR_KEYS.has(key)) {
|
|
116
|
-
out[key] = unquote(rest);
|
|
117
|
-
i++;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
i++;
|
|
121
|
-
}
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
function normalizeKey(key) {
|
|
125
|
-
if (key === "allowed-tools") return "allowedTools";
|
|
126
|
-
if (key === "required-capabilities") return "requiredCapabilities";
|
|
127
|
-
if (key === "required-tools") return "requiredTools";
|
|
128
|
-
if (key === "optional-capabilities") return "optionalCapabilities";
|
|
129
|
-
return key;
|
|
130
|
-
}
|
|
131
|
-
function unquote(s) {
|
|
132
|
-
if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
|
|
133
|
-
return s.slice(1, -1);
|
|
134
|
-
}
|
|
135
|
-
return s;
|
|
136
|
-
}
|
|
137
|
-
function isValidSkillNameFormat(name) {
|
|
138
|
-
return name.length >= 1 && name.length <= 64 && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
|
|
139
|
-
}
|
|
140
|
-
function validateSkillName(name, parentDirName) {
|
|
141
|
-
const errors = [];
|
|
142
|
-
if (!name || name.trim().length === 0) {
|
|
143
|
-
errors.push("name is empty");
|
|
144
|
-
return errors;
|
|
145
|
-
}
|
|
146
|
-
if (name.length > 64) errors.push(`name is ${name.length} characters (max 64)`);
|
|
147
|
-
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
148
|
-
errors.push(
|
|
149
|
-
"name must be lowercase letters, digits, and single hyphens only (no leading/trailing/consecutive hyphens)"
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (parentDirName !== void 0 && name !== parentDirName) {
|
|
153
|
-
errors.push(`name "${name}" must match its parent directory "${parentDirName}"`);
|
|
154
|
-
}
|
|
155
|
-
return errors;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// src/skills/github-fetcher.ts
|
|
159
|
-
import { createWriteStream } from "node:fs";
|
|
160
|
-
import * as fs from "node:fs/promises";
|
|
161
|
-
import * as os from "node:os";
|
|
162
|
-
import * as path from "node:path";
|
|
163
|
-
import { Readable } from "node:stream";
|
|
164
|
-
import { pipeline } from "node:stream/promises";
|
|
165
|
-
import { createGunzip } from "node:zlib";
|
|
166
|
-
|
|
167
|
-
// src/utils/error.ts
|
|
168
|
-
function toErrorMessage(err) {
|
|
169
|
-
return err instanceof Error ? err.message : String(err);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// src/types/errors.ts
|
|
173
|
-
var ERROR_CODES = {
|
|
174
|
-
// Provider
|
|
175
|
-
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
176
|
-
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
177
|
-
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
178
|
-
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
179
|
-
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
180
|
-
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
|
|
181
|
-
PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
|
|
182
|
-
// Tool
|
|
183
|
-
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
|
|
184
|
-
TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
|
|
185
|
-
TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
|
|
186
|
-
TOOL_TIMEOUT: "TOOL_TIMEOUT",
|
|
187
|
-
TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
|
|
188
|
-
// Config
|
|
189
|
-
CONFIG_INVALID: "CONFIG_INVALID",
|
|
190
|
-
CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
|
|
191
|
-
CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
|
|
192
|
-
CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
|
|
193
|
-
// Plugin
|
|
194
|
-
PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
|
|
195
|
-
PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
|
|
196
|
-
PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
|
|
197
|
-
// Agent
|
|
198
|
-
AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
|
|
199
|
-
AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
|
|
200
|
-
AGENT_ABORTED: "AGENT_ABORTED",
|
|
201
|
-
AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
|
|
202
|
-
// Session
|
|
203
|
-
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
204
|
-
SESSION_CORRUPTED: "SESSION_CORRUPTED",
|
|
205
|
-
SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
|
|
206
|
-
// Container / Registry
|
|
207
|
-
CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
|
|
208
|
-
CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
|
|
209
|
-
CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
|
|
210
|
-
REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
|
|
211
|
-
REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
|
|
212
|
-
REGISTRY_INVALID: "REGISTRY_INVALID",
|
|
213
|
-
// File system
|
|
214
|
-
FS_READ_FAILED: "FS_READ_FAILED",
|
|
215
|
-
FS_WRITE_FAILED: "FS_WRITE_FAILED",
|
|
216
|
-
FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
|
|
217
|
-
FS_DELETE_FAILED: "FS_DELETE_FAILED",
|
|
218
|
-
FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
|
|
219
|
-
// SDD (Spec-Driven Development)
|
|
220
|
-
SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
|
|
221
|
-
SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
|
|
222
|
-
SDD_INVALID_STATE: "SDD_INVALID_STATE",
|
|
223
|
-
SDD_NOT_READY: "SDD_NOT_READY",
|
|
224
|
-
// General
|
|
225
|
-
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
226
|
-
PARSE_FAILED: "PARSE_FAILED",
|
|
227
|
-
UNKNOWN: "UNKNOWN"
|
|
228
|
-
};
|
|
229
|
-
var WrongStackError = class extends Error {
|
|
230
|
-
code;
|
|
231
|
-
subsystem;
|
|
232
|
-
severity;
|
|
233
|
-
recoverable;
|
|
234
|
-
context;
|
|
235
|
-
constructor(opts) {
|
|
236
|
-
super(opts.message, { cause: opts.cause });
|
|
237
|
-
this.name = "WrongStackError";
|
|
238
|
-
this.code = opts.code;
|
|
239
|
-
this.subsystem = opts.subsystem;
|
|
240
|
-
this.severity = opts.severity ?? "error";
|
|
241
|
-
this.recoverable = opts.recoverable ?? false;
|
|
242
|
-
this.context = opts.context;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Render a one-line user-facing description.
|
|
246
|
-
* Subclasses should override for domain-specific formatting.
|
|
247
|
-
*/
|
|
248
|
-
describe() {
|
|
249
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
250
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
function formatContext(ctx) {
|
|
254
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
255
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
256
|
-
}
|
|
257
|
-
var FsError = class extends WrongStackError {
|
|
258
|
-
path;
|
|
259
|
-
constructor(opts) {
|
|
260
|
-
super({
|
|
261
|
-
message: opts.message,
|
|
262
|
-
code: opts.code,
|
|
263
|
-
subsystem: "fs",
|
|
264
|
-
severity: "error",
|
|
265
|
-
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
266
|
-
context: { path: opts.path, ...opts.context },
|
|
267
|
-
cause: opts.cause
|
|
268
|
-
});
|
|
269
|
-
this.name = "FsError";
|
|
270
|
-
this.path = opts.path;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
var FetchError = class extends WrongStackError {
|
|
274
|
-
status;
|
|
275
|
-
constructor(opts) {
|
|
276
|
-
super({
|
|
277
|
-
message: opts.message,
|
|
278
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
279
|
-
subsystem: "general",
|
|
280
|
-
severity: "error",
|
|
281
|
-
recoverable: opts.status === 429 || opts.status >= 500,
|
|
282
|
-
context: { status: opts.status, ...opts.context },
|
|
283
|
-
cause: opts.cause
|
|
284
|
-
});
|
|
285
|
-
this.name = "FetchError";
|
|
286
|
-
this.status = opts.status;
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
var ParseError = class extends WrongStackError {
|
|
290
|
-
source;
|
|
291
|
-
constructor(opts) {
|
|
292
|
-
super({
|
|
293
|
-
message: opts.message,
|
|
294
|
-
code: ERROR_CODES.PARSE_FAILED,
|
|
295
|
-
subsystem: "general",
|
|
296
|
-
severity: "error",
|
|
297
|
-
recoverable: false,
|
|
298
|
-
context: { source: opts.source, ...opts.context },
|
|
299
|
-
cause: opts.cause
|
|
300
|
-
});
|
|
301
|
-
this.name = "ParseError";
|
|
302
|
-
this.source = opts.source;
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// src/utils/expect-defined.ts
|
|
307
|
-
function expectDefined(value, label) {
|
|
308
|
-
if (value === null || value === void 0) {
|
|
309
|
-
const err = new Error(label ? `Expected ${label} to be defined` : "Expected value to be defined");
|
|
310
|
-
err.name = "ExpectDefinedError";
|
|
311
|
-
throw err;
|
|
312
|
-
}
|
|
313
|
-
return value;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// src/skills/limits.ts
|
|
317
|
-
var SKILL_LIMITS = {
|
|
318
|
-
/**
|
|
319
|
-
* Per-skill body cap when injecting a full skill body into the system prompt
|
|
320
|
-
* (eager mode), and when returning a skill body from the `skill` tool.
|
|
321
|
-
* ~4k tokens. Oversized bodies are truncated at a paragraph boundary.
|
|
322
|
-
*
|
|
323
|
-
* Consumers: `system-prompt-builder.capSkillBody`, `skill` tool body return.
|
|
324
|
-
*/
|
|
325
|
-
MAX_SKILL_BODY_CHARS: 16e3,
|
|
326
|
-
/**
|
|
327
|
-
* Per-resource cap when the `skill` tool loads a bundled file
|
|
328
|
-
* (`scripts/`, `references/`, `assets/`). ~8k tokens.
|
|
329
|
-
*
|
|
330
|
-
* Consumer: `skill` tool `loadResource`.
|
|
331
|
-
*/
|
|
332
|
-
MAX_RESOURCE_CHARS: 32e3,
|
|
333
|
-
/**
|
|
334
|
-
* Maximum number of bundled resource paths the `skill` tool lists in one
|
|
335
|
-
* response. Caps a pathological skill (huge `assets/` tree) from blowing up
|
|
336
|
-
* the tool result.
|
|
337
|
-
*
|
|
338
|
-
* Consumer: `skill` tool resource listing.
|
|
339
|
-
*/
|
|
340
|
-
MAX_LISTED_RESOURCES: 100,
|
|
341
|
-
/**
|
|
342
|
-
* Max size of a single installed skill file (SKILL.md or a bundled resource).
|
|
343
|
-
* Guards against a malicious registry skill shipping a multi-MB blob that
|
|
344
|
-
* then flows into the prompt. 100KB.
|
|
345
|
-
*
|
|
346
|
-
* Consumer: `SkillInstaller.install` / `importFromDir`.
|
|
347
|
-
*/
|
|
348
|
-
MAX_SKILL_FILE_SIZE: 100 * 1024,
|
|
349
|
-
/**
|
|
350
|
-
* Max size of a GitHub tarball downloaded by the skill installer. Guards
|
|
351
|
-
* against a repo accidentally (or maliciously) shipping a giant tarball that
|
|
352
|
-
* would be extracted into a temp dir. 50MB.
|
|
353
|
-
*
|
|
354
|
-
* Consumer: `downloadGitHubTarball`.
|
|
355
|
-
*/
|
|
356
|
-
MAX_TARBALL_SIZE: 50 * 1024 * 1024,
|
|
357
|
-
/**
|
|
358
|
-
* Maximum size of a GitHub skill tarball after gzip decompression. The
|
|
359
|
-
* compressed download limit alone does not protect against highly
|
|
360
|
-
* compressible archives expanding until the process runs out of memory.
|
|
361
|
-
*
|
|
362
|
-
* Consumer: `downloadGitHubTarball`.
|
|
363
|
-
*/
|
|
364
|
-
MAX_UNCOMPRESSED_TARBALL_SIZE: 64 * 1024 * 1024,
|
|
365
|
-
/**
|
|
366
|
-
* Default total char budget for skill bodies injected in eager mode when
|
|
367
|
-
* `config.skills.eagerMaxChars` is unset. ~6k tokens. Skills are injected
|
|
368
|
-
* highest-priority first; once the budget is exhausted the remaining skills
|
|
369
|
-
* are listed as a manifest the agent loads via the `skill` tool. Set very
|
|
370
|
-
* high to effectively disable budgeting.
|
|
371
|
-
*
|
|
372
|
-
* Consumer: `DefaultSystemPromptBuilder.buildMemoryAndSkills` (default for
|
|
373
|
-
* `skillEagerMaxChars`).
|
|
374
|
-
*/
|
|
375
|
-
EAGER_DEFAULT_MAX_CHARS: 24e3,
|
|
376
|
-
/**
|
|
377
|
-
* Default total char budget for compact (token-saving) skill bodies injected
|
|
378
|
-
* by `buildCompactSkillBodiesText`. Mirrors the eager ceiling so compact mode
|
|
379
|
-
* can never inject more total skill text than eager mode. Before this bound
|
|
380
|
-
* existed, compact mode injected EVERY skill's compact body with no cap, so
|
|
381
|
-
* the section grew linearly with the number of installed skills. Skills past
|
|
382
|
-
* the budget overflow to a name list the agent loads via the `skill` tool.
|
|
383
|
-
*
|
|
384
|
-
* Consumer: `buildCompactSkillBodiesText` (default for its `budget` param).
|
|
385
|
-
*/
|
|
386
|
-
COMPACT_DEFAULT_MAX_CHARS: 24e3,
|
|
387
|
-
/**
|
|
388
|
-
* Auto-compact body limits (the token-saving fallback used when a skill has
|
|
389
|
-
* no hand-crafted `SKILL.save.md`). The Overview and Rules sections are
|
|
390
|
-
* extracted and trimmed to these char budgets, then the total is capped.
|
|
391
|
-
*
|
|
392
|
-
* Consumer: `DefaultSkillLoader.compactSkillBody`.
|
|
393
|
-
*/
|
|
394
|
-
COMPACT_OVERVIEW_MAX: 200,
|
|
395
|
-
COMPACT_RULES_MAX: 350,
|
|
396
|
-
COMPACT_TOTAL_MAX: 450,
|
|
397
|
-
/**
|
|
398
|
-
* Max length of a skill name (agentskills.io spec). Enforced by the
|
|
399
|
-
* frontmatter validator's format regex plus this length bound.
|
|
400
|
-
*
|
|
401
|
-
* Consumer: `isValidSkillNameFormat`.
|
|
402
|
-
*/
|
|
403
|
-
SKILL_NAME_MAX_LEN: 64,
|
|
404
|
-
/**
|
|
405
|
-
* Soft line limit for a SKILL.md body, per the bundled `skill-creator` rule.
|
|
406
|
-
* Not enforced in code (a skill can exceed it) — surfaced by `/skill-gen
|
|
407
|
-
* validate` and the `skill-creator` skill as guidance to move deep material
|
|
408
|
-
* into `references/`.
|
|
409
|
-
*
|
|
410
|
-
* Consumer: `skill-creator` skill, `/skill-gen validate` advisory.
|
|
411
|
-
*/
|
|
412
|
-
SKILL_BODY_LINE_LIMIT: 500
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
// src/skills/github-fetcher.ts
|
|
416
|
-
function parseSkillRef(input) {
|
|
417
|
-
const trimmed = input.trim().replace(/^https?:\/\/github\.com\//, "").replace(/\.git$/, "");
|
|
418
|
-
const atIdx = trimmed.indexOf("@");
|
|
419
|
-
let refPath;
|
|
420
|
-
let ref;
|
|
421
|
-
if (atIdx > 0) {
|
|
422
|
-
refPath = trimmed.slice(0, atIdx);
|
|
423
|
-
ref = trimmed.slice(atIdx + 1);
|
|
424
|
-
} else {
|
|
425
|
-
refPath = trimmed;
|
|
426
|
-
ref = "main";
|
|
427
|
-
}
|
|
428
|
-
const parts = refPath.split("/").filter(Boolean);
|
|
429
|
-
if (parts.length < 2) {
|
|
430
|
-
throw new WrongStackError({
|
|
431
|
-
message: `Invalid skill reference "${input}". Expected format: user/repo or user/repo@ref`,
|
|
432
|
-
code: ERROR_CODES.UNKNOWN,
|
|
433
|
-
subsystem: "general",
|
|
434
|
-
context: { input }
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
return { owner: expectDefined(parts[0]), repo: expectDefined(parts[1]), ref };
|
|
438
|
-
}
|
|
439
|
-
async function writeBoundedGzipStream(source, outputPath, maxBytes, maxCompressedBytes = SKILL_LIMITS.MAX_TARBALL_SIZE) {
|
|
440
|
-
let compressedBytes = 0;
|
|
441
|
-
let writtenBytes = 0;
|
|
442
|
-
await pipeline(
|
|
443
|
-
source,
|
|
444
|
-
async function* (chunks) {
|
|
445
|
-
for await (const chunk of chunks) {
|
|
446
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
447
|
-
compressedBytes += buffer.byteLength;
|
|
448
|
-
if (compressedBytes > maxCompressedBytes) {
|
|
449
|
-
throw new WrongStackError({
|
|
450
|
-
message: `Compressed tarball too large. Max: ${maxCompressedBytes / 1024 / 1024}MB`,
|
|
451
|
-
code: ERROR_CODES.UNKNOWN,
|
|
452
|
-
subsystem: "general",
|
|
453
|
-
context: { compressedBytes, maxBytes: maxCompressedBytes }
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
yield buffer;
|
|
457
|
-
}
|
|
458
|
-
},
|
|
459
|
-
createGunzip(),
|
|
460
|
-
async function* (chunks) {
|
|
461
|
-
for await (const chunk of chunks) {
|
|
462
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
463
|
-
writtenBytes += buffer.byteLength;
|
|
464
|
-
if (writtenBytes > maxBytes) {
|
|
465
|
-
throw new WrongStackError({
|
|
466
|
-
message: `Uncompressed tarball too large. Max: ${maxBytes / 1024 / 1024}MB`,
|
|
467
|
-
code: ERROR_CODES.UNKNOWN,
|
|
468
|
-
subsystem: "general",
|
|
469
|
-
context: { uncompressedBytes: writtenBytes, maxBytes }
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
yield buffer;
|
|
473
|
-
}
|
|
474
|
-
},
|
|
475
|
-
createWriteStream(outputPath, { flags: "wx" })
|
|
476
|
-
);
|
|
477
|
-
return writtenBytes;
|
|
478
|
-
}
|
|
479
|
-
function resolveGitHubToken(env = process.env) {
|
|
480
|
-
const raw = env["WRONGSTACK_GITHUB_TOKEN"] ?? env["GITHUB_TOKEN"] ?? env["GH_TOKEN"];
|
|
481
|
-
if (!raw) return void 0;
|
|
482
|
-
const trimmed = raw.trim();
|
|
483
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
484
|
-
}
|
|
485
|
-
async function downloadGitHubTarball(parsed) {
|
|
486
|
-
const url = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/tarball/${parsed.ref}`;
|
|
487
|
-
const token = resolveGitHubToken();
|
|
488
|
-
const headers = {
|
|
489
|
-
Accept: "application/vnd.github+json",
|
|
490
|
-
"User-Agent": "wrongstack-skill-installer"
|
|
491
|
-
};
|
|
492
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
493
|
-
let response;
|
|
494
|
-
try {
|
|
495
|
-
response = await fetch(url, {
|
|
496
|
-
signal: AbortSignal.timeout(3e4),
|
|
497
|
-
headers,
|
|
498
|
-
redirect: "follow"
|
|
499
|
-
});
|
|
500
|
-
} catch (err) {
|
|
501
|
-
throw new FetchError({
|
|
502
|
-
message: `Network error fetching ${parsed.owner}/${parsed.repo}: ${err instanceof Error ? err.message : String(err)}`,
|
|
503
|
-
status: 0,
|
|
504
|
-
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, op: "tarball" },
|
|
505
|
-
cause: err
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
if (!response.ok) {
|
|
509
|
-
if (response.status === 404) {
|
|
510
|
-
throw new WrongStackError({
|
|
511
|
-
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)."),
|
|
512
|
-
code: ERROR_CODES.UNKNOWN,
|
|
513
|
-
subsystem: "general",
|
|
514
|
-
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, status: 404 }
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
if (response.status === 403) {
|
|
518
|
-
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
519
|
-
const isRateLimited = remaining === "0";
|
|
520
|
-
throw new WrongStackError({
|
|
521
|
-
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.`,
|
|
522
|
-
code: ERROR_CODES.UNKNOWN,
|
|
523
|
-
subsystem: "general",
|
|
524
|
-
context: {
|
|
525
|
-
owner: parsed.owner,
|
|
526
|
-
repo: parsed.repo,
|
|
527
|
-
status: 403,
|
|
528
|
-
rateLimited: isRateLimited,
|
|
529
|
-
hasToken: Boolean(token)
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
throw new WrongStackError({
|
|
534
|
-
message: `GitHub API error (${response.status}): ${response.statusText}`,
|
|
535
|
-
code: ERROR_CODES.UNKNOWN,
|
|
536
|
-
subsystem: "general",
|
|
537
|
-
context: { owner: parsed.owner, repo: parsed.repo, status: response.status }
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
const contentLength = response.headers.get("content-length");
|
|
541
|
-
if (contentLength && Number.parseInt(contentLength, 10) > SKILL_LIMITS.MAX_TARBALL_SIZE) {
|
|
542
|
-
throw new WrongStackError({
|
|
543
|
-
message: `Tarball too large (${(Number.parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Max: ${SKILL_LIMITS.MAX_TARBALL_SIZE / 1024 / 1024}MB`,
|
|
544
|
-
code: ERROR_CODES.UNKNOWN,
|
|
545
|
-
subsystem: "general",
|
|
546
|
-
context: {
|
|
547
|
-
owner: parsed.owner,
|
|
548
|
-
repo: parsed.repo,
|
|
549
|
-
contentLengthBytes: Number.parseInt(contentLength, 10),
|
|
550
|
-
maxBytes: SKILL_LIMITS.MAX_TARBALL_SIZE
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "wskill-"));
|
|
555
|
-
try {
|
|
556
|
-
if (!response.body) {
|
|
557
|
-
throw new WrongStackError({
|
|
558
|
-
message: "Empty response body from GitHub API",
|
|
559
|
-
code: ERROR_CODES.UNKNOWN,
|
|
560
|
-
subsystem: "general",
|
|
561
|
-
context: { owner: parsed.owner, repo: parsed.repo }
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
const nodeStream = Readable.fromWeb(response.body);
|
|
565
|
-
const tarPath = path.join(tempDir, ".wrongstack-download.tar");
|
|
566
|
-
await writeBoundedGzipStream(nodeStream, tarPath, SKILL_LIMITS.MAX_UNCOMPRESSED_TARBALL_SIZE);
|
|
567
|
-
const tarBuf = await fs.readFile(tarPath);
|
|
568
|
-
try {
|
|
569
|
-
await extractTar(tarBuf, tempDir);
|
|
570
|
-
} finally {
|
|
571
|
-
await fs.rm(tarPath, { force: true });
|
|
572
|
-
}
|
|
573
|
-
return { tempDir };
|
|
574
|
-
} catch (err) {
|
|
575
|
-
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
576
|
-
});
|
|
577
|
-
throw err;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
function readTarSize(buf, start) {
|
|
581
|
-
const first = buf[start] ?? 0;
|
|
582
|
-
if ((first & 128) !== 0) {
|
|
583
|
-
if (first === 255) {
|
|
584
|
-
throw new Error("tar: negative base-256 size field");
|
|
585
|
-
}
|
|
586
|
-
let value = first & 127;
|
|
587
|
-
for (let i = start + 1; i < start + 12; i++) {
|
|
588
|
-
value = value * 256 + (buf[i] ?? 0);
|
|
589
|
-
if (!Number.isSafeInteger(value)) {
|
|
590
|
-
throw new Error("tar: base-256 size exceeds the safe integer range");
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return value;
|
|
594
|
-
}
|
|
595
|
-
const text = readTarString(buf, start, 12).trim();
|
|
596
|
-
if (text === "") return 0;
|
|
597
|
-
if (!/^[0-7]+$/.test(text)) {
|
|
598
|
-
throw new Error(`tar: unparseable size field ${JSON.stringify(text)}`);
|
|
599
|
-
}
|
|
600
|
-
const parsed = Number.parseInt(text, 8);
|
|
601
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
602
|
-
throw new Error("tar: size field out of range");
|
|
603
|
-
}
|
|
604
|
-
return parsed;
|
|
605
|
-
}
|
|
606
|
-
function readTarString(buf, start, maxLen) {
|
|
607
|
-
let end = start;
|
|
608
|
-
while (end < start + maxLen && end < buf.length && buf[end] !== 0) {
|
|
609
|
-
end++;
|
|
610
|
-
}
|
|
611
|
-
return buf.subarray(start, end).toString("utf8");
|
|
612
|
-
}
|
|
613
|
-
function stripTopDir(p) {
|
|
614
|
-
const idx = p.indexOf("/");
|
|
615
|
-
if (idx === -1) return "";
|
|
616
|
-
return p.slice(idx + 1);
|
|
617
|
-
}
|
|
618
|
-
async function extractTar(buf, destDir) {
|
|
619
|
-
let offset = 0;
|
|
620
|
-
while (offset + 512 <= buf.length) {
|
|
621
|
-
const header = buf.subarray(offset, offset + 512);
|
|
622
|
-
if (header.every((b) => b === 0)) break;
|
|
623
|
-
const name = readTarString(buf, offset, 100);
|
|
624
|
-
const prefix = readTarString(buf, offset + 345, 155);
|
|
625
|
-
const size = readTarSize(buf, offset + 124);
|
|
626
|
-
const typeflag = buf[offset + 156] ?? 0;
|
|
627
|
-
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
628
|
-
const relPath = stripTopDir(fullPath);
|
|
629
|
-
if (relPath && relPath !== "." && relPath !== "..") {
|
|
630
|
-
const destPath = path.join(destDir, relPath);
|
|
631
|
-
const resolvedDest = path.resolve(destPath);
|
|
632
|
-
const resolvedRoot = path.resolve(destDir);
|
|
633
|
-
if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path.sep)) {
|
|
634
|
-
offset += 512 + Math.ceil(size / 512) * 512;
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if (typeflag === 53 || typeflag === 0) {
|
|
638
|
-
if (relPath.endsWith("/") || typeflag === 53) {
|
|
639
|
-
await fs.mkdir(destPath, { recursive: true });
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
|
|
643
|
-
const dir = path.dirname(destPath);
|
|
644
|
-
await fs.mkdir(dir, { recursive: true });
|
|
645
|
-
const dataStart = offset + 512;
|
|
646
|
-
const dataEnd = dataStart + size;
|
|
647
|
-
if (dataEnd > buf.length) break;
|
|
648
|
-
await fs.writeFile(destPath, buf.subarray(dataStart, dataEnd));
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
offset += 512 + Math.ceil(size / 512) * 512;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// src/skills/manifest-store.ts
|
|
656
|
-
import * as fs2 from "node:fs/promises";
|
|
657
|
-
import * as path2 from "node:path";
|
|
658
|
-
|
|
659
|
-
// src/utils/atomic-write.ts
|
|
1
|
+
import "../chunk-GTCU6DZ3.js";
|
|
660
2
|
import {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
return this.cache;
|
|
697
|
-
}
|
|
698
|
-
async write(data) {
|
|
699
|
-
const dir = path2.dirname(this.manifestPath);
|
|
700
|
-
await fs2.mkdir(dir, { recursive: true });
|
|
701
|
-
await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
|
|
702
|
-
this.cache = data;
|
|
703
|
-
}
|
|
704
|
-
async addEntry(entry) {
|
|
705
|
-
const data = await this.read();
|
|
706
|
-
data.skills = data.skills.filter((s) => !(s.name === entry.name && s.scope === entry.scope));
|
|
707
|
-
data.skills.push(entry);
|
|
708
|
-
await this.write(data);
|
|
709
|
-
}
|
|
710
|
-
async removeEntry(name, scope) {
|
|
711
|
-
const data = await this.read();
|
|
712
|
-
const before = data.skills.length;
|
|
713
|
-
data.skills = data.skills.filter((s) => !(s.name === name && s.scope === scope));
|
|
714
|
-
if (data.skills.length === before) return false;
|
|
715
|
-
await this.write(data);
|
|
716
|
-
return true;
|
|
717
|
-
}
|
|
718
|
-
async findByName(name) {
|
|
719
|
-
const data = await this.read();
|
|
720
|
-
return data.skills.filter((s) => s.name === name);
|
|
721
|
-
}
|
|
722
|
-
async findBySource(source) {
|
|
723
|
-
const data = await this.read();
|
|
724
|
-
return data.skills.filter((s) => s.source === source);
|
|
725
|
-
}
|
|
726
|
-
async listAll() {
|
|
727
|
-
const data = await this.read();
|
|
728
|
-
return data.skills;
|
|
729
|
-
}
|
|
730
|
-
/** Invalidate the in-memory cache (e.g. after external file changes). */
|
|
731
|
-
invalidateCache() {
|
|
732
|
-
this.cache = void 0;
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
// src/skills/registry/github-direct-adapter.ts
|
|
737
|
-
var githubDirectAdapter = {
|
|
738
|
-
id: "github",
|
|
739
|
-
displayName: "GitHub (direct)",
|
|
740
|
-
async search(_query, _opts = {}) {
|
|
741
|
-
return { adapterId: "github", results: [], hasMore: false };
|
|
742
|
-
},
|
|
743
|
-
resolveInstallRef(registryId) {
|
|
744
|
-
const trimmed = registryId.trim();
|
|
745
|
-
if (!trimmed.includes("/")) {
|
|
746
|
-
return trimmed;
|
|
747
|
-
}
|
|
748
|
-
return trimmed;
|
|
749
|
-
}
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
// src/skills/registry/skills-sh-adapter.ts
|
|
753
|
-
var DEFAULT_SKILLS_SH_URL = "https://skills.sh";
|
|
754
|
-
var SEARCH_TIMEOUT_MS = 15e3;
|
|
755
|
-
var MAX_PAGE_SIZE = 50;
|
|
756
|
-
var defaultFetcher = async (url) => {
|
|
757
|
-
let res;
|
|
758
|
-
try {
|
|
759
|
-
res = await fetch(url, {
|
|
760
|
-
signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
761
|
-
headers: {
|
|
762
|
-
Accept: "application/json",
|
|
763
|
-
"User-Agent": "wrongstack-skill-installer"
|
|
764
|
-
},
|
|
765
|
-
redirect: "follow"
|
|
766
|
-
});
|
|
767
|
-
} catch (err) {
|
|
768
|
-
throw new FetchError({
|
|
769
|
-
message: `Network error querying skill registry: ${err instanceof Error ? err.message : String(err)}`,
|
|
770
|
-
status: 0,
|
|
771
|
-
context: { url, op: "skills.sh.search" },
|
|
772
|
-
cause: err
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
if (!res.ok) {
|
|
776
|
-
throw new FetchError({
|
|
777
|
-
message: `Skill registry returned ${res.status} ${res.statusText}`,
|
|
778
|
-
status: res.status,
|
|
779
|
-
context: { url, op: "skills.sh.search" }
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
try {
|
|
783
|
-
return await res.json();
|
|
784
|
-
} catch (err) {
|
|
785
|
-
throw new ParseError({
|
|
786
|
-
message: `Skill registry response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
787
|
-
source: "skills.sh.search",
|
|
788
|
-
context: { url },
|
|
789
|
-
cause: err
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
function createSkillsShAdapter(opts = {}) {
|
|
794
|
-
const baseUrl = (opts.baseUrl ?? DEFAULT_SKILLS_SH_URL).replace(/\/+$/, "");
|
|
795
|
-
const fetcher = opts.fetcher ?? defaultFetcher;
|
|
796
|
-
const id = "skills.sh";
|
|
797
|
-
return {
|
|
798
|
-
id,
|
|
799
|
-
displayName: "skills.sh",
|
|
800
|
-
async search(query, sopts = {}) {
|
|
801
|
-
const page = Math.max(1, sopts.page ?? 1);
|
|
802
|
-
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, sopts.pageSize ?? 20));
|
|
803
|
-
const q = query.trim();
|
|
804
|
-
if (!q) return { adapterId: id, results: [], hasMore: false };
|
|
805
|
-
const url = `${baseUrl}/api/skills?query=${encodeURIComponent(q)}&page=${page}&pageSize=${pageSize}&sortBy=installs`;
|
|
806
|
-
const raw = await fetcher(url);
|
|
807
|
-
const results = parseResults(raw, url);
|
|
808
|
-
return {
|
|
809
|
-
adapterId: id,
|
|
810
|
-
results,
|
|
811
|
-
hasMore: results.length === pageSize
|
|
812
|
-
};
|
|
813
|
-
},
|
|
814
|
-
resolveInstallRef(registryId) {
|
|
815
|
-
const trimmed = registryId.trim();
|
|
816
|
-
if (!trimmed) {
|
|
817
|
-
throw new ParseError({
|
|
818
|
-
message: "Empty skills.sh registry id.",
|
|
819
|
-
source: "skills.sh.resolveInstallRef"
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
const atIdx = trimmed.indexOf("@");
|
|
823
|
-
const repoPart = atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
|
|
824
|
-
const segs = repoPart.split("/").filter(Boolean);
|
|
825
|
-
if (segs.length !== 2) {
|
|
826
|
-
throw new ParseError({
|
|
827
|
-
message: `Invalid skills.sh id "${registryId}". Expected "<owner>/<repo>" or "<owner>/<repo>@<ref>".`,
|
|
828
|
-
source: "skills.sh.resolveInstallRef",
|
|
829
|
-
context: { registryId }
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
return trimmed;
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
function parseResults(raw, url) {
|
|
837
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
838
|
-
throw new ParseError({
|
|
839
|
-
message: "Skill registry response was not a JSON object.",
|
|
840
|
-
source: "skills.sh.search",
|
|
841
|
-
context: { url }
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
const results = raw.results;
|
|
845
|
-
if (!Array.isArray(results)) {
|
|
846
|
-
return [];
|
|
847
|
-
}
|
|
848
|
-
const out = [];
|
|
849
|
-
for (let i = 0; i < results.length; i++) {
|
|
850
|
-
const entry = results[i];
|
|
851
|
-
if (!entry || typeof entry !== "object") continue;
|
|
852
|
-
const name = strField(entry, "name") ?? strField(entry, "slug");
|
|
853
|
-
const description = strField(entry, "description") ?? "";
|
|
854
|
-
const owner = strField(entry, "owner") ?? strField(entry, "author");
|
|
855
|
-
const repo = strField(entry, "repo") ?? strField(entry, "repository");
|
|
856
|
-
if (!name || !owner || !repo) continue;
|
|
857
|
-
const ref = strField(entry, "ref") ?? strField(entry, "branch") ?? strField(entry, "tag");
|
|
858
|
-
const installRef = ref ? `${owner}/${repo}@${ref}` : `${owner}/${repo}`;
|
|
859
|
-
out.push({
|
|
860
|
-
id: strField(entry, "id") ?? `${owner}/${repo}`,
|
|
861
|
-
name,
|
|
862
|
-
description,
|
|
863
|
-
author: owner,
|
|
864
|
-
installs: numField(entry, "installs") ?? numField(entry, "installCount"),
|
|
865
|
-
stars: numField(entry, "stars") ?? numField(entry, "starCount"),
|
|
866
|
-
securityScore: numField(entry, "securityScore") ?? numField(entry, "score"),
|
|
867
|
-
updatedAt: strField(entry, "updatedAt") ?? strField(entry, "updated_at"),
|
|
868
|
-
installRef
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
return out;
|
|
872
|
-
}
|
|
873
|
-
function strField(rec, key) {
|
|
874
|
-
const v = rec[key];
|
|
875
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
876
|
-
}
|
|
877
|
-
function numField(rec, key) {
|
|
878
|
-
const v = rec[key];
|
|
879
|
-
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
880
|
-
if (typeof v === "string" && /^\d+(\.\d+)?$/.test(v.trim())) {
|
|
881
|
-
const n = Number(v);
|
|
882
|
-
if (Number.isFinite(n)) return n;
|
|
883
|
-
}
|
|
884
|
-
return void 0;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// src/skills/skill-generator.ts
|
|
888
|
-
import { spawn } from "node:child_process";
|
|
889
|
-
import * as fs3 from "node:fs/promises";
|
|
890
|
-
import * as path3 from "node:path";
|
|
891
|
-
|
|
892
|
-
// src/utils/win32-cmd.ts
|
|
893
|
-
var WIN32_CMD_META = /[&|<>"%\r\n\0]/;
|
|
894
|
-
function buildWin32CmdShimInvocation(command, args = []) {
|
|
895
|
-
assertSafeWin32CmdArgs([command, ...args]);
|
|
896
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
897
|
-
return {
|
|
898
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
899
|
-
args: ["/d", "/c", line],
|
|
900
|
-
windowsVerbatimArguments: true
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
function assertSafeWin32CmdArgs(args) {
|
|
904
|
-
for (const arg of args) {
|
|
905
|
-
if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
|
|
906
|
-
throw new Error(
|
|
907
|
-
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
function quoteWin32CmdArg(arg) {
|
|
913
|
-
return `"${arg}"`;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// src/skills/skill-generator.ts
|
|
917
|
-
async function validateSkillNameAvailable(name, loader) {
|
|
918
|
-
const formatViolations = validateSkillName(name);
|
|
919
|
-
const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
|
|
920
|
-
const formatOk = formatViolations.length === 0;
|
|
921
|
-
const collisionBlocks = conflicts.some((c) => c.source === "project" || c.source === "user");
|
|
922
|
-
return {
|
|
923
|
-
ok: formatOk && !collisionBlocks,
|
|
924
|
-
formatViolations,
|
|
925
|
-
conflicts
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
function generateSkillSkeleton(opts) {
|
|
929
|
-
const name = opts.name.trim();
|
|
930
|
-
if (!isValidSkillNameFormat(name)) {
|
|
931
|
-
throw new WrongStackError({
|
|
932
|
-
message: `Cannot generate skeleton: invalid skill name "${name}". Names must be kebab-case (a-z0-9 and hyphens), \u226464 chars.`,
|
|
933
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
934
|
-
subsystem: "general",
|
|
935
|
-
context: { name }
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
const description = opts.description.trim();
|
|
939
|
-
const triggers = (opts.triggerKeywords ?? []).map((k) => k.trim()).filter(Boolean).map((k) => `"${k}"`).join(", ");
|
|
940
|
-
const triggerLine = triggers ? `
|
|
941
|
-
Triggers: user says ${triggers}.` : "";
|
|
942
|
-
const version = opts.version?.trim() || "1.0.0";
|
|
943
|
-
const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
944
|
-
return `---
|
|
945
|
-
name: ${name}
|
|
946
|
-
description: |
|
|
947
|
-
${description || `Use this skill when <trigger situation>.`}${triggerLine}
|
|
948
|
-
version: ${version}
|
|
949
|
-
---
|
|
950
|
-
|
|
951
|
-
# ${title}
|
|
952
|
-
|
|
953
|
-
## Overview
|
|
954
|
-
|
|
955
|
-
${description || "One-line description of what this skill does."}
|
|
956
|
-
|
|
957
|
-
## Rules
|
|
958
|
-
|
|
959
|
-
1. Rule one
|
|
960
|
-
2. Rule two
|
|
961
|
-
|
|
962
|
-
## Patterns
|
|
963
|
-
|
|
964
|
-
### Do
|
|
965
|
-
|
|
966
|
-
\`\`\`ts
|
|
967
|
-
// good example
|
|
968
|
-
\`\`\`
|
|
969
|
-
|
|
970
|
-
### Don't
|
|
971
|
-
|
|
972
|
-
\`\`\`ts
|
|
973
|
-
// bad example
|
|
974
|
-
\`\`\`
|
|
975
|
-
|
|
976
|
-
## Skills in scope
|
|
977
|
-
|
|
978
|
-
- \`other-skill\` \u2014 for delegation when this skill needs help
|
|
979
|
-
`;
|
|
980
|
-
}
|
|
981
|
-
function extractSkillFromPrompt(prompt) {
|
|
982
|
-
const text = prompt.trim();
|
|
983
|
-
if (!text) {
|
|
984
|
-
return { suggestedName: "", description: "", body: "", triggerKeywords: [] };
|
|
985
|
-
}
|
|
986
|
-
const headingMatch = text.match(/^#{1,6}\s+(.+?)\s*$/m);
|
|
987
|
-
const firstLine = text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? text;
|
|
988
|
-
const titleSource = headingMatch?.[1] ?? firstLine.replace(/^#+\s*/, "");
|
|
989
|
-
const suggestedName = toKebab(titleSource).slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN);
|
|
990
|
-
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
991
|
-
const description = paragraphs[0]?.replace(/^#{1,6}\s+/m, "").trim() ?? titleSource;
|
|
992
|
-
const body = paragraphs.slice(1).join("\n\n");
|
|
993
|
-
const quoted = [...text.matchAll(/"([^"]{2,40})"/g)].map((m) => m[1]).filter((q) => typeof q === "string").map((q) => q.toLowerCase());
|
|
994
|
-
const titleWords = titleSource.split(/\W+/).map((w) => w.toLowerCase()).filter((w) => w.length > 3 && !STOPWORDS.has(w));
|
|
995
|
-
const triggerKeywords = dedupe([...quoted, ...titleWords]).slice(0, 8);
|
|
996
|
-
return { suggestedName, description, body, triggerKeywords };
|
|
997
|
-
}
|
|
998
|
-
async function openInEditor(filePath, env = process.env) {
|
|
999
|
-
const editor = env["VISUAL"]?.trim() && env["VISUAL"] || env["EDITOR"]?.trim() && env["EDITOR"] || defaultEditor();
|
|
1000
|
-
if (!editor) {
|
|
1001
|
-
throw new WrongStackError({
|
|
1002
|
-
message: "No editor found. Set the EDITOR or VISUAL environment variable (e.g. `export EDITOR=code` or `export EDITOR=vim`).",
|
|
1003
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1004
|
-
subsystem: "general",
|
|
1005
|
-
context: { filePath }
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
const shell = process.platform === "win32";
|
|
1009
|
-
const parts = editor.split(/\s+/).filter(Boolean);
|
|
1010
|
-
if (parts.length === 0) {
|
|
1011
|
-
throw new WrongStackError({
|
|
1012
|
-
message: "Resolved editor command is empty.",
|
|
1013
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1014
|
-
subsystem: "general",
|
|
1015
|
-
context: { filePath }
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
const editorArgs = [...parts.slice(1), filePath];
|
|
1019
|
-
const child = shell ? (() => {
|
|
1020
|
-
const inv = buildWin32CmdShimInvocation(parts[0], editorArgs);
|
|
1021
|
-
return spawn(inv.command, inv.args, {
|
|
1022
|
-
stdio: "ignore",
|
|
1023
|
-
detached: true,
|
|
1024
|
-
windowsVerbatimArguments: inv.windowsVerbatimArguments,
|
|
1025
|
-
// Suppresses the console flash the cmd.exe wrapper would otherwise
|
|
1026
|
-
// show before the editor appears. Repo convention — see
|
|
1027
|
-
// `core/tests/architecture/spawn-convention.test.ts`.
|
|
1028
|
-
windowsHide: true
|
|
1029
|
-
});
|
|
1030
|
-
})() : spawn(parts[0], editorArgs, {
|
|
1031
|
-
stdio: "ignore",
|
|
1032
|
-
detached: true,
|
|
1033
|
-
windowsHide: true
|
|
1034
|
-
});
|
|
1035
|
-
child.unref();
|
|
1036
|
-
}
|
|
1037
|
-
async function writeSkeletonSkill(skillsDir, body, opts = {}) {
|
|
1038
|
-
const fm = parseSkillFrontmatter(body);
|
|
1039
|
-
const name = fm.name;
|
|
1040
|
-
if (!name || !isValidSkillNameFormat(name)) {
|
|
1041
|
-
throw new WrongStackError({
|
|
1042
|
-
message: "Skeleton body has no valid `name` in its frontmatter.",
|
|
1043
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1044
|
-
subsystem: "general"
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
const skillDir = path3.join(skillsDir, name);
|
|
1048
|
-
const skillFile = path3.join(skillDir, "SKILL.md");
|
|
1049
|
-
if (!opts.overwrite) {
|
|
1050
|
-
try {
|
|
1051
|
-
await fs3.access(skillFile);
|
|
1052
|
-
throw new WrongStackError({
|
|
1053
|
-
message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
|
|
1054
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1055
|
-
subsystem: "general",
|
|
1056
|
-
context: { skillFile }
|
|
1057
|
-
});
|
|
1058
|
-
} catch (err) {
|
|
1059
|
-
if (err instanceof WrongStackError) throw err;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
await fs3.mkdir(skillDir, { recursive: true });
|
|
1063
|
-
await fs3.writeFile(skillFile, body, "utf8");
|
|
1064
|
-
return skillFile;
|
|
1065
|
-
}
|
|
1066
|
-
function bodyLineAdvisory(body) {
|
|
1067
|
-
const lines = body.split("\n").length;
|
|
1068
|
-
return { lines, over: lines > SKILL_LIMITS.SKILL_BODY_LINE_LIMIT };
|
|
1069
|
-
}
|
|
1070
|
-
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1071
|
-
"the",
|
|
1072
|
-
"and",
|
|
1073
|
-
"for",
|
|
1074
|
-
"with",
|
|
1075
|
-
"that",
|
|
1076
|
-
"this",
|
|
1077
|
-
"from",
|
|
1078
|
-
"your",
|
|
1079
|
-
"have",
|
|
1080
|
-
"will",
|
|
1081
|
-
"are",
|
|
1082
|
-
"was",
|
|
1083
|
-
"were",
|
|
1084
|
-
"been",
|
|
1085
|
-
"into",
|
|
1086
|
-
"when",
|
|
1087
|
-
"use",
|
|
1088
|
-
"skill",
|
|
1089
|
-
"about"
|
|
1090
|
-
]);
|
|
1091
|
-
function toKebab(s) {
|
|
1092
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN) || "skill";
|
|
1093
|
-
}
|
|
1094
|
-
function dedupe(arr) {
|
|
1095
|
-
return [...new Set(arr)];
|
|
1096
|
-
}
|
|
1097
|
-
function defaultEditor() {
|
|
1098
|
-
if (process.platform === "win32") return "notepad";
|
|
1099
|
-
if (process.platform === "darwin") return "open";
|
|
1100
|
-
return void 0;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// src/skills/skill-installer.ts
|
|
1104
|
-
import * as fs4 from "node:fs/promises";
|
|
1105
|
-
import * as path4 from "node:path";
|
|
1106
|
-
function isInside(resolved, destDir) {
|
|
1107
|
-
const root = path4.resolve(destDir);
|
|
1108
|
-
return resolved === root || resolved.startsWith(root + path4.sep);
|
|
1109
|
-
}
|
|
1110
|
-
var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
|
|
1111
|
-
var SkillInstaller = class {
|
|
1112
|
-
opts;
|
|
1113
|
-
manifest;
|
|
1114
|
-
adapters;
|
|
1115
|
-
constructor(opts) {
|
|
1116
|
-
this.opts = opts;
|
|
1117
|
-
this.manifest = new SkillManifestStore(opts.manifestPath);
|
|
1118
|
-
this.adapters = opts.registryAdapters?.length ? opts.registryAdapters : [githubDirectAdapter];
|
|
1119
|
-
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Install skills from a skill reference.
|
|
1122
|
-
*
|
|
1123
|
-
* Accepts two ref formats:
|
|
1124
|
-
* - `user/repo[@ref]` — direct GitHub install (original path)
|
|
1125
|
-
* - `<adapterId>:<registryId>` — registry-resolved (e.g.
|
|
1126
|
-
* `skills.sh:owner/repo@v1`); the
|
|
1127
|
-
* adapter resolves it to a `user/repo`
|
|
1128
|
-
* ref and the install proceeds as above.
|
|
1129
|
-
*
|
|
1130
|
-
* Supports both single-skill repos (SKILL.md at root) and multi-skill repos
|
|
1131
|
-
* (skills/ subdirectory). The manifest records the GitHub source as
|
|
1132
|
-
* `github:owner/repo` (so `/skill-update` keeps working) plus the originating
|
|
1133
|
-
* registry in `registryFrom` when the install came through a registry.
|
|
1134
|
-
*/
|
|
1135
|
-
async install(refInput, opts) {
|
|
1136
|
-
const resolved = this.resolveRef(refInput);
|
|
1137
|
-
const parsed = parseSkillRef(resolved.installRef);
|
|
1138
|
-
const scope = opts?.global ? "user" : "project";
|
|
1139
|
-
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
1140
|
-
const source = `github:${parsed.owner}/${parsed.repo}`;
|
|
1141
|
-
this.opts.log?.(
|
|
1142
|
-
resolved.fromRegistry ? `Resolving ${refInput} \u2192 ${resolved.installRef} (${resolved.adapterId}), downloading...` : `Downloading ${parsed.owner}/${parsed.repo}@${parsed.ref}...`
|
|
1143
|
-
);
|
|
1144
|
-
const { tempDir } = await downloadGitHubTarball(parsed);
|
|
1145
|
-
try {
|
|
1146
|
-
const skills = await this.detectSkills(tempDir);
|
|
1147
|
-
if (skills.length === 0) {
|
|
1148
|
-
throw new WrongStackError({
|
|
1149
|
-
message: "No skills found in repository. Expected SKILL.md at root or skills/ subdirectory.",
|
|
1150
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1151
|
-
subsystem: "general",
|
|
1152
|
-
context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref }
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
const results = [];
|
|
1156
|
-
for (const skill of skills) {
|
|
1157
|
-
const existing = await this.manifest.findByName(skill.name);
|
|
1158
|
-
const existingInScope = existing.find((e) => e.scope === scope);
|
|
1159
|
-
if (existingInScope) {
|
|
1160
|
-
this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
|
|
1161
|
-
await this.removeSkillFiles(skill.name, scope);
|
|
1162
|
-
}
|
|
1163
|
-
const destDir = path4.join(targetDir, skill.name);
|
|
1164
|
-
await fs4.mkdir(destDir, { recursive: true });
|
|
1165
|
-
const copiedFiles = [];
|
|
1166
|
-
for (const file of skill.files) {
|
|
1167
|
-
const srcPath = path4.join(skill.baseDir, file);
|
|
1168
|
-
const destPath = path4.join(destDir, file);
|
|
1169
|
-
const resolved2 = path4.resolve(destPath);
|
|
1170
|
-
if (!isInside(resolved2, destDir)) {
|
|
1171
|
-
throw new FsError({
|
|
1172
|
-
message: `Path traversal detected in skill file: ${file}`,
|
|
1173
|
-
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1174
|
-
path: destPath,
|
|
1175
|
-
context: { reason: "path_traversal", skillName: skill.name }
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
const stat2 = await fs4.stat(srcPath);
|
|
1179
|
-
if (stat2.size > MAX_SKILL_FILE_SIZE) {
|
|
1180
|
-
throw new FsError({
|
|
1181
|
-
message: `Skill file "${file}" is too large (${(stat2.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
|
|
1182
|
-
code: ERROR_CODES.FS_WRITE_FAILED,
|
|
1183
|
-
path: srcPath,
|
|
1184
|
-
context: { skillName: skill.name, fileSize: stat2.size, maxSize: MAX_SKILL_FILE_SIZE }
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
await fs4.mkdir(path4.dirname(destPath), { recursive: true });
|
|
1188
|
-
await fs4.copyFile(srcPath, destPath);
|
|
1189
|
-
copiedFiles.push(file);
|
|
1190
|
-
}
|
|
1191
|
-
const entry = {
|
|
1192
|
-
name: skill.name,
|
|
1193
|
-
source,
|
|
1194
|
-
ref: parsed.ref,
|
|
1195
|
-
scope,
|
|
1196
|
-
projectHash: scope === "project" ? this.opts.projectHash : void 0,
|
|
1197
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1198
|
-
files: copiedFiles,
|
|
1199
|
-
// When the install came through a registry (e.g. skills.sh), record
|
|
1200
|
-
// which adapter + registry id resolved to this GitHub repo, so the
|
|
1201
|
-
// source is traceable. `source` stays `github:owner/repo` for
|
|
1202
|
-
// backward compat with `/skill-update`.
|
|
1203
|
-
...resolved.fromRegistry ? { registryFrom: { adapterId: resolved.adapterId, registryId: resolved.registryId } } : {}
|
|
1204
|
-
};
|
|
1205
|
-
await this.manifest.addEntry(entry);
|
|
1206
|
-
results.push({
|
|
1207
|
-
name: skill.name,
|
|
1208
|
-
path: destDir,
|
|
1209
|
-
scope,
|
|
1210
|
-
source,
|
|
1211
|
-
ref: parsed.ref,
|
|
1212
|
-
skillCount: 1
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
this.invalidateLoaderCache();
|
|
1216
|
-
return results;
|
|
1217
|
-
} finally {
|
|
1218
|
-
await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Import skills from a local directory (e.g. `.claude/skills`) into the
|
|
1224
|
-
* project or user skills dir, optionally as symlinks. Used by `/skill-import`
|
|
1225
|
-
* to take ownership of foreign skills so they can be edited/committed.
|
|
1226
|
-
* Each direct subdirectory containing a valid `SKILL.md` is copied verbatim.
|
|
1227
|
-
*/
|
|
1228
|
-
async importFromDir(srcDir, opts) {
|
|
1229
|
-
const scope = opts?.global ? "user" : "project";
|
|
1230
|
-
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
1231
|
-
let entries;
|
|
1232
|
-
try {
|
|
1233
|
-
entries = await fs4.readdir(srcDir, { withFileTypes: true });
|
|
1234
|
-
} catch {
|
|
1235
|
-
throw new WrongStackError({
|
|
1236
|
-
message: `Source directory not found or not readable: ${srcDir}`,
|
|
1237
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1238
|
-
subsystem: "general",
|
|
1239
|
-
context: { srcDir }
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
const results = [];
|
|
1243
|
-
for (const e of entries) {
|
|
1244
|
-
if (!await entryIsDirectory(srcDir, e)) continue;
|
|
1245
|
-
const skillMdPath = path4.join(srcDir, e.name, "SKILL.md");
|
|
1246
|
-
let content;
|
|
1247
|
-
try {
|
|
1248
|
-
content = await fs4.readFile(skillMdPath, "utf8");
|
|
1249
|
-
} catch {
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
const fm = parseSkillFrontmatter(content);
|
|
1253
|
-
if (!fm.name || !fm.description || !isValidSkillNameFormat(fm.name)) continue;
|
|
1254
|
-
const existing = await this.manifest.findByName(fm.name);
|
|
1255
|
-
if (existing.find((x) => x.scope === scope)) {
|
|
1256
|
-
await this.removeSkillFiles(fm.name, scope);
|
|
1257
|
-
}
|
|
1258
|
-
const destDir = path4.join(targetDir, fm.name);
|
|
1259
|
-
await fs4.mkdir(destDir, { recursive: true });
|
|
1260
|
-
const srcSkillDir = path4.join(srcDir, e.name);
|
|
1261
|
-
const files = await collectFiles(srcSkillDir, srcSkillDir);
|
|
1262
|
-
const copiedFiles = [];
|
|
1263
|
-
for (const file of files) {
|
|
1264
|
-
const srcPath = path4.join(srcSkillDir, file);
|
|
1265
|
-
const destPath = path4.join(destDir, file);
|
|
1266
|
-
const resolved = path4.resolve(destPath);
|
|
1267
|
-
if (!isInside(resolved, destDir)) {
|
|
1268
|
-
throw new FsError({
|
|
1269
|
-
message: `Path traversal detected in skill file: ${file}`,
|
|
1270
|
-
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1271
|
-
path: destPath,
|
|
1272
|
-
context: { reason: "path_traversal", skillName: fm.name }
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
1275
|
-
await fs4.mkdir(path4.dirname(destPath), { recursive: true });
|
|
1276
|
-
if (opts?.link) {
|
|
1277
|
-
try {
|
|
1278
|
-
await fs4.symlink(srcPath, destPath);
|
|
1279
|
-
} catch (err) {
|
|
1280
|
-
await fs4.copyFile(srcPath, destPath);
|
|
1281
|
-
this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
|
|
1282
|
-
}
|
|
1283
|
-
} else {
|
|
1284
|
-
await fs4.copyFile(srcPath, destPath);
|
|
1285
|
-
}
|
|
1286
|
-
copiedFiles.push(file);
|
|
1287
|
-
}
|
|
1288
|
-
await this.manifest.addEntry({
|
|
1289
|
-
name: fm.name,
|
|
1290
|
-
source: `import:${srcDir}`,
|
|
1291
|
-
ref: "-",
|
|
1292
|
-
scope,
|
|
1293
|
-
projectHash: scope === "project" ? this.opts.projectHash : void 0,
|
|
1294
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1295
|
-
files: copiedFiles
|
|
1296
|
-
});
|
|
1297
|
-
results.push({
|
|
1298
|
-
name: fm.name,
|
|
1299
|
-
path: destDir,
|
|
1300
|
-
scope,
|
|
1301
|
-
source: `import:${srcDir}`,
|
|
1302
|
-
ref: "-",
|
|
1303
|
-
skillCount: 1
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
this.invalidateLoaderCache();
|
|
1307
|
-
return results;
|
|
1308
|
-
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Update installed skills.
|
|
1311
|
-
* - No args: update all
|
|
1312
|
-
* - Name: update that specific skill
|
|
1313
|
-
* - Name + newRef: update to a different ref
|
|
1314
|
-
*/
|
|
1315
|
-
async update(nameOrRef, _opts) {
|
|
1316
|
-
const result = { updated: [], unchanged: [], errors: [] };
|
|
1317
|
-
const allEntries = await this.manifest.listAll();
|
|
1318
|
-
let targets;
|
|
1319
|
-
if (nameOrRef) {
|
|
1320
|
-
const byName = allEntries.filter((e) => e.name === nameOrRef);
|
|
1321
|
-
if (byName.length > 0) {
|
|
1322
|
-
targets = byName;
|
|
1323
|
-
} else {
|
|
1324
|
-
try {
|
|
1325
|
-
const parsed = parseSkillRef(nameOrRef);
|
|
1326
|
-
const source = `github:${parsed.owner}/${parsed.repo}`;
|
|
1327
|
-
targets = allEntries.filter((e) => e.source === source);
|
|
1328
|
-
if (targets.length === 0) {
|
|
1329
|
-
result.errors.push({
|
|
1330
|
-
name: nameOrRef,
|
|
1331
|
-
error: `No installed skills found matching "${nameOrRef}"`
|
|
1332
|
-
});
|
|
1333
|
-
return result;
|
|
1334
|
-
}
|
|
1335
|
-
} catch {
|
|
1336
|
-
result.errors.push({
|
|
1337
|
-
name: nameOrRef,
|
|
1338
|
-
error: `Invalid reference: ${nameOrRef}`
|
|
1339
|
-
});
|
|
1340
|
-
return result;
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
} else {
|
|
1344
|
-
targets = allEntries;
|
|
1345
|
-
}
|
|
1346
|
-
const bySource = /* @__PURE__ */ new Map();
|
|
1347
|
-
for (const entry of targets) {
|
|
1348
|
-
const key = `${entry.source}@${entry.ref}`;
|
|
1349
|
-
if (!bySource.has(key)) bySource.set(key, []);
|
|
1350
|
-
bySource.get(key)?.push(entry);
|
|
1351
|
-
}
|
|
1352
|
-
for (const [, entries] of bySource) {
|
|
1353
|
-
const first = expectDefined(entries[0]);
|
|
1354
|
-
const scope = first.scope;
|
|
1355
|
-
const isGlobal = scope === "user";
|
|
1356
|
-
try {
|
|
1357
|
-
const sourceRepo = first.source.replace("github:", "");
|
|
1358
|
-
let refToInstall = first.ref;
|
|
1359
|
-
if (nameOrRef && !allEntries.find((e) => e.name === nameOrRef)) {
|
|
1360
|
-
try {
|
|
1361
|
-
const parsed = parseSkillRef(nameOrRef);
|
|
1362
|
-
refToInstall = parsed.ref;
|
|
1363
|
-
} catch {
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
this.opts.log?.(`Updating ${first.source}@${refToInstall}...`);
|
|
1367
|
-
const results = await this.install(`${sourceRepo}@${refToInstall}`, { global: isGlobal });
|
|
1368
|
-
for (const r of results) {
|
|
1369
|
-
result.updated.push({
|
|
1370
|
-
name: r.name,
|
|
1371
|
-
oldRef: first.ref,
|
|
1372
|
-
newRef: refToInstall
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
} catch (err) {
|
|
1376
|
-
const msg = toErrorMessage(err);
|
|
1377
|
-
for (const entry of entries) {
|
|
1378
|
-
result.errors.push({ name: entry.name, error: msg });
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
return result;
|
|
1383
|
-
}
|
|
1384
|
-
/**
|
|
1385
|
-
* Uninstall a skill by name.
|
|
1386
|
-
*/
|
|
1387
|
-
async uninstall(name, opts) {
|
|
1388
|
-
const scope = opts?.global ? "user" : "project";
|
|
1389
|
-
const entries = await this.manifest.findByName(name);
|
|
1390
|
-
const entry = entries.find((e) => e.scope === scope);
|
|
1391
|
-
if (!entry) {
|
|
1392
|
-
throw new WrongStackError({
|
|
1393
|
-
message: `Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`,
|
|
1394
|
-
code: ERROR_CODES.VALIDATION_ERROR,
|
|
1395
|
-
subsystem: "general",
|
|
1396
|
-
context: { skillName: name, scope }
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
await this.removeSkillFiles(name, scope);
|
|
1400
|
-
await this.manifest.removeEntry(name, scope);
|
|
1401
|
-
this.invalidateLoaderCache();
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* List all installed skills from the manifest.
|
|
1405
|
-
*/
|
|
1406
|
-
async listInstalled() {
|
|
1407
|
-
return this.manifest.listAll();
|
|
1408
|
-
}
|
|
1409
|
-
/**
|
|
1410
|
-
* Search across all configured registry adapters. Results from each adapter
|
|
1411
|
-
* are merged (deduplicated by `installRef`, adapters earlier in the list win
|
|
1412
|
-
* conflicts). Adapters that don't support search (e.g. github-direct)
|
|
1413
|
-
* contribute nothing.
|
|
1414
|
-
*/
|
|
1415
|
-
async search(query, opts) {
|
|
1416
|
-
const settled = await Promise.allSettled(this.adapters.map((a) => a.search(query, opts)));
|
|
1417
|
-
const perAdapter = [];
|
|
1418
|
-
const errors = [];
|
|
1419
|
-
settled.forEach((s, i) => {
|
|
1420
|
-
if (s.status === "fulfilled") perAdapter.push(s.value);
|
|
1421
|
-
else {
|
|
1422
|
-
const id = this.adapters[i]?.id ?? "?";
|
|
1423
|
-
errors.push(`${id}: ${toErrorMessage(s.reason)}`);
|
|
1424
|
-
}
|
|
1425
|
-
});
|
|
1426
|
-
if (errors.length > 0) {
|
|
1427
|
-
this.opts.log?.(`Some registries failed during search: ${errors.join("; ")}`);
|
|
1428
|
-
}
|
|
1429
|
-
return dedupeSearchResults(perAdapter);
|
|
1430
|
-
}
|
|
1431
|
-
// ── Private helpers ──────────────────────────────────────────────
|
|
1432
|
-
/**
|
|
1433
|
-
* Detect skills in an extracted repository.
|
|
1434
|
-
* Returns an array of detected skills with their files.
|
|
1435
|
-
*/
|
|
1436
|
-
async detectSkills(baseDir) {
|
|
1437
|
-
const results = [];
|
|
1438
|
-
const rootSkillMd = path4.join(baseDir, "SKILL.md");
|
|
1439
|
-
try {
|
|
1440
|
-
await fs4.access(rootSkillMd);
|
|
1441
|
-
const content = await fs4.readFile(rootSkillMd, "utf8");
|
|
1442
|
-
const fm = parseSkillFrontmatter(content);
|
|
1443
|
-
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
1444
|
-
results.push({
|
|
1445
|
-
name: fm.name,
|
|
1446
|
-
baseDir,
|
|
1447
|
-
files: ["SKILL.md"]
|
|
1448
|
-
});
|
|
1449
|
-
return results;
|
|
1450
|
-
}
|
|
1451
|
-
} catch {
|
|
1452
|
-
}
|
|
1453
|
-
const skillsDir = path4.join(baseDir, "skills");
|
|
1454
|
-
try {
|
|
1455
|
-
const entries = await fs4.readdir(skillsDir, { withFileTypes: true });
|
|
1456
|
-
for (const entry of entries) {
|
|
1457
|
-
if (!entry.isDirectory()) continue;
|
|
1458
|
-
const skillFile = path4.join(skillsDir, entry.name, "SKILL.md");
|
|
1459
|
-
try {
|
|
1460
|
-
const content = await fs4.readFile(skillFile, "utf8");
|
|
1461
|
-
const fm = parseSkillFrontmatter(content);
|
|
1462
|
-
if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
|
|
1463
|
-
const skillDir = path4.join(skillsDir, entry.name);
|
|
1464
|
-
const files = await collectFiles(skillDir, skillDir);
|
|
1465
|
-
results.push({
|
|
1466
|
-
name: fm.name,
|
|
1467
|
-
baseDir: skillDir,
|
|
1468
|
-
files
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
} catch {
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
} catch {
|
|
1475
|
-
}
|
|
1476
|
-
return results;
|
|
1477
|
-
}
|
|
1478
|
-
/**
|
|
1479
|
-
* Remove all files for an installed skill.
|
|
1480
|
-
*/
|
|
1481
|
-
/**
|
|
1482
|
-
* Delete an installed skill's directory.
|
|
1483
|
-
*
|
|
1484
|
-
* The containment check is not ceremonial: this is a `recursive: true,
|
|
1485
|
-
* force: true` delete whose target is built from a skill NAME taken from
|
|
1486
|
-
* manifest frontmatter. `path.join` normalises `..`, so a crafted name
|
|
1487
|
-
* escapes `targetDir` and takes the recursive delete with it. This file
|
|
1488
|
-
* already had `isInside` and applied it on the copy path but not here.
|
|
1489
|
-
*
|
|
1490
|
-
* The strict-subdirectory requirement matters separately: `isInside` allows
|
|
1491
|
-
* equality by design, and an empty or `.` name resolves `skillDir` to
|
|
1492
|
-
* `targetDir` itself — deleting every installed skill (WS-097).
|
|
1493
|
-
*/
|
|
1494
|
-
async removeSkillFiles(name, scope) {
|
|
1495
|
-
const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
|
|
1496
|
-
const root = path4.resolve(targetDir);
|
|
1497
|
-
const skillDir = path4.resolve(path4.join(targetDir, name));
|
|
1498
|
-
if (skillDir === root || !isInside(skillDir, root)) {
|
|
1499
|
-
throw new FsError({
|
|
1500
|
-
message: `Refusing to delete skill files outside the skills directory: ${name}`,
|
|
1501
|
-
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1502
|
-
path: skillDir,
|
|
1503
|
-
context: { reason: "path_traversal", skillName: name, scope }
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1506
|
-
await fs4.rm(skillDir, { recursive: true, force: true });
|
|
1507
|
-
}
|
|
1508
|
-
/**
|
|
1509
|
-
* Invalidate the skill loader's cache so newly installed skills appear.
|
|
1510
|
-
*/
|
|
1511
|
-
invalidateLoaderCache() {
|
|
1512
|
-
const loader = this.opts.skillLoader;
|
|
1513
|
-
if (loader && typeof loader.invalidateCache === "function") {
|
|
1514
|
-
loader.invalidateCache();
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Resolve an install ref, dispatching registry-prefixed refs to the matching
|
|
1519
|
-
* adapter. Returns the concrete `user/repo[@ref]` install ref plus provenance
|
|
1520
|
-
* (which adapter resolved it, if any).
|
|
1521
|
-
*/
|
|
1522
|
-
resolveRef(refInput) {
|
|
1523
|
-
const trimmed = refInput.trim();
|
|
1524
|
-
const colonIdx = trimmed.indexOf(":");
|
|
1525
|
-
if (colonIdx > 0) {
|
|
1526
|
-
const maybeAdapterId = trimmed.slice(0, colonIdx);
|
|
1527
|
-
const adapter = this.adapters.find((a) => a.id === maybeAdapterId);
|
|
1528
|
-
if (adapter) {
|
|
1529
|
-
const registryId = trimmed.slice(colonIdx + 1);
|
|
1530
|
-
const installRef = adapter.resolveInstallRef(registryId);
|
|
1531
|
-
return { installRef, fromRegistry: true, adapterId: adapter.id, registryId };
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
return { installRef: trimmed, fromRegistry: false, adapterId: "", registryId: "" };
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
async function entryIsDirectory(dir, entry) {
|
|
1538
|
-
if (entry.isDirectory()) return true;
|
|
1539
|
-
if (entry.isSymbolicLink()) {
|
|
1540
|
-
try {
|
|
1541
|
-
return (await fs4.stat(path4.join(dir, entry.name))).isDirectory();
|
|
1542
|
-
} catch {
|
|
1543
|
-
return false;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
return false;
|
|
1547
|
-
}
|
|
1548
|
-
async function collectFiles(dir, baseDir) {
|
|
1549
|
-
const results = [];
|
|
1550
|
-
const entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1551
|
-
for (const entry of entries) {
|
|
1552
|
-
const fullPath = path4.join(dir, entry.name);
|
|
1553
|
-
const relPath = path4.relative(baseDir, fullPath);
|
|
1554
|
-
if (entry.isDirectory()) {
|
|
1555
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1556
|
-
results.push(...await collectFiles(fullPath, baseDir));
|
|
1557
|
-
} else {
|
|
1558
|
-
results.push(relPath);
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
return results;
|
|
1562
|
-
}
|
|
1563
|
-
function dedupeSearchResults(perAdapter) {
|
|
1564
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1565
|
-
const out = [];
|
|
1566
|
-
for (const block of perAdapter) {
|
|
1567
|
-
const kept = [];
|
|
1568
|
-
for (const r of block.results) {
|
|
1569
|
-
const key = r.installRef;
|
|
1570
|
-
if (seen.has(key)) continue;
|
|
1571
|
-
seen.add(key);
|
|
1572
|
-
kept.push(r);
|
|
1573
|
-
}
|
|
1574
|
-
if (kept.length > 0 || block.results.length > 0) {
|
|
1575
|
-
out.push({ ...block, results: kept });
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
return out;
|
|
1579
|
-
}
|
|
3
|
+
DEFAULT_SKILLS_SH_URL,
|
|
4
|
+
SkillInstaller,
|
|
5
|
+
SkillManifestStore,
|
|
6
|
+
bodyLineAdvisory,
|
|
7
|
+
createSkillsShAdapter,
|
|
8
|
+
downloadGitHubTarball,
|
|
9
|
+
extractSkillFromPrompt,
|
|
10
|
+
generateSkillSkeleton,
|
|
11
|
+
githubDirectAdapter,
|
|
12
|
+
openInEditor,
|
|
13
|
+
parseSkillRef,
|
|
14
|
+
validateSkillNameAvailable,
|
|
15
|
+
writeSkeletonSkill
|
|
16
|
+
} from "../chunk-SN2DQRL6.js";
|
|
17
|
+
import {
|
|
18
|
+
FOREIGN_SKILL_TOOLS,
|
|
19
|
+
resolveForeignToolIds,
|
|
20
|
+
resolveForeignToolIdsWithWarnings,
|
|
21
|
+
securityScoreToTier
|
|
22
|
+
} from "../chunk-JVKB2TBL.js";
|
|
23
|
+
import "../chunk-LHOA5DWA.js";
|
|
24
|
+
import {
|
|
25
|
+
SKILL_LIMITS
|
|
26
|
+
} from "../chunk-XEBHSJLT.js";
|
|
27
|
+
import "../chunk-7ZHZE5EO.js";
|
|
28
|
+
import {
|
|
29
|
+
isValidSkillNameFormat,
|
|
30
|
+
parseSkillFrontmatter,
|
|
31
|
+
stripFrontmatter,
|
|
32
|
+
validateSkillName
|
|
33
|
+
} from "../chunk-WEBMSQP2.js";
|
|
34
|
+
import "../chunk-WOEZV3Q4.js";
|
|
35
|
+
import "../chunk-TBRL2RCC.js";
|
|
36
|
+
import "../chunk-PFZO3C6H.js";
|
|
1580
37
|
export {
|
|
1581
38
|
DEFAULT_SKILLS_SH_URL,
|
|
1582
39
|
FOREIGN_SKILL_TOOLS,
|