@wrongstack/core 0.277.1 → 0.280.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/{agent-bridge-BFJ2ODzI.d.ts → agent-bridge-DXC6QDJ4.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-BimKihiC.d.ts → agent-subagent-runner-PoqNKiR4.d.ts} +563 -471
  3. package/dist/{compactor-D3BGw26y.d.ts → compactor-U3agvUIG.d.ts} +1 -1
  4. package/dist/{config-DAOjriz9.d.ts → config-Cr3312zc.d.ts} +102 -4
  5. package/dist/coordination/index.d.ts +1087 -998
  6. package/dist/coordination/index.js +12235 -12052
  7. package/dist/coordination/index.js.map +1 -1
  8. package/dist/defaults/index.d.ts +31 -30
  9. package/dist/defaults/index.js +403 -189
  10. package/dist/defaults/index.js.map +1 -1
  11. package/dist/{brain-CCfuEOdp.d.ts → events-Bs2fmldo.d.ts} +117 -112
  12. package/dist/execution/index.d.ts +27 -19
  13. package/dist/execution/index.js +216 -63
  14. package/dist/execution/index.js.map +1 -1
  15. package/dist/execution/prompt-enhancer.d.ts +1 -1
  16. package/dist/execution/prompt-enhancer.js.map +1 -1
  17. package/dist/extension/index.d.ts +8 -7
  18. package/dist/{global-mailbox-Dr4cTKqL.d.ts → global-mailbox-Ct7IorLJ.d.ts} +84 -6
  19. package/dist/{goal-store-C1uH4srH.d.ts → goal-store-C4F6DjC0.d.ts} +1 -1
  20. package/dist/hq/index.d.ts +504 -7
  21. package/dist/hq/index.js +1069 -20
  22. package/dist/hq/index.js.map +1 -1
  23. package/dist/{index-DJXj-dcr.d.ts → index-kidebiDh.d.ts} +8 -5
  24. package/dist/{index-cMEmzCVN.d.ts → index-nP09-oP2.d.ts} +2 -2
  25. package/dist/index.d.ts +153 -76
  26. package/dist/index.js +5791 -3163
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +7 -6
  29. package/dist/kernel/index.d.ts +14 -13
  30. package/dist/kernel/index.js +31 -15
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/{mailbox-types-DTl7bRH3.d.ts → mailbox-types-BGZWrYTJ.d.ts} +38 -0
  33. package/dist/{mcp-servers-CFb60-pH.d.ts → mcp-servers-D910X5_r.d.ts} +3 -3
  34. package/dist/models/index.d.ts +5 -5
  35. package/dist/models/index.js.map +1 -1
  36. package/dist/{models-registry-5Ufn7f2m.d.ts → models-registry-CLkoOcHk.d.ts} +1 -1
  37. package/dist/{multi-agent-coordinator-CcrcncvG.d.ts → multi-agent-coordinator-CieyUoEL.d.ts} +1 -1
  38. package/dist/{null-fleet-bus-C9KsYyrI.d.ts → null-fleet-bus-DkdmZJ_W.d.ts} +464 -464
  39. package/dist/observability/index.d.ts +3 -2
  40. package/dist/{path-resolver-CEeX9I7O.d.ts → path-resolver-XfZ9eLxG.d.ts} +3 -3
  41. package/dist/{permission-DbsGOA1C.d.ts → permission-Dx6dIqS2.d.ts} +2 -7
  42. package/dist/{permission-policy-BpEea3r7.d.ts → permission-policy-C8vJcnX5.d.ts} +2 -2
  43. package/dist/{pipeline-CEjBjzVA.d.ts → pipeline-BwAP21_4.d.ts} +9 -4
  44. package/dist/{provider-model-resolve-BpfXp3Jj.d.ts → provider-model-resolve-CwQNZWt_.d.ts} +3 -3
  45. package/dist/{provider-runner-CnOSr5BN.d.ts → provider-runner-CYHFImzV.d.ts} +3 -3
  46. package/dist/{retry-policy-Git9WF6d.d.ts → retry-policy-D4feSLk3.d.ts} +1 -1
  47. package/dist/sdd/index.d.ts +11 -10
  48. package/dist/sdd/index.js +2 -2
  49. package/dist/sdd/index.js.map +1 -1
  50. package/dist/secret-scrubber-3MHDDAtm.d.ts +6 -0
  51. package/dist/{secret-vault-DDSMHqIm.d.ts → secret-vault-CImt2XrR.d.ts} +1 -1
  52. package/dist/security/index.d.ts +6 -5
  53. package/dist/security/index.js.map +1 -1
  54. package/dist/{selector-Cq72C0Oy.d.ts → selector-Dy-MzKp1.d.ts} +1 -1
  55. package/dist/{session-event-bridge-DG94B3Bk.d.ts → session-event-bridge-CqdiGnfU.d.ts} +1 -1
  56. package/dist/{session-reader-BzT-iMQT.d.ts → session-reader-Hk0WbNm9.d.ts} +1 -1
  57. package/dist/{skill-DGIXCtdv.d.ts → skill-DHniprNl.d.ts} +15 -1
  58. package/dist/skills/index.d.ts +472 -26
  59. package/dist/skills/index.js +872 -129
  60. package/dist/skills/index.js.map +1 -1
  61. package/dist/storage/index.d.ts +27 -14
  62. package/dist/storage/index.js +264 -85
  63. package/dist/storage/index.js.map +1 -1
  64. package/dist/{strategy-compactor-Bt_ZH6R0.d.ts → strategy-compactor-CQwhbErd.d.ts} +32 -17
  65. package/dist/{todos-checkpoint-CH1pcua9.d.ts → todos-checkpoint-Bk2uP7Ex.d.ts} +6 -6
  66. package/dist/{context-DPlA6kid.d.ts → tool-BkOgs_KL.d.ts} +306 -286
  67. package/dist/{tool-executor-SVFq7IOR.d.ts → tool-executor-SiE1wlZo.d.ts} +9 -9
  68. package/dist/tools/index.d.ts +2 -2
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/types/index.d.ts +22 -21
  71. package/dist/types/index.js +7 -9
  72. package/dist/types/index.js.map +1 -1
  73. package/dist/utils/index.d.ts +30 -4
  74. package/dist/utils/index.js +50 -1
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/{worktree-manager-C4YIf1Fa.d.ts → worktree-manager-BjOFF6bt.d.ts} +1 -1
  77. package/dist/{wstack-paths-_NrRovdr.d.ts → wstack-paths-CMl_cYgq.d.ts} +8 -0
  78. package/package.json +1 -1
  79. package/skills/mailbox-bridge/SKILL.md +1 -0
  80. package/skills/plugin-author/SKILL.md +350 -0
  81. package/skills/sdd/SKILL.md +134 -134
  82. package/skills/skill-creator/SKILL.md +45 -7
  83. package/skills/wrongstack-mailbox/SKILL.md +40 -21
@@ -1,37 +1,168 @@
1
- import * as fs4 from 'fs/promises';
2
- import * as path4 from 'path';
1
+ import * as fs5 from 'fs/promises';
3
2
  import * as os from 'os';
4
- import { createGunzip } from 'zlib';
3
+ import * as path5 from 'path';
5
4
  import { Readable } from 'stream';
6
5
  import { pipeline } from 'stream/promises';
6
+ import { createGunzip } from 'zlib';
7
7
  import { randomBytes } from 'crypto';
8
+ import { spawn } from 'child_process';
8
9
 
9
- // src/utils/expect-defined.ts
10
- function expectDefined(value, label) {
11
- if (value === null || value === void 0) {
12
- const err = new Error("Expected value to be defined");
13
- err.name = "ExpectDefinedError";
14
- throw err;
10
+ // src/skills/foreign-sources.ts
11
+ var FOREIGN_SKILL_TOOLS = [
12
+ { id: "agents", subdir: "skills" },
13
+ // shared store (asm / agentskills.io ecosystem)
14
+ { id: "codex", subdir: "skills" },
15
+ // OpenAI Codex CLI
16
+ { id: "gemini", subdir: "skills" },
17
+ // Gemini CLI
18
+ { id: "cursor", subdir: "skills" },
19
+ // Cursor (standard subdir, aligned with skills.sh ecosystem)
20
+ { id: "qwen", subdir: "skills" },
21
+ // Qwen Code
22
+ { id: "trae", subdir: "skills" },
23
+ // Trae
24
+ { id: "windsurf", subdir: "skills" }
25
+ // Windsurf
26
+ ];
27
+ var KNOWN_FOREIGN_IDS = new Set(FOREIGN_SKILL_TOOLS.map((t) => t.id));
28
+ function resolveForeignToolIds(opt) {
29
+ return resolveForeignToolIdsWithWarnings(opt).ids;
30
+ }
31
+ function resolveForeignToolIdsWithWarnings(opt) {
32
+ if (opt === false) return { ids: [], unknownIds: [] };
33
+ if (Array.isArray(opt)) {
34
+ const ids = [];
35
+ const unknownIds = [];
36
+ for (const id of opt) {
37
+ if (KNOWN_FOREIGN_IDS.has(id)) ids.push(id);
38
+ else unknownIds.push(id);
39
+ }
40
+ return { ids, unknownIds };
15
41
  }
16
- return value;
42
+ return { ids: FOREIGN_SKILL_TOOLS.map((t) => t.id), unknownIds: [] };
43
+ }
44
+ function securityScoreToTier(score) {
45
+ if (score == null || Number.isNaN(score)) return "low";
46
+ if (score < 30) return "low";
47
+ if (score < 70) return "medium";
48
+ return "high";
17
49
  }
18
50
 
19
- // src/utils/error.ts
20
- function toErrorMessage(err) {
21
- return err instanceof Error ? err.message : String(err);
51
+ // src/skills/frontmatter.ts
52
+ var SCALAR_KEYS = /* @__PURE__ */ new Set(["name", "description", "version", "license", "compatibility"]);
53
+ function parseSkillFrontmatter(raw) {
54
+ const text = normalizeLineEndings(raw);
55
+ if (!text.startsWith("---")) return {};
56
+ const end = text.indexOf("\n---", 4);
57
+ if (end === -1) return {};
58
+ return parseFrontmatterBlock(text.slice(4, end));
59
+ }
60
+ function stripFrontmatter(raw) {
61
+ const text = normalizeLineEndings(raw);
62
+ if (!text.startsWith("---")) return text;
63
+ const end = text.indexOf("\n---", 4);
64
+ if (end === -1) return text;
65
+ let body = text.slice(end + 4);
66
+ if (body.startsWith("\n")) body = body.slice(1);
67
+ return body;
68
+ }
69
+ function normalizeLineEndings(s) {
70
+ return s.replace(/\r\n?/g, "\n");
71
+ }
72
+ function parseFrontmatterBlock(block) {
73
+ const out = {};
74
+ const lines = block.split("\n");
75
+ let i = 0;
76
+ while (i < lines.length) {
77
+ const line = lines[i] ?? "";
78
+ const m = /^([a-zA-Z][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
79
+ if (!m) {
80
+ i++;
81
+ continue;
82
+ }
83
+ const key = m[1] ?? "";
84
+ const rest = (m[2] ?? "").trim();
85
+ if (key === "metadata") {
86
+ const map = {};
87
+ i++;
88
+ while (i < lines.length) {
89
+ const sub = lines[i] ?? "";
90
+ const sm = /^\s+([a-zA-Z0-9_.-]+):\s*(.*)$/.exec(sub);
91
+ if (!sm) break;
92
+ map[sm[1] ?? ""] = unquote((sm[2] ?? "").trim());
93
+ i++;
94
+ }
95
+ if (Object.keys(map).length > 0) out.metadata = map;
96
+ continue;
97
+ }
98
+ if (rest === "|" || rest === ">") {
99
+ const collected = [];
100
+ i++;
101
+ while (i < lines.length) {
102
+ const sub = lines[i] ?? "";
103
+ if (sub === "" || sub.startsWith(" ") || sub.startsWith(" ")) {
104
+ collected.push(sub.replace(/^\s+/, ""));
105
+ i++;
106
+ } else break;
107
+ }
108
+ out[normalizeKey(key)] = collected.join("\n").trim();
109
+ continue;
110
+ }
111
+ if (key === "allowed-tools" || key === "allowedTools") {
112
+ out.allowedTools = rest.split(/[\s,]+/).filter(Boolean);
113
+ i++;
114
+ continue;
115
+ }
116
+ if (SCALAR_KEYS.has(key)) {
117
+ out[key] = unquote(rest);
118
+ i++;
119
+ continue;
120
+ }
121
+ i++;
122
+ }
123
+ return out;
124
+ }
125
+ function normalizeKey(key) {
126
+ return key === "allowed-tools" ? "allowedTools" : key;
127
+ }
128
+ function unquote(s) {
129
+ if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
130
+ return s.slice(1, -1);
131
+ }
132
+ return s;
133
+ }
134
+ function isValidSkillNameFormat(name) {
135
+ return name.length >= 1 && name.length <= 64 && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
136
+ }
137
+ function validateSkillName(name, parentDirName) {
138
+ const errors = [];
139
+ if (!name || name.trim().length === 0) {
140
+ errors.push("name is empty");
141
+ return errors;
142
+ }
143
+ if (name.length > 64) errors.push(`name is ${name.length} characters (max 64)`);
144
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
145
+ errors.push(
146
+ "name must be lowercase letters, digits, and single hyphens only (no leading/trailing/consecutive hyphens)"
147
+ );
148
+ }
149
+ if (parentDirName !== void 0 && name !== parentDirName) {
150
+ errors.push(`name "${name}" must match its parent directory "${parentDirName}"`);
151
+ }
152
+ return errors;
22
153
  }
23
154
  async function atomicWrite(targetPath, content, opts = {}) {
24
- const dir = path4.dirname(targetPath);
25
- await fs4.mkdir(dir, { recursive: true });
26
- const tmp = path4.join(dir, `.${path4.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
155
+ const dir = path5.dirname(targetPath);
156
+ await fs5.mkdir(dir, { recursive: true });
157
+ const tmp = path5.join(dir, `.${path5.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
27
158
  try {
28
159
  if (typeof content === "string") {
29
- await fs4.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
160
+ await fs5.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
30
161
  } else {
31
- await fs4.writeFile(tmp, content, { flag: "wx" });
162
+ await fs5.writeFile(tmp, content, { flag: "wx" });
32
163
  }
33
164
  try {
34
- const fh = await fs4.open(tmp, "r+");
165
+ const fh = await fs5.open(tmp, "r+");
35
166
  try {
36
167
  await fh.sync();
37
168
  } finally {
@@ -41,24 +172,24 @@ async function atomicWrite(targetPath, content, opts = {}) {
41
172
  }
42
173
  let mode;
43
174
  try {
44
- const stat3 = await fs4.stat(targetPath);
175
+ const stat3 = await fs5.stat(targetPath);
45
176
  mode = stat3.mode & 511;
46
177
  } catch {
47
178
  mode = opts.mode;
48
179
  }
49
180
  if (mode !== void 0) {
50
- await fs4.chmod(tmp, mode);
181
+ await fs5.chmod(tmp, mode);
51
182
  }
52
183
  await renameWithRetry(tmp, targetPath);
53
184
  if (mode !== void 0 && process.platform === "win32") {
54
185
  try {
55
- await fs4.chmod(targetPath, mode);
186
+ await fs5.chmod(targetPath, mode);
56
187
  } catch {
57
188
  }
58
189
  }
59
190
  } catch (err) {
60
191
  try {
61
- await fs4.unlink(tmp);
192
+ await fs5.unlink(tmp);
62
193
  } catch {
63
194
  }
64
195
  throw err;
@@ -67,14 +198,14 @@ async function atomicWrite(targetPath, content, opts = {}) {
67
198
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
68
199
  async function renameWithRetry(from, to) {
69
200
  if (process.platform !== "win32") {
70
- await fs4.rename(from, to);
201
+ await fs5.rename(from, to);
71
202
  return;
72
203
  }
73
204
  const delays = [10, 25, 60, 120, 250];
74
205
  let lastErr;
75
206
  for (let i = 0; i <= delays.length; i++) {
76
207
  try {
77
- await fs4.rename(from, to);
208
+ await fs5.rename(from, to);
78
209
  return;
79
210
  } catch (err) {
80
211
  lastErr = err;
@@ -88,6 +219,21 @@ async function renameWithRetry(from, to) {
88
219
  throw lastErr;
89
220
  }
90
221
 
222
+ // src/utils/error.ts
223
+ function toErrorMessage(err) {
224
+ return err instanceof Error ? err.message : String(err);
225
+ }
226
+
227
+ // src/utils/expect-defined.ts
228
+ function expectDefined(value, label) {
229
+ if (value === null || value === void 0) {
230
+ const err = new Error("Expected value to be defined");
231
+ err.name = "ExpectDefinedError";
232
+ throw err;
233
+ }
234
+ return value;
235
+ }
236
+
91
237
  // src/types/errors.ts
92
238
  var ERROR_CODES = {
93
239
  // Provider
@@ -189,6 +335,118 @@ var FsError = class extends WrongStackError {
189
335
  this.path = opts.path;
190
336
  }
191
337
  };
338
+ var FetchError = class extends WrongStackError {
339
+ status;
340
+ constructor(opts) {
341
+ super({
342
+ message: opts.message,
343
+ code: ERROR_CODES.VALIDATION_ERROR,
344
+ subsystem: "general",
345
+ severity: "error",
346
+ recoverable: opts.status === 429 || opts.status >= 500,
347
+ context: { status: opts.status, ...opts.context },
348
+ cause: opts.cause
349
+ });
350
+ this.name = "FetchError";
351
+ this.status = opts.status;
352
+ }
353
+ };
354
+ var ParseError = class extends WrongStackError {
355
+ source;
356
+ constructor(opts) {
357
+ super({
358
+ message: opts.message,
359
+ code: ERROR_CODES.PARSE_FAILED,
360
+ subsystem: "general",
361
+ severity: "error",
362
+ recoverable: false,
363
+ context: { source: opts.source, ...opts.context },
364
+ cause: opts.cause
365
+ });
366
+ this.name = "ParseError";
367
+ this.source = opts.source;
368
+ }
369
+ };
370
+
371
+ // src/skills/limits.ts
372
+ var SKILL_LIMITS = {
373
+ /**
374
+ * Per-skill body cap when injecting a full skill body into the system prompt
375
+ * (eager mode), and when returning a skill body from the `skill` tool.
376
+ * ~4k tokens. Oversized bodies are truncated at a paragraph boundary.
377
+ *
378
+ * Consumers: `system-prompt-builder.capSkillBody`, `skill` tool body return.
379
+ */
380
+ MAX_SKILL_BODY_CHARS: 16e3,
381
+ /**
382
+ * Per-resource cap when the `skill` tool loads a bundled file
383
+ * (`scripts/`, `references/`, `assets/`). ~8k tokens.
384
+ *
385
+ * Consumer: `skill` tool `loadResource`.
386
+ */
387
+ MAX_RESOURCE_CHARS: 32e3,
388
+ /**
389
+ * Maximum number of bundled resource paths the `skill` tool lists in one
390
+ * response. Caps a pathological skill (huge `assets/` tree) from blowing up
391
+ * the tool result.
392
+ *
393
+ * Consumer: `skill` tool resource listing.
394
+ */
395
+ MAX_LISTED_RESOURCES: 100,
396
+ /**
397
+ * Max size of a single installed skill file (SKILL.md or a bundled resource).
398
+ * Guards against a malicious registry skill shipping a multi-MB blob that
399
+ * then flows into the prompt. 100KB.
400
+ *
401
+ * Consumer: `SkillInstaller.install` / `importFromDir`.
402
+ */
403
+ MAX_SKILL_FILE_SIZE: 100 * 1024,
404
+ /**
405
+ * Max size of a GitHub tarball downloaded by the skill installer. Guards
406
+ * against a repo accidentally (or maliciously) shipping a giant tarball that
407
+ * would be extracted into a temp dir. 50MB.
408
+ *
409
+ * Consumer: `downloadGitHubTarball`.
410
+ */
411
+ MAX_TARBALL_SIZE: 50 * 1024 * 1024,
412
+ /**
413
+ * Default total char budget for skill bodies injected in eager mode when
414
+ * `config.skills.eagerMaxChars` is unset. ~6k tokens. Skills are injected
415
+ * highest-priority first; once the budget is exhausted the remaining skills
416
+ * are listed as a manifest the agent loads via the `skill` tool. Set very
417
+ * high to effectively disable budgeting.
418
+ *
419
+ * Consumer: `DefaultSystemPromptBuilder.buildMemoryAndSkills` (default for
420
+ * `skillEagerMaxChars`).
421
+ */
422
+ EAGER_DEFAULT_MAX_CHARS: 24e3,
423
+ /**
424
+ * Auto-compact body limits (the token-saving fallback used when a skill has
425
+ * no hand-crafted `SKILL.save.md`). The Overview and Rules sections are
426
+ * extracted and trimmed to these char budgets, then the total is capped.
427
+ *
428
+ * Consumer: `DefaultSkillLoader.compactSkillBody`.
429
+ */
430
+ COMPACT_OVERVIEW_MAX: 200,
431
+ COMPACT_RULES_MAX: 350,
432
+ COMPACT_TOTAL_MAX: 450,
433
+ /**
434
+ * Max length of a skill name (agentskills.io spec). Enforced by the
435
+ * frontmatter validator's format regex plus this length bound.
436
+ *
437
+ * Consumer: `isValidSkillNameFormat`.
438
+ */
439
+ SKILL_NAME_MAX_LEN: 64,
440
+ /**
441
+ * Soft line limit for a SKILL.md body, per the bundled `skill-creator` rule.
442
+ * Not enforced in code (a skill can exceed it) — surfaced by `/skill-gen
443
+ * validate` and the `skill-creator` skill as guidance to move deep material
444
+ * into `references/`.
445
+ *
446
+ * Consumer: `skill-creator` skill, `/skill-gen validate` advisory.
447
+ */
448
+ SKILL_BODY_LINE_LIMIT: 500
449
+ };
192
450
 
193
451
  // src/skills/github-fetcher.ts
194
452
  function parseSkillRef(input) {
@@ -214,32 +472,58 @@ function parseSkillRef(input) {
214
472
  }
215
473
  return { owner: expectDefined(parts[0]), repo: expectDefined(parts[1]), ref };
216
474
  }
217
- var MAX_TARBALL_SIZE = 50 * 1024 * 1024;
475
+ function resolveGitHubToken(env = process.env) {
476
+ const raw = env["WRONGSTACK_GITHUB_TOKEN"] ?? env["GITHUB_TOKEN"] ?? env["GH_TOKEN"];
477
+ if (!raw) return void 0;
478
+ const trimmed = raw.trim();
479
+ return trimmed.length > 0 ? trimmed : void 0;
480
+ }
218
481
  async function downloadGitHubTarball(parsed) {
219
482
  const url = `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/tarball/${parsed.ref}`;
220
- const response = await fetch(url, {
221
- signal: AbortSignal.timeout(3e4),
222
- headers: {
223
- Accept: "application/vnd.github+json",
224
- "User-Agent": "wrongstack-skill-installer"
225
- },
226
- redirect: "follow"
227
- });
483
+ const token = resolveGitHubToken();
484
+ const headers = {
485
+ Accept: "application/vnd.github+json",
486
+ "User-Agent": "wrongstack-skill-installer"
487
+ };
488
+ if (token) headers["Authorization"] = `Bearer ${token}`;
489
+ let response;
490
+ try {
491
+ response = await fetch(url, {
492
+ signal: AbortSignal.timeout(3e4),
493
+ headers,
494
+ redirect: "follow"
495
+ });
496
+ } catch (err) {
497
+ throw new FetchError({
498
+ message: `Network error fetching ${parsed.owner}/${parsed.repo}: ${err instanceof Error ? err.message : String(err)}`,
499
+ status: 0,
500
+ context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, op: "tarball" },
501
+ cause: err
502
+ });
503
+ }
228
504
  if (!response.ok) {
229
505
  if (response.status === 404) {
230
506
  throw new WrongStackError({
231
- message: `Repository not found: ${parsed.owner}/${parsed.repo}` + (parsed.ref !== "main" ? ` (ref: ${parsed.ref})` : ""),
507
+ 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)."),
232
508
  code: ERROR_CODES.UNKNOWN,
233
509
  subsystem: "general",
234
510
  context: { owner: parsed.owner, repo: parsed.repo, ref: parsed.ref, status: 404 }
235
511
  });
236
512
  }
237
513
  if (response.status === 403) {
514
+ const remaining = response.headers.get("x-ratelimit-remaining");
515
+ const isRateLimited = remaining === "0";
238
516
  throw new WrongStackError({
239
- message: `Access denied: ${parsed.owner}/${parsed.repo}. The repository may be private or rate-limited.`,
517
+ 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.`,
240
518
  code: ERROR_CODES.UNKNOWN,
241
519
  subsystem: "general",
242
- context: { owner: parsed.owner, repo: parsed.repo, status: 403 }
520
+ context: {
521
+ owner: parsed.owner,
522
+ repo: parsed.repo,
523
+ status: 403,
524
+ rateLimited: isRateLimited,
525
+ hasToken: Boolean(token)
526
+ }
243
527
  });
244
528
  }
245
529
  throw new WrongStackError({
@@ -250,20 +534,20 @@ async function downloadGitHubTarball(parsed) {
250
534
  });
251
535
  }
252
536
  const contentLength = response.headers.get("content-length");
253
- if (contentLength && Number.parseInt(contentLength, 10) > MAX_TARBALL_SIZE) {
537
+ if (contentLength && Number.parseInt(contentLength, 10) > SKILL_LIMITS.MAX_TARBALL_SIZE) {
254
538
  throw new WrongStackError({
255
- message: `Tarball too large (${(Number.parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Max: ${MAX_TARBALL_SIZE / 1024 / 1024}MB`,
539
+ message: `Tarball too large (${(Number.parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB). Max: ${SKILL_LIMITS.MAX_TARBALL_SIZE / 1024 / 1024}MB`,
256
540
  code: ERROR_CODES.UNKNOWN,
257
541
  subsystem: "general",
258
542
  context: {
259
543
  owner: parsed.owner,
260
544
  repo: parsed.repo,
261
545
  contentLengthBytes: Number.parseInt(contentLength, 10),
262
- maxBytes: MAX_TARBALL_SIZE
546
+ maxBytes: SKILL_LIMITS.MAX_TARBALL_SIZE
263
547
  }
264
548
  });
265
549
  }
266
- const tempDir = await fs4.mkdtemp(path4.join(os.tmpdir(), "wskill-"));
550
+ const tempDir = await fs5.mkdtemp(path5.join(os.tmpdir(), "wskill-"));
267
551
  try {
268
552
  if (!response.body) {
269
553
  throw new WrongStackError({
@@ -285,7 +569,7 @@ async function downloadGitHubTarball(parsed) {
285
569
  await extractTar(tarBuf, tempDir);
286
570
  return { tempDir };
287
571
  } catch (err) {
288
- await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
572
+ await fs5.rm(tempDir, { recursive: true, force: true }).catch(() => {
289
573
  });
290
574
  throw err;
291
575
  }
@@ -302,25 +586,25 @@ async function extractTar(buf, destDir) {
302
586
  const fullPath = prefix ? `${prefix}/${name}` : name;
303
587
  const relPath = stripTopDir(fullPath);
304
588
  if (relPath && relPath !== "." && relPath !== "..") {
305
- const destPath = path4.join(destDir, relPath);
306
- const resolvedDest = path4.resolve(destPath);
307
- const resolvedRoot = path4.resolve(destDir);
308
- if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path4.sep)) {
589
+ const destPath = path5.join(destDir, relPath);
590
+ const resolvedDest = path5.resolve(destPath);
591
+ const resolvedRoot = path5.resolve(destDir);
592
+ if (resolvedDest !== resolvedRoot && !resolvedDest.startsWith(resolvedRoot + path5.sep)) {
309
593
  offset += 512 + Math.ceil(size / 512) * 512;
310
594
  continue;
311
595
  }
312
596
  if (typeflag === 53 || typeflag === 0) {
313
597
  if (relPath.endsWith("/") || typeflag === 53) {
314
- await fs4.mkdir(destPath, { recursive: true });
598
+ await fs5.mkdir(destPath, { recursive: true });
315
599
  }
316
600
  }
317
601
  if ((typeflag === 48 || typeflag === 0 || typeflag === 0) && size > 0) {
318
- const dir = path4.dirname(destPath);
319
- await fs4.mkdir(dir, { recursive: true });
602
+ const dir = path5.dirname(destPath);
603
+ await fs5.mkdir(dir, { recursive: true });
320
604
  const dataStart = offset + 512;
321
605
  const dataEnd = dataStart + size;
322
606
  if (dataEnd > buf.length) break;
323
- await fs4.writeFile(destPath, buf.subarray(dataStart, dataEnd));
607
+ await fs5.writeFile(destPath, buf.subarray(dataStart, dataEnd));
324
608
  }
325
609
  }
326
610
  offset += 512 + Math.ceil(size / 512) * 512;
@@ -347,7 +631,7 @@ var SkillManifestStore = class {
347
631
  async read() {
348
632
  if (this.cache) return this.cache;
349
633
  try {
350
- const raw = await fs4.readFile(this.manifestPath, "utf8");
634
+ const raw = await fs5.readFile(this.manifestPath, "utf8");
351
635
  const data = JSON.parse(raw);
352
636
  if (!Array.isArray(data.skills)) {
353
637
  this.cache = { skills: [] };
@@ -360,25 +644,21 @@ var SkillManifestStore = class {
360
644
  return this.cache;
361
645
  }
362
646
  async write(data) {
363
- const dir = path4.dirname(this.manifestPath);
364
- await fs4.mkdir(dir, { recursive: true });
647
+ const dir = path5.dirname(this.manifestPath);
648
+ await fs5.mkdir(dir, { recursive: true });
365
649
  await atomicWrite(this.manifestPath, JSON.stringify(data, null, 2) + "\n");
366
650
  this.cache = data;
367
651
  }
368
652
  async addEntry(entry) {
369
653
  const data = await this.read();
370
- data.skills = data.skills.filter(
371
- (s) => !(s.name === entry.name && s.scope === entry.scope)
372
- );
654
+ data.skills = data.skills.filter((s) => !(s.name === entry.name && s.scope === entry.scope));
373
655
  data.skills.push(entry);
374
656
  await this.write(data);
375
657
  }
376
658
  async removeEntry(name, scope) {
377
659
  const data = await this.read();
378
660
  const before = data.skills.length;
379
- data.skills = data.skills.filter(
380
- (s) => !(s.name === name && s.scope === scope)
381
- );
661
+ data.skills = data.skills.filter((s) => !(s.name === name && s.scope === scope));
382
662
  if (data.skills.length === before) return false;
383
663
  await this.write(data);
384
664
  return true;
@@ -401,25 +681,363 @@ var SkillManifestStore = class {
401
681
  }
402
682
  };
403
683
 
404
- // src/skills/skill-installer.ts
405
- var MAX_SKILL_FILE_SIZE = 100 * 1024;
684
+ // src/skills/registry/github-direct-adapter.ts
685
+ var githubDirectAdapter = {
686
+ id: "github",
687
+ displayName: "GitHub (direct)",
688
+ async search(_query, _opts = {}) {
689
+ return { adapterId: "github", results: [], hasMore: false };
690
+ },
691
+ resolveInstallRef(registryId) {
692
+ const trimmed = registryId.trim();
693
+ if (!trimmed.includes("/")) {
694
+ return trimmed;
695
+ }
696
+ return trimmed;
697
+ }
698
+ };
699
+
700
+ // src/skills/registry/skills-sh-adapter.ts
701
+ var DEFAULT_SKILLS_SH_URL = "https://skills.sh";
702
+ var SEARCH_TIMEOUT_MS = 15e3;
703
+ var MAX_PAGE_SIZE = 50;
704
+ var defaultFetcher = async (url) => {
705
+ let res;
706
+ try {
707
+ res = await fetch(url, {
708
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS),
709
+ headers: {
710
+ Accept: "application/json",
711
+ "User-Agent": "wrongstack-skill-installer"
712
+ },
713
+ redirect: "follow"
714
+ });
715
+ } catch (err) {
716
+ throw new FetchError({
717
+ message: `Network error querying skill registry: ${err instanceof Error ? err.message : String(err)}`,
718
+ status: 0,
719
+ context: { url, op: "skills.sh.search" },
720
+ cause: err
721
+ });
722
+ }
723
+ if (!res.ok) {
724
+ throw new FetchError({
725
+ message: `Skill registry returned ${res.status} ${res.statusText}`,
726
+ status: res.status,
727
+ context: { url, op: "skills.sh.search" }
728
+ });
729
+ }
730
+ try {
731
+ return await res.json();
732
+ } catch (err) {
733
+ throw new ParseError({
734
+ message: `Skill registry response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
735
+ source: "skills.sh.search",
736
+ context: { url },
737
+ cause: err
738
+ });
739
+ }
740
+ };
741
+ function createSkillsShAdapter(opts = {}) {
742
+ const baseUrl = (opts.baseUrl ?? DEFAULT_SKILLS_SH_URL).replace(/\/+$/, "");
743
+ const fetcher = opts.fetcher ?? defaultFetcher;
744
+ const id = "skills.sh";
745
+ return {
746
+ id,
747
+ displayName: "skills.sh",
748
+ async search(query, sopts = {}) {
749
+ const page = Math.max(1, sopts.page ?? 1);
750
+ const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, sopts.pageSize ?? 20));
751
+ const q = query.trim();
752
+ if (!q) return { adapterId: id, results: [], hasMore: false };
753
+ const url = `${baseUrl}/api/skills?query=${encodeURIComponent(q)}&page=${page}&pageSize=${pageSize}&sortBy=installs`;
754
+ const raw = await fetcher(url);
755
+ const results = parseResults(raw, url);
756
+ return {
757
+ adapterId: id,
758
+ results,
759
+ hasMore: results.length === pageSize
760
+ };
761
+ },
762
+ resolveInstallRef(registryId) {
763
+ const trimmed = registryId.trim();
764
+ if (!trimmed) {
765
+ throw new ParseError({
766
+ message: "Empty skills.sh registry id.",
767
+ source: "skills.sh.resolveInstallRef"
768
+ });
769
+ }
770
+ const atIdx = trimmed.indexOf("@");
771
+ const repoPart = atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
772
+ const segs = repoPart.split("/").filter(Boolean);
773
+ if (segs.length !== 2) {
774
+ throw new ParseError({
775
+ message: `Invalid skills.sh id "${registryId}". Expected "<owner>/<repo>" or "<owner>/<repo>@<ref>".`,
776
+ source: "skills.sh.resolveInstallRef",
777
+ context: { registryId }
778
+ });
779
+ }
780
+ return trimmed;
781
+ }
782
+ };
783
+ }
784
+ function parseResults(raw, url) {
785
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
786
+ throw new ParseError({
787
+ message: "Skill registry response was not a JSON object.",
788
+ source: "skills.sh.search",
789
+ context: { url }
790
+ });
791
+ }
792
+ const results = raw.results;
793
+ if (!Array.isArray(results)) {
794
+ return [];
795
+ }
796
+ const out = [];
797
+ for (let i = 0; i < results.length; i++) {
798
+ const entry = results[i];
799
+ if (!entry || typeof entry !== "object") continue;
800
+ const name = strField(entry, "name") ?? strField(entry, "slug");
801
+ const description = strField(entry, "description") ?? "";
802
+ const owner = strField(entry, "owner") ?? strField(entry, "author");
803
+ const repo = strField(entry, "repo") ?? strField(entry, "repository");
804
+ if (!name || !owner || !repo) continue;
805
+ const ref = strField(entry, "ref") ?? strField(entry, "branch") ?? strField(entry, "tag");
806
+ const installRef = ref ? `${owner}/${repo}@${ref}` : `${owner}/${repo}`;
807
+ out.push({
808
+ id: strField(entry, "id") ?? `${owner}/${repo}`,
809
+ name,
810
+ description,
811
+ author: owner,
812
+ installs: numField(entry, "installs") ?? numField(entry, "installCount"),
813
+ stars: numField(entry, "stars") ?? numField(entry, "starCount"),
814
+ securityScore: numField(entry, "securityScore") ?? numField(entry, "score"),
815
+ updatedAt: strField(entry, "updatedAt") ?? strField(entry, "updated_at"),
816
+ installRef
817
+ });
818
+ }
819
+ return out;
820
+ }
821
+ function strField(rec, key) {
822
+ const v = rec[key];
823
+ return typeof v === "string" && v.length > 0 ? v : void 0;
824
+ }
825
+ function numField(rec, key) {
826
+ const v = rec[key];
827
+ if (typeof v === "number" && Number.isFinite(v)) return v;
828
+ if (typeof v === "string" && /^\d+(\.\d+)?$/.test(v.trim())) {
829
+ const n = Number(v);
830
+ if (Number.isFinite(n)) return n;
831
+ }
832
+ return void 0;
833
+ }
834
+ async function validateSkillNameAvailable(name, loader) {
835
+ const formatViolations = validateSkillName(name);
836
+ const conflicts = loader ? (await loader.listEntries()).filter((e) => e.name === name) : [];
837
+ const formatOk = formatViolations.length === 0;
838
+ const collisionBlocks = conflicts.some((c) => c.source === "project" || c.source === "user");
839
+ return {
840
+ ok: formatOk && !collisionBlocks,
841
+ formatViolations,
842
+ conflicts
843
+ };
844
+ }
845
+ function generateSkillSkeleton(opts) {
846
+ const name = opts.name.trim();
847
+ if (!isValidSkillNameFormat(name)) {
848
+ throw new WrongStackError({
849
+ message: `Cannot generate skeleton: invalid skill name "${name}". Names must be kebab-case (a-z0-9 and hyphens), \u226464 chars.`,
850
+ code: ERROR_CODES.VALIDATION_ERROR,
851
+ subsystem: "general",
852
+ context: { name }
853
+ });
854
+ }
855
+ const description = opts.description.trim();
856
+ const triggers = (opts.triggerKeywords ?? []).map((k) => k.trim()).filter(Boolean).map((k) => `"${k}"`).join(", ");
857
+ const triggerLine = triggers ? `
858
+ Triggers: user says ${triggers}.` : "";
859
+ const version = opts.version?.trim() || "1.0.0";
860
+ const title = name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
861
+ return `---
862
+ name: ${name}
863
+ description: |
864
+ ${description || `Use this skill when <trigger situation>.`}${triggerLine}
865
+ version: ${version}
866
+ ---
867
+
868
+ # ${title}
869
+
870
+ ## Overview
871
+
872
+ ${description || "One-line description of what this skill does."}
873
+
874
+ ## Rules
875
+
876
+ 1. Rule one
877
+ 2. Rule two
878
+
879
+ ## Patterns
880
+
881
+ ### Do
882
+
883
+ \`\`\`ts
884
+ // good example
885
+ \`\`\`
886
+
887
+ ### Don't
888
+
889
+ \`\`\`ts
890
+ // bad example
891
+ \`\`\`
892
+
893
+ ## Skills in scope
894
+
895
+ - \`other-skill\` \u2014 for delegation when this skill needs help
896
+ `;
897
+ }
898
+ function extractSkillFromPrompt(prompt) {
899
+ const text = prompt.trim();
900
+ if (!text) {
901
+ return { suggestedName: "", description: "", body: "", triggerKeywords: [] };
902
+ }
903
+ const headingMatch = text.match(/^#{1,6}\s+(.+?)\s*$/m);
904
+ const firstLine = text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? text;
905
+ const titleSource = headingMatch?.[1] ?? firstLine.replace(/^#+\s*/, "");
906
+ const suggestedName = toKebab(titleSource).slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN);
907
+ const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
908
+ const description = paragraphs[0]?.replace(/^#{1,6}\s+/m, "").trim() ?? titleSource;
909
+ const body = paragraphs.slice(1).join("\n\n");
910
+ const quoted = [...text.matchAll(/"([^"]{2,40})"/g)].map((m) => m[1]).filter((q) => typeof q === "string").map((q) => q.toLowerCase());
911
+ const titleWords = titleSource.split(/\W+/).map((w) => w.toLowerCase()).filter((w) => w.length > 3 && !STOPWORDS.has(w));
912
+ const triggerKeywords = dedupe([...quoted, ...titleWords]).slice(0, 8);
913
+ return { suggestedName, description, body, triggerKeywords };
914
+ }
915
+ async function openInEditor(filePath, env = process.env) {
916
+ const editor = env["VISUAL"]?.trim() && env["VISUAL"] || env["EDITOR"]?.trim() && env["EDITOR"] || defaultEditor();
917
+ if (!editor) {
918
+ throw new WrongStackError({
919
+ message: "No editor found. Set the EDITOR or VISUAL environment variable (e.g. `export EDITOR=code` or `export EDITOR=vim`).",
920
+ code: ERROR_CODES.VALIDATION_ERROR,
921
+ subsystem: "general",
922
+ context: { filePath }
923
+ });
924
+ }
925
+ const shell = process.platform === "win32";
926
+ const parts = editor.split(/\s+/).filter(Boolean);
927
+ if (parts.length === 0) {
928
+ throw new WrongStackError({
929
+ message: "Resolved editor command is empty.",
930
+ code: ERROR_CODES.VALIDATION_ERROR,
931
+ subsystem: "general",
932
+ context: { filePath }
933
+ });
934
+ }
935
+ const child = spawn(parts[0], [...parts.slice(1), filePath], {
936
+ stdio: "ignore",
937
+ detached: true,
938
+ shell
939
+ });
940
+ child.unref();
941
+ }
942
+ async function writeSkeletonSkill(skillsDir, body, opts = {}) {
943
+ const fm = parseSkillFrontmatter(body);
944
+ const name = fm.name;
945
+ if (!name || !isValidSkillNameFormat(name)) {
946
+ throw new WrongStackError({
947
+ message: "Skeleton body has no valid `name` in its frontmatter.",
948
+ code: ERROR_CODES.VALIDATION_ERROR,
949
+ subsystem: "general"
950
+ });
951
+ }
952
+ const skillDir = path5.join(skillsDir, name);
953
+ const skillFile = path5.join(skillDir, "SKILL.md");
954
+ if (!opts.overwrite) {
955
+ try {
956
+ await fs5.access(skillFile);
957
+ throw new WrongStackError({
958
+ message: `A skill already exists at ${skillFile}. Use --force to overwrite.`,
959
+ code: ERROR_CODES.VALIDATION_ERROR,
960
+ subsystem: "general",
961
+ context: { skillFile }
962
+ });
963
+ } catch (err) {
964
+ if (err instanceof WrongStackError) throw err;
965
+ }
966
+ }
967
+ await fs5.mkdir(skillDir, { recursive: true });
968
+ await fs5.writeFile(skillFile, body, "utf8");
969
+ return skillFile;
970
+ }
971
+ function bodyLineAdvisory(body) {
972
+ const lines = body.split("\n").length;
973
+ return { lines, over: lines > SKILL_LIMITS.SKILL_BODY_LINE_LIMIT };
974
+ }
975
+ var STOPWORDS = /* @__PURE__ */ new Set([
976
+ "the",
977
+ "and",
978
+ "for",
979
+ "with",
980
+ "that",
981
+ "this",
982
+ "from",
983
+ "your",
984
+ "have",
985
+ "will",
986
+ "are",
987
+ "was",
988
+ "were",
989
+ "been",
990
+ "into",
991
+ "when",
992
+ "use",
993
+ "skill",
994
+ "about"
995
+ ]);
996
+ function toKebab(s) {
997
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SKILL_LIMITS.SKILL_NAME_MAX_LEN) || "skill";
998
+ }
999
+ function dedupe(arr) {
1000
+ return [...new Set(arr)];
1001
+ }
1002
+ function defaultEditor() {
1003
+ if (process.platform === "win32") return "notepad";
1004
+ if (process.platform === "darwin") return "open";
1005
+ return void 0;
1006
+ }
1007
+ var MAX_SKILL_FILE_SIZE = SKILL_LIMITS.MAX_SKILL_FILE_SIZE;
406
1008
  var SkillInstaller = class {
407
1009
  opts;
408
1010
  manifest;
1011
+ adapters;
409
1012
  constructor(opts) {
410
1013
  this.opts = opts;
411
1014
  this.manifest = new SkillManifestStore(opts.manifestPath);
1015
+ this.adapters = opts.registryAdapters?.length ? opts.registryAdapters : [githubDirectAdapter];
412
1016
  }
413
1017
  /**
414
- * Install skills from a GitHub repository.
415
- * Supports both single-skill repos (SKILL.md at root) and multi-skill repos (skills/ subdirectory).
1018
+ * Install skills from a skill reference.
1019
+ *
1020
+ * Accepts two ref formats:
1021
+ * - `user/repo[@ref]` — direct GitHub install (original path)
1022
+ * - `<adapterId>:<registryId>` — registry-resolved (e.g.
1023
+ * `skills.sh:owner/repo@v1`); the
1024
+ * adapter resolves it to a `user/repo`
1025
+ * ref and the install proceeds as above.
1026
+ *
1027
+ * Supports both single-skill repos (SKILL.md at root) and multi-skill repos
1028
+ * (skills/ subdirectory). The manifest records the GitHub source as
1029
+ * `github:owner/repo` (so `/skill-update` keeps working) plus the originating
1030
+ * registry in `registryFrom` when the install came through a registry.
416
1031
  */
417
1032
  async install(refInput, opts) {
418
- const parsed = parseSkillRef(refInput);
1033
+ const resolved = this.resolveRef(refInput);
1034
+ const parsed = parseSkillRef(resolved.installRef);
419
1035
  const scope = opts?.global ? "user" : "project";
420
1036
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
421
1037
  const source = `github:${parsed.owner}/${parsed.repo}`;
422
- this.opts.log?.(`Downloading ${parsed.owner}/${parsed.repo}@${parsed.ref}...`);
1038
+ this.opts.log?.(
1039
+ resolved.fromRegistry ? `Resolving ${refInput} \u2192 ${resolved.installRef} (${resolved.adapterId}), downloading...` : `Downloading ${parsed.owner}/${parsed.repo}@${parsed.ref}...`
1040
+ );
423
1041
  const { tempDir } = await downloadGitHubTarball(parsed);
424
1042
  try {
425
1043
  const skills = await this.detectSkills(tempDir);
@@ -439,14 +1057,14 @@ var SkillInstaller = class {
439
1057
  this.opts.log?.(`Overwriting existing skill "${skill.name}" (${scope})...`);
440
1058
  await this.removeSkillFiles(skill.name, scope);
441
1059
  }
442
- const destDir = path4.join(targetDir, skill.name);
443
- await fs4.mkdir(destDir, { recursive: true });
1060
+ const destDir = path5.join(targetDir, skill.name);
1061
+ await fs5.mkdir(destDir, { recursive: true });
444
1062
  const copiedFiles = [];
445
1063
  for (const file of skill.files) {
446
- const srcPath = path4.join(skill.baseDir, file);
447
- const destPath = path4.join(destDir, file);
448
- const resolved = path4.resolve(destPath);
449
- if (!resolved.startsWith(path4.resolve(destDir))) {
1064
+ const srcPath = path5.join(skill.baseDir, file);
1065
+ const destPath = path5.join(destDir, file);
1066
+ const resolved2 = path5.resolve(destPath);
1067
+ if (!resolved2.startsWith(path5.resolve(destDir))) {
450
1068
  throw new FsError({
451
1069
  message: `Path traversal detected in skill file: ${file}`,
452
1070
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -454,7 +1072,7 @@ var SkillInstaller = class {
454
1072
  context: { reason: "path_traversal", skillName: skill.name }
455
1073
  });
456
1074
  }
457
- const stat3 = await fs4.stat(srcPath);
1075
+ const stat3 = await fs5.stat(srcPath);
458
1076
  if (stat3.size > MAX_SKILL_FILE_SIZE) {
459
1077
  throw new FsError({
460
1078
  message: `Skill file "${file}" is too large (${(stat3.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
@@ -463,8 +1081,8 @@ var SkillInstaller = class {
463
1081
  context: { skillName: skill.name, fileSize: stat3.size, maxSize: MAX_SKILL_FILE_SIZE }
464
1082
  });
465
1083
  }
466
- await fs4.mkdir(path4.dirname(destPath), { recursive: true });
467
- await fs4.copyFile(srcPath, destPath);
1084
+ await fs5.mkdir(path5.dirname(destPath), { recursive: true });
1085
+ await fs5.copyFile(srcPath, destPath);
468
1086
  copiedFiles.push(file);
469
1087
  }
470
1088
  const entry = {
@@ -474,7 +1092,12 @@ var SkillInstaller = class {
474
1092
  scope,
475
1093
  projectHash: scope === "project" ? this.opts.projectHash : void 0,
476
1094
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
477
- files: copiedFiles
1095
+ files: copiedFiles,
1096
+ // When the install came through a registry (e.g. skills.sh), record
1097
+ // which adapter + registry id resolved to this GitHub repo, so the
1098
+ // source is traceable. `source` stays `github:owner/repo` for
1099
+ // backward compat with `/skill-update`.
1100
+ ...resolved.fromRegistry ? { registryFrom: { adapterId: resolved.adapterId, registryId: resolved.registryId } } : {}
478
1101
  };
479
1102
  await this.manifest.addEntry(entry);
480
1103
  results.push({
@@ -489,10 +1112,97 @@ var SkillInstaller = class {
489
1112
  this.invalidateLoaderCache();
490
1113
  return results;
491
1114
  } finally {
492
- await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
1115
+ await fs5.rm(tempDir, { recursive: true, force: true }).catch(() => {
493
1116
  });
494
1117
  }
495
1118
  }
1119
+ /**
1120
+ * Import skills from a local directory (e.g. `.claude/skills`) into the
1121
+ * project or user skills dir, optionally as symlinks. Used by `/skill-import`
1122
+ * to take ownership of foreign skills so they can be edited/committed.
1123
+ * Each direct subdirectory containing a valid `SKILL.md` is copied verbatim.
1124
+ */
1125
+ async importFromDir(srcDir, opts) {
1126
+ const scope = opts?.global ? "user" : "project";
1127
+ const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
1128
+ let entries;
1129
+ try {
1130
+ entries = await fs5.readdir(srcDir, { withFileTypes: true });
1131
+ } catch {
1132
+ throw new WrongStackError({
1133
+ message: `Source directory not found or not readable: ${srcDir}`,
1134
+ code: ERROR_CODES.VALIDATION_ERROR,
1135
+ subsystem: "general",
1136
+ context: { srcDir }
1137
+ });
1138
+ }
1139
+ const results = [];
1140
+ for (const e of entries) {
1141
+ if (!await entryIsDirectory(srcDir, e)) continue;
1142
+ const skillMdPath = path5.join(srcDir, e.name, "SKILL.md");
1143
+ let content;
1144
+ try {
1145
+ content = await fs5.readFile(skillMdPath, "utf8");
1146
+ } catch {
1147
+ continue;
1148
+ }
1149
+ const fm = parseSkillFrontmatter(content);
1150
+ if (!fm.name || !fm.description || !isValidSkillNameFormat(fm.name)) continue;
1151
+ const existing = await this.manifest.findByName(fm.name);
1152
+ if (existing.find((x) => x.scope === scope)) {
1153
+ await this.removeSkillFiles(fm.name, scope);
1154
+ }
1155
+ const destDir = path5.join(targetDir, fm.name);
1156
+ await fs5.mkdir(destDir, { recursive: true });
1157
+ const srcSkillDir = path5.join(srcDir, e.name);
1158
+ const files = await collectFiles(srcSkillDir, srcSkillDir);
1159
+ const copiedFiles = [];
1160
+ for (const file of files) {
1161
+ const srcPath = path5.join(srcSkillDir, file);
1162
+ const destPath = path5.join(destDir, file);
1163
+ const resolved = path5.resolve(destPath);
1164
+ if (!resolved.startsWith(path5.resolve(destDir))) {
1165
+ throw new FsError({
1166
+ message: `Path traversal detected in skill file: ${file}`,
1167
+ code: ERROR_CODES.FS_DELETE_FAILED,
1168
+ path: destPath,
1169
+ context: { reason: "path_traversal", skillName: fm.name }
1170
+ });
1171
+ }
1172
+ await fs5.mkdir(path5.dirname(destPath), { recursive: true });
1173
+ if (opts?.link) {
1174
+ try {
1175
+ await fs5.symlink(srcPath, destPath);
1176
+ } catch (err) {
1177
+ await fs5.copyFile(srcPath, destPath);
1178
+ this.opts.log?.(`symlink failed for ${file} (${toErrorMessage(err)}); copied instead`);
1179
+ }
1180
+ } else {
1181
+ await fs5.copyFile(srcPath, destPath);
1182
+ }
1183
+ copiedFiles.push(file);
1184
+ }
1185
+ await this.manifest.addEntry({
1186
+ name: fm.name,
1187
+ source: `import:${srcDir}`,
1188
+ ref: "-",
1189
+ scope,
1190
+ projectHash: scope === "project" ? this.opts.projectHash : void 0,
1191
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1192
+ files: copiedFiles
1193
+ });
1194
+ results.push({
1195
+ name: fm.name,
1196
+ path: destDir,
1197
+ scope,
1198
+ source: `import:${srcDir}`,
1199
+ ref: "-",
1200
+ skillCount: 1
1201
+ });
1202
+ }
1203
+ this.invalidateLoaderCache();
1204
+ return results;
1205
+ }
496
1206
  /**
497
1207
  * Update installed skills.
498
1208
  * - No args: update all
@@ -593,6 +1303,28 @@ var SkillInstaller = class {
593
1303
  async listInstalled() {
594
1304
  return this.manifest.listAll();
595
1305
  }
1306
+ /**
1307
+ * Search across all configured registry adapters. Results from each adapter
1308
+ * are merged (deduplicated by `installRef`, adapters earlier in the list win
1309
+ * conflicts). Adapters that don't support search (e.g. github-direct)
1310
+ * contribute nothing.
1311
+ */
1312
+ async search(query, opts) {
1313
+ const settled = await Promise.allSettled(this.adapters.map((a) => a.search(query, opts)));
1314
+ const perAdapter = [];
1315
+ const errors = [];
1316
+ settled.forEach((s, i) => {
1317
+ if (s.status === "fulfilled") perAdapter.push(s.value);
1318
+ else {
1319
+ const id = this.adapters[i]?.id ?? "?";
1320
+ errors.push(`${id}: ${toErrorMessage(s.reason)}`);
1321
+ }
1322
+ });
1323
+ if (errors.length > 0) {
1324
+ this.opts.log?.(`Some registries failed during search: ${errors.join("; ")}`);
1325
+ }
1326
+ return dedupeSearchResults(perAdapter);
1327
+ }
596
1328
  // ── Private helpers ──────────────────────────────────────────────
597
1329
  /**
598
1330
  * Detect skills in an extracted repository.
@@ -600,14 +1332,14 @@ var SkillInstaller = class {
600
1332
  */
601
1333
  async detectSkills(baseDir) {
602
1334
  const results = [];
603
- const rootSkillMd = path4.join(baseDir, "SKILL.md");
1335
+ const rootSkillMd = path5.join(baseDir, "SKILL.md");
604
1336
  try {
605
- await fs4.access(rootSkillMd);
606
- const content = await fs4.readFile(rootSkillMd, "utf8");
607
- const meta = parseFrontmatter(content);
608
- if (meta.name && meta.description) {
1337
+ await fs5.access(rootSkillMd);
1338
+ const content = await fs5.readFile(rootSkillMd, "utf8");
1339
+ const fm = parseSkillFrontmatter(content);
1340
+ if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
609
1341
  results.push({
610
- name: meta.name,
1342
+ name: fm.name,
611
1343
  baseDir,
612
1344
  files: ["SKILL.md"]
613
1345
  });
@@ -615,20 +1347,20 @@ var SkillInstaller = class {
615
1347
  }
616
1348
  } catch {
617
1349
  }
618
- const skillsDir = path4.join(baseDir, "skills");
1350
+ const skillsDir = path5.join(baseDir, "skills");
619
1351
  try {
620
- const entries = await fs4.readdir(skillsDir, { withFileTypes: true });
1352
+ const entries = await fs5.readdir(skillsDir, { withFileTypes: true });
621
1353
  for (const entry of entries) {
622
1354
  if (!entry.isDirectory()) continue;
623
- const skillFile = path4.join(skillsDir, entry.name, "SKILL.md");
1355
+ const skillFile = path5.join(skillsDir, entry.name, "SKILL.md");
624
1356
  try {
625
- const content = await fs4.readFile(skillFile, "utf8");
626
- const meta = parseFrontmatter(content);
627
- if (meta.name && meta.description) {
628
- const skillDir = path4.join(skillsDir, entry.name);
1357
+ const content = await fs5.readFile(skillFile, "utf8");
1358
+ const fm = parseSkillFrontmatter(content);
1359
+ if (fm.name && fm.description && isValidSkillNameFormat(fm.name)) {
1360
+ const skillDir = path5.join(skillsDir, entry.name);
629
1361
  const files = await collectFiles(skillDir, skillDir);
630
1362
  results.push({
631
- name: meta.name,
1363
+ name: fm.name,
632
1364
  baseDir: skillDir,
633
1365
  files
634
1366
  });
@@ -645,8 +1377,8 @@ var SkillInstaller = class {
645
1377
  */
646
1378
  async removeSkillFiles(name, scope) {
647
1379
  const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
648
- const skillDir = path4.join(targetDir, name);
649
- await fs4.rm(skillDir, { recursive: true, force: true });
1380
+ const skillDir = path5.join(targetDir, name);
1381
+ await fs5.rm(skillDir, { recursive: true, force: true });
650
1382
  }
651
1383
  /**
652
1384
  * Invalidate the skill loader's cache so newly installed skills appear.
@@ -657,49 +1389,43 @@ var SkillInstaller = class {
657
1389
  loader.invalidateCache();
658
1390
  }
659
1391
  }
660
- };
661
- function parseFrontmatter(raw) {
662
- if (!raw.startsWith("---")) return {};
663
- const end = raw.indexOf("\n---", 4);
664
- if (end === -1) return {};
665
- const block = raw.slice(4, end);
666
- const out = {};
667
- let key = null;
668
- let value = [];
669
- const flush = () => {
670
- if (key) {
671
- out[key] = value.join("\n").trim();
672
- }
673
- key = null;
674
- value = [];
675
- };
676
- for (const line of block.split("\n")) {
677
- const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
678
- if (m) {
679
- flush();
680
- key = m[1] ?? "";
681
- const pipe = m[2];
682
- const rest = m[3] ?? "";
683
- if (pipe === "|") {
684
- value = [];
685
- } else if (rest) {
686
- value = [rest];
687
- } else {
688
- value = [];
1392
+ /**
1393
+ * Resolve an install ref, dispatching registry-prefixed refs to the matching
1394
+ * adapter. Returns the concrete `user/repo[@ref]` install ref plus provenance
1395
+ * (which adapter resolved it, if any).
1396
+ */
1397
+ resolveRef(refInput) {
1398
+ const trimmed = refInput.trim();
1399
+ const colonIdx = trimmed.indexOf(":");
1400
+ if (colonIdx > 0) {
1401
+ const maybeAdapterId = trimmed.slice(0, colonIdx);
1402
+ const adapter = this.adapters.find((a) => a.id === maybeAdapterId);
1403
+ if (adapter) {
1404
+ const registryId = trimmed.slice(colonIdx + 1);
1405
+ const installRef = adapter.resolveInstallRef(registryId);
1406
+ return { installRef, fromRegistry: true, adapterId: adapter.id, registryId };
689
1407
  }
690
- } else if (key) {
691
- value.push(line.replace(/^\s+/, ""));
692
1408
  }
1409
+ return { installRef: trimmed, fromRegistry: false, adapterId: "", registryId: "" };
693
1410
  }
694
- flush();
695
- return out;
1411
+ };
1412
+ async function entryIsDirectory(dir, entry) {
1413
+ if (entry.isDirectory()) return true;
1414
+ if (entry.isSymbolicLink()) {
1415
+ try {
1416
+ return (await fs5.stat(path5.join(dir, entry.name))).isDirectory();
1417
+ } catch {
1418
+ return false;
1419
+ }
1420
+ }
1421
+ return false;
696
1422
  }
697
1423
  async function collectFiles(dir, baseDir) {
698
1424
  const results = [];
699
- const entries = await fs4.readdir(dir, { withFileTypes: true });
1425
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
700
1426
  for (const entry of entries) {
701
- const fullPath = path4.join(dir, entry.name);
702
- const relPath = path4.relative(baseDir, fullPath);
1427
+ const fullPath = path5.join(dir, entry.name);
1428
+ const relPath = path5.relative(baseDir, fullPath);
703
1429
  if (entry.isDirectory()) {
704
1430
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
705
1431
  results.push(...await collectFiles(fullPath, baseDir));
@@ -709,7 +1435,24 @@ async function collectFiles(dir, baseDir) {
709
1435
  }
710
1436
  return results;
711
1437
  }
1438
+ function dedupeSearchResults(perAdapter) {
1439
+ const seen = /* @__PURE__ */ new Set();
1440
+ const out = [];
1441
+ for (const block of perAdapter) {
1442
+ const kept = [];
1443
+ for (const r of block.results) {
1444
+ const key = r.installRef;
1445
+ if (seen.has(key)) continue;
1446
+ seen.add(key);
1447
+ kept.push(r);
1448
+ }
1449
+ if (kept.length > 0 || block.results.length > 0) {
1450
+ out.push({ ...block, results: kept });
1451
+ }
1452
+ }
1453
+ return out;
1454
+ }
712
1455
 
713
- export { SkillInstaller, SkillManifestStore, downloadGitHubTarball, parseSkillRef };
1456
+ export { DEFAULT_SKILLS_SH_URL, FOREIGN_SKILL_TOOLS, SKILL_LIMITS, SkillInstaller, SkillManifestStore, bodyLineAdvisory, createSkillsShAdapter, downloadGitHubTarball, extractSkillFromPrompt, generateSkillSkeleton, githubDirectAdapter, isValidSkillNameFormat, openInEditor, parseSkillFrontmatter, parseSkillRef, resolveForeignToolIds, resolveForeignToolIdsWithWarnings, securityScoreToTier, stripFrontmatter, validateSkillName, validateSkillNameAvailable, writeSkeletonSkill };
714
1457
  //# sourceMappingURL=index.js.map
715
1458
  //# sourceMappingURL=index.js.map