@schoolai/shipyard 3.7.0 → 3.8.0-rc.20260529.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 (104) hide show
  1. package/dist/{auth-SS7LV5XK.js → auth-EXHO3AG5.js} +4 -4
  2. package/dist/capability-detector-worker.js +142 -0
  3. package/dist/capability-detector-worker.js.map +1 -0
  4. package/dist/{chunk-DKMDBOFU.js → chunk-2CNIEBKO.js} +21 -11
  5. package/dist/chunk-2CNIEBKO.js.map +1 -0
  6. package/dist/chunk-4T2OQAVL.js +51 -0
  7. package/dist/chunk-4T2OQAVL.js.map +1 -0
  8. package/dist/chunk-5ER6ZHA2.js +46 -0
  9. package/dist/chunk-5ER6ZHA2.js.map +1 -0
  10. package/dist/chunk-7LSEE26O.js +24227 -0
  11. package/dist/chunk-7LSEE26O.js.map +1 -0
  12. package/dist/{chunk-7AHRFPAL.js → chunk-7YOU7MBN.js} +183 -17
  13. package/dist/chunk-7YOU7MBN.js.map +1 -0
  14. package/dist/chunk-CMGJGK6R.js +382 -0
  15. package/dist/chunk-CMGJGK6R.js.map +1 -0
  16. package/dist/{chunk-VPMN47TL.js → chunk-CNR7O5YH.js} +1 -2
  17. package/dist/{chunk-2J3WSIAF.js → chunk-EF2DAODF.js} +18 -3
  18. package/dist/chunk-EF2DAODF.js.map +1 -0
  19. package/dist/chunk-HQ43PHOH.js +1203 -0
  20. package/dist/chunk-HQ43PHOH.js.map +1 -0
  21. package/dist/chunk-KITSAHTX.js +134 -0
  22. package/dist/chunk-KITSAHTX.js.map +1 -0
  23. package/dist/chunk-LESHN5J5.js +6898 -0
  24. package/dist/chunk-LESHN5J5.js.map +1 -0
  25. package/dist/{chunk-LW2MS4T5.js → chunk-LMJFHKRD.js} +15 -12
  26. package/dist/chunk-LMJFHKRD.js.map +1 -0
  27. package/dist/{chunk-SNYEQHUK.js → chunk-NACJENDW.js} +14 -21
  28. package/dist/chunk-NACJENDW.js.map +1 -0
  29. package/dist/{chunk-IISLTKYY.js → chunk-TU63KZFW.js} +2 -2
  30. package/dist/chunk-TX6DK4PK.js +186 -0
  31. package/dist/chunk-TX6DK4PK.js.map +1 -0
  32. package/dist/chunk-UQVXWOPT.js +48 -0
  33. package/dist/chunk-UQVXWOPT.js.map +1 -0
  34. package/dist/{chunk-3MNPDCO5.js → chunk-WBB4XHLH.js} +139 -140
  35. package/dist/chunk-WBB4XHLH.js.map +1 -0
  36. package/dist/chunk-X3MULCV5.js +11 -0
  37. package/dist/chunk-X3MULCV5.js.map +1 -0
  38. package/dist/chunk-YZ3Z3ZYI.js +787 -0
  39. package/dist/chunk-YZ3Z3ZYI.js.map +1 -0
  40. package/dist/{chunk-2UN5AR7V.js → chunk-ZAOPND5G.js} +2 -2
  41. package/dist/chunk-ZFKJAYAN.js +542 -0
  42. package/dist/chunk-ZFKJAYAN.js.map +1 -0
  43. package/dist/cursor-hook-shim.js +316 -0
  44. package/dist/cursor-hook-shim.js.map +1 -0
  45. package/dist/cursor-runner.js +358 -0
  46. package/dist/cursor-runner.js.map +1 -0
  47. package/dist/electron-utility.js +111 -0
  48. package/dist/electron-utility.js.map +1 -0
  49. package/dist/git-pool-V73Q53NX.js +18 -0
  50. package/dist/{git-repo-VRT57DGC.js → git-repo-TN3VZXQV.js} +9 -6
  51. package/dist/index.js +12 -12
  52. package/dist/index.js.map +1 -1
  53. package/dist/{logger-GQCSLSZH.js → logger-QHPTO22N.js} +4 -4
  54. package/dist/login-Q7SZI7JJ.js +20 -0
  55. package/dist/{logout-VUNCW5B2.js → logout-O4AVMO5S.js} +6 -6
  56. package/dist/mcp-servers-F64M5T4I.js +24 -0
  57. package/dist/{roi-Y3MX5UW4.js → roi-EYDLPOCS.js} +5 -5
  58. package/dist/rss-worker.js +159 -0
  59. package/dist/rss-worker.js.map +1 -0
  60. package/dist/{serve-O53FNK64.js → serve-6A7RJWEF.js} +89862 -102999
  61. package/dist/{serve-O53FNK64.js.map → serve-6A7RJWEF.js.map} +1 -1
  62. package/dist/skills-ZHEPSBHW.js +11 -0
  63. package/dist/{start-IDFDHRD6.js → start-YGYYIK53.js} +229 -27
  64. package/dist/start-YGYYIK53.js.map +1 -0
  65. package/dist/vault-crypto-BKDOA65F.js +13 -0
  66. package/dist/vault-crypto-BKDOA65F.js.map +1 -0
  67. package/dist/worker.js +6 -3
  68. package/dist/worker.js.map +1 -1
  69. package/package.json +17 -10
  70. package/dist/chunk-2J3WSIAF.js.map +0 -1
  71. package/dist/chunk-3MNPDCO5.js.map +0 -1
  72. package/dist/chunk-66OBOZ3X.js +0 -79
  73. package/dist/chunk-66OBOZ3X.js.map +0 -1
  74. package/dist/chunk-7AHRFPAL.js.map +0 -1
  75. package/dist/chunk-DKMDBOFU.js.map +0 -1
  76. package/dist/chunk-L2WQMPWS.js +0 -666
  77. package/dist/chunk-L2WQMPWS.js.map +0 -1
  78. package/dist/chunk-LW2MS4T5.js.map +0 -1
  79. package/dist/chunk-PI77CUEP.js +0 -49
  80. package/dist/chunk-PI77CUEP.js.map +0 -1
  81. package/dist/chunk-RXI4637N.js +0 -395
  82. package/dist/chunk-RXI4637N.js.map +0 -1
  83. package/dist/chunk-SNYEQHUK.js.map +0 -1
  84. package/dist/chunk-VBPHGPBR.js +0 -126
  85. package/dist/chunk-VBPHGPBR.js.map +0 -1
  86. package/dist/index.d.ts +0 -2
  87. package/dist/login-L4BBPUYO.js +0 -20
  88. package/dist/mcp-servers-MXS5VAWI.js +0 -18
  89. package/dist/shell-V36EX2IJ.js +0 -27
  90. package/dist/skills-GPGRNV4R.js +0 -9
  91. package/dist/start-IDFDHRD6.js.map +0 -1
  92. package/dist/worker.d.ts +0 -49
  93. /package/dist/{auth-SS7LV5XK.js.map → auth-EXHO3AG5.js.map} +0 -0
  94. /package/dist/{chunk-VPMN47TL.js.map → chunk-CNR7O5YH.js.map} +0 -0
  95. /package/dist/{chunk-IISLTKYY.js.map → chunk-TU63KZFW.js.map} +0 -0
  96. /package/dist/{chunk-2UN5AR7V.js.map → chunk-ZAOPND5G.js.map} +0 -0
  97. /package/dist/{git-repo-VRT57DGC.js.map → git-pool-V73Q53NX.js.map} +0 -0
  98. /package/dist/{logger-GQCSLSZH.js.map → git-repo-TN3VZXQV.js.map} +0 -0
  99. /package/dist/{login-L4BBPUYO.js.map → logger-QHPTO22N.js.map} +0 -0
  100. /package/dist/{mcp-servers-MXS5VAWI.js.map → login-Q7SZI7JJ.js.map} +0 -0
  101. /package/dist/{logout-VUNCW5B2.js.map → logout-O4AVMO5S.js.map} +0 -0
  102. /package/dist/{shell-V36EX2IJ.js.map → mcp-servers-F64M5T4I.js.map} +0 -0
  103. /package/dist/{roi-Y3MX5UW4.js.map → roi-EYDLPOCS.js.map} +0 -0
  104. /package/dist/{skills-GPGRNV4R.js.map → skills-ZHEPSBHW.js.map} +0 -0
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/shared/capabilities/skills.ts
4
+ import { readdir, readFile, stat } from "fs/promises";
5
+ import { homedir } from "os";
6
+ import { dirname, join, parse } from "path";
7
+
8
+ // src/shared/capabilities/skill-compat.ts
9
+ function classifySkill(skill, mirrorEnabled) {
10
+ const path = skill.path;
11
+ if (!path) {
12
+ const fallback2 = skill.sourceAgent === "codex" ? "codex" : skill.sourceAgent === "cursor" ? "cursor" : "claude-code";
13
+ return {
14
+ compatibleAgents: [fallback2],
15
+ bodyResolution: "unreachable"
16
+ };
17
+ }
18
+ if (mirrorEnabled) {
19
+ return {
20
+ compatibleAgents: ["claude-code", "codex", "cursor"],
21
+ bodyResolution: "mirror"
22
+ };
23
+ }
24
+ if (path.includes("/cursor-skills/") || path.includes("/.cursor/")) {
25
+ return {
26
+ compatibleAgents: ["cursor"],
27
+ bodyResolution: "native"
28
+ };
29
+ }
30
+ if (path.includes("/.codex/") || path.includes("/.agents/")) {
31
+ return {
32
+ compatibleAgents: ["codex"],
33
+ bodyResolution: "native"
34
+ };
35
+ }
36
+ if (path.includes("/.claude/")) {
37
+ return {
38
+ compatibleAgents: ["claude-code", "codex", "cursor"],
39
+ bodyResolution: "pass-in"
40
+ };
41
+ }
42
+ const fallback = skill.sourceAgent === "codex" ? "codex" : skill.sourceAgent === "cursor" ? "cursor" : "claude-code";
43
+ return {
44
+ compatibleAgents: [fallback],
45
+ bodyResolution: "unreachable"
46
+ };
47
+ }
48
+
49
+ // src/shared/capabilities/skills.ts
50
+ function parseSkillFrontmatter(content, fallbackName) {
51
+ const match = /^---\n([\s\S]*?)\n---/.exec(content);
52
+ if (!match?.[1]) return { name: fallbackName, description: "" };
53
+ const frontmatter = match[1];
54
+ const nameMatch = /^name:\s*["']?(.+?)["']?\s*$/m.exec(frontmatter);
55
+ const descMatch = /^description:\s*["']?(.+?)["']?\s*$/m.exec(frontmatter);
56
+ const name = nameMatch?.[1]?.trim() ?? fallbackName;
57
+ const description = descMatch?.[1]?.trim() ?? "";
58
+ return { name, description };
59
+ }
60
+ function sourceAgentForPath(path) {
61
+ if (path.includes("/cursor-skills/") || path.includes("/.cursor/")) {
62
+ return "cursor";
63
+ }
64
+ if (path.includes("/.codex/") || path.includes("/.agents/")) {
65
+ return "codex";
66
+ }
67
+ return "claude-code";
68
+ }
69
+ async function readSkillsFromDir(dirPath) {
70
+ try {
71
+ const entries = await readdir(dirPath, { withFileTypes: true });
72
+ const skills = [];
73
+ for (const entry of entries) {
74
+ if (!entry.isDirectory()) continue;
75
+ const skillDir = join(dirPath, entry.name);
76
+ const skillFile = join(skillDir, "SKILL.md");
77
+ try {
78
+ const [content, st] = await Promise.all([readFile(skillFile, "utf-8"), stat(skillFile)]);
79
+ const parsed = parseSkillFrontmatter(content, entry.name);
80
+ skills.push({
81
+ ...parsed,
82
+ path: skillFile,
83
+ bodyBytes: st.size,
84
+ sourceAgent: sourceAgentForPath(skillFile)
85
+ });
86
+ } catch {
87
+ skills.push({
88
+ name: entry.name,
89
+ description: "",
90
+ sourceAgent: sourceAgentForPath(skillDir)
91
+ });
92
+ }
93
+ }
94
+ return skills;
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
99
+ async function readSkillsRecursive(dirPath, depth = 0, maxDepth = 4) {
100
+ if (depth > maxDepth) return [];
101
+ const skills = [];
102
+ try {
103
+ const entries = await readdir(dirPath, { withFileTypes: true });
104
+ for (const entry of entries) {
105
+ if (!entry.isDirectory()) continue;
106
+ const subDir = join(dirPath, entry.name);
107
+ const skillFile = join(subDir, "SKILL.md");
108
+ try {
109
+ const [content, st] = await Promise.all([readFile(skillFile, "utf-8"), stat(skillFile)]);
110
+ const parsed = parseSkillFrontmatter(content, entry.name);
111
+ skills.push({
112
+ ...parsed,
113
+ path: skillFile,
114
+ bodyBytes: st.size,
115
+ sourceAgent: sourceAgentForPath(skillFile)
116
+ });
117
+ } catch {
118
+ const nested = await readSkillsRecursive(subDir, depth + 1, maxDepth);
119
+ skills.push(...nested);
120
+ }
121
+ }
122
+ } catch {
123
+ }
124
+ return skills;
125
+ }
126
+ async function readPluginSkills() {
127
+ const skills = [];
128
+ try {
129
+ const settingsPath = join(homedir(), ".claude", "settings.json");
130
+ const settingsRaw = await readFile(settingsPath, "utf-8");
131
+ const settings = JSON.parse(settingsRaw);
132
+ if (!settings.enabledPlugins) return [];
133
+ const installedPath = join(homedir(), ".claude", "plugins", "installed_plugins.json");
134
+ const installedRaw = await readFile(installedPath, "utf-8");
135
+ const installed = JSON.parse(installedRaw);
136
+ if (!installed.plugins) return [];
137
+ for (const [pluginId, enabled] of Object.entries(settings.enabledPlugins)) {
138
+ if (!enabled) continue;
139
+ const installs = installed.plugins[pluginId];
140
+ if (!installs || installs.length === 0) continue;
141
+ const installPath = installs[0]?.installPath;
142
+ if (!installPath) continue;
143
+ const skillsDir = join(installPath, "skills");
144
+ const pluginSkills = await readSkillsRecursive(skillsDir);
145
+ skills.push(...pluginSkills);
146
+ }
147
+ } catch {
148
+ }
149
+ return skills;
150
+ }
151
+ async function readCodexPluginCacheSkills() {
152
+ const cacheRoot = join(homedir(), ".codex", "plugins", "cache");
153
+ const out = [];
154
+ try {
155
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
156
+ for (const entry of entries) {
157
+ if (!entry.isDirectory()) continue;
158
+ const skillsDir = join(cacheRoot, entry.name, "skills");
159
+ const found = await readSkillsRecursive(skillsDir, 0, 2);
160
+ out.push(...found);
161
+ }
162
+ } catch {
163
+ }
164
+ return out;
165
+ }
166
+ async function readCodexUserSkillsDir() {
167
+ const dirPath = join(homedir(), ".codex", "skills");
168
+ try {
169
+ const entries = await readdir(dirPath, { withFileTypes: true });
170
+ const skills = [];
171
+ for (const entry of entries) {
172
+ if (!entry.isDirectory()) continue;
173
+ if (entry.name === ".system") continue;
174
+ const skillDir = join(dirPath, entry.name);
175
+ const skillFile = join(skillDir, "SKILL.md");
176
+ try {
177
+ const [content, st] = await Promise.all([readFile(skillFile, "utf-8"), stat(skillFile)]);
178
+ const parsed = parseSkillFrontmatter(content, entry.name);
179
+ skills.push({
180
+ ...parsed,
181
+ path: skillFile,
182
+ bodyBytes: st.size,
183
+ sourceAgent: "codex"
184
+ });
185
+ } catch {
186
+ skills.push({
187
+ name: entry.name,
188
+ description: "",
189
+ sourceAgent: "codex"
190
+ });
191
+ }
192
+ }
193
+ return skills;
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
198
+ async function readAncestorAgentsSkills(startDir) {
199
+ let dir = startDir;
200
+ for (let i = 0; i < 32; i += 1) {
201
+ const candidate = join(dir, ".agents", "skills");
202
+ const found = await readSkillsFromDir(candidate);
203
+ if (found.length > 0) return found;
204
+ try {
205
+ const gitMarker = await stat(join(dir, ".git"));
206
+ if (gitMarker) return [];
207
+ } catch {
208
+ }
209
+ const parent = dirname(dir);
210
+ if (parent === dir || parent === parse(dir).root) return [];
211
+ dir = parent;
212
+ }
213
+ return [];
214
+ }
215
+ function inferPluginName(path) {
216
+ const match = /\/\.(?:claude|codex)\/plugins\/cache\/(.+?)\/skills\//.exec(path);
217
+ if (!match?.[1]) return null;
218
+ const segments = match[1].split("/").filter(Boolean);
219
+ if (segments.length === 0) return null;
220
+ if (segments.length === 1) return segments[0] ?? null;
221
+ if (segments.length === 2) return segments[0] ?? null;
222
+ return segments[1] ?? null;
223
+ }
224
+ async function detectSkills(environments, lastKnown, mirrorEnabled = false) {
225
+ try {
226
+ return await detectSkillsInner(environments, mirrorEnabled);
227
+ } catch (err) {
228
+ const { logger } = await import("./logger-QHPTO22N.js");
229
+ if (lastKnown && lastKnown.length > 0) {
230
+ logger.warn(
231
+ { err, lastKnownCount: lastKnown.length },
232
+ "detectSkills threw \u2014 preserving lastKnown"
233
+ );
234
+ return lastKnown;
235
+ }
236
+ logger.debug({ err }, "detectSkills threw with no lastKnown \u2014 returning []");
237
+ return [];
238
+ }
239
+ }
240
+ function tag(skills, tier) {
241
+ return skills.map((s) => ({ ...s, __tier: tier }));
242
+ }
243
+ async function detectSkillsInner(environments, mirrorEnabled) {
244
+ const home = homedir();
245
+ const codexSystemDir = join(home, ".codex", "skills", ".system");
246
+ const agentsUserDir = join(home, ".agents", "skills");
247
+ const claudeUserDir = join(home, ".claude", "skills");
248
+ const [userClaude, pluginClaude, codexSystem, codexUser, agentsUser, pluginCodex] = await Promise.all([
249
+ readSkillsFromDir(claudeUserDir),
250
+ readPluginSkills(),
251
+ readSkillsFromDir(codexSystemDir),
252
+ readCodexUserSkillsDir(),
253
+ readSkillsFromDir(agentsUserDir),
254
+ readCodexPluginCacheSkills()
255
+ ]);
256
+ const projectClaude = [];
257
+ const projectCodex = [];
258
+ for (const env of environments) {
259
+ const [claude, codex] = await Promise.all([
260
+ readSkillsFromDir(join(env.path, ".claude", "skills")),
261
+ readAncestorAgentsSkills(env.path)
262
+ ]);
263
+ projectClaude.push(...claude);
264
+ projectCodex.push(...codex);
265
+ await new Promise((resolve) => {
266
+ setImmediate(resolve);
267
+ });
268
+ }
269
+ const tiered = [
270
+ ...tag(userClaude, "user"),
271
+ ...tag(codexSystem, "user"),
272
+ ...tag(codexUser, "user"),
273
+ ...tag(agentsUser, "user"),
274
+ ...tag(pluginClaude, "plugin"),
275
+ ...tag(pluginCodex, "plugin"),
276
+ ...tag(projectClaude, "project"),
277
+ ...tag(projectCodex, "project")
278
+ ];
279
+ const byName = /* @__PURE__ */ new Map();
280
+ for (const skill of tiered) {
281
+ const bucket = byName.get(skill.name);
282
+ if (bucket) {
283
+ bucket.push(skill);
284
+ } else {
285
+ byName.set(skill.name, [skill]);
286
+ }
287
+ }
288
+ const resolved = [];
289
+ for (const [, candidates] of byName) {
290
+ resolved.push(...resolveBucket(candidates));
291
+ }
292
+ return resolved.map((skill) => {
293
+ const { compatibleAgents, bodyResolution } = classifySkill(skill, mirrorEnabled);
294
+ return { ...skill, compatibleAgents, bodyResolution };
295
+ });
296
+ }
297
+ function resolveBucket(candidates) {
298
+ if (candidates.length === 1) {
299
+ const only = candidates[0];
300
+ return only ? [stripTier(only)] : [];
301
+ }
302
+ const winners = pickHighestTier(candidates);
303
+ if (winners.length === 1) {
304
+ const w = winners[0];
305
+ return w ? [stripTier(w)] : [];
306
+ }
307
+ const claudeWinners = winners.filter((c) => c.sourceAgent === "claude-code");
308
+ const codexWinners = winners.filter((c) => c.sourceAgent === "codex");
309
+ if (claudeWinners.length > 0 && codexWinners.length > 0) {
310
+ return resolveCrossAgentCollision(claudeWinners, codexWinners);
311
+ }
312
+ return resolveSameAgentCollision(winners);
313
+ }
314
+ function canonicalWinner(candidates) {
315
+ let best;
316
+ for (const c of candidates) {
317
+ if (!best) {
318
+ best = c;
319
+ } else if (!best.path && c.path) {
320
+ best = c;
321
+ } else if (best.path && c.path && c.path < best.path) {
322
+ best = c;
323
+ }
324
+ }
325
+ return best;
326
+ }
327
+ function resolveCrossAgentCollision(claudeWinners, codexWinners) {
328
+ const out = [];
329
+ const bestClaude = canonicalWinner(claudeWinners);
330
+ const bestCodex = canonicalWinner(codexWinners);
331
+ if (bestClaude) out.push(applyPluginNamespace(stripTier(bestClaude)));
332
+ if (bestCodex) out.push(applyPluginNamespace(stripTier(bestCodex)));
333
+ return out;
334
+ }
335
+ function applyPluginNamespace(skill) {
336
+ if (!skill.path) return skill;
337
+ const pluginId = inferPluginName(skill.path);
338
+ if (pluginId === null) return skill;
339
+ return { ...skill, namespace: pluginId };
340
+ }
341
+ function resolveSameAgentCollision(winners) {
342
+ const buckets = /* @__PURE__ */ new Map();
343
+ for (const candidate of winners) {
344
+ const pluginId = candidate.path ? inferPluginName(candidate.path) : null;
345
+ const bucket = buckets.get(pluginId);
346
+ if (bucket) {
347
+ bucket.push(candidate);
348
+ } else {
349
+ buckets.set(pluginId, [candidate]);
350
+ }
351
+ }
352
+ const out = [];
353
+ for (const [pluginId, candidates] of buckets) {
354
+ const winner = canonicalWinner(candidates);
355
+ if (!winner) continue;
356
+ const base = stripTier(winner);
357
+ out.push(pluginId !== null ? { ...base, namespace: pluginId } : base);
358
+ }
359
+ return out;
360
+ }
361
+ function pickHighestTier(candidates) {
362
+ const order = { user: 0, plugin: 1, project: 2 };
363
+ let bestRank = Number.POSITIVE_INFINITY;
364
+ for (const c of candidates) {
365
+ const r = order[c.__tier];
366
+ if (r < bestRank) bestRank = r;
367
+ }
368
+ return candidates.filter((c) => order[c.__tier] === bestRank);
369
+ }
370
+ function stripTier(skill) {
371
+ const { __tier, ...rest } = skill;
372
+ void __tier;
373
+ return rest;
374
+ }
375
+ var _resolveBucketForTesting = resolveBucket;
376
+
377
+ export {
378
+ classifySkill,
379
+ detectSkills,
380
+ _resolveBucketForTesting
381
+ };
382
+ //# sourceMappingURL=chunk-CMGJGK6R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/capabilities/skills.ts","../src/shared/capabilities/skill-compat.ts"],"sourcesContent":["import { readdir, readFile, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join, parse } from 'node:path';\nimport type { GitRepoInfo, SkillInfo } from '@shipyard/session';\n\nimport { classifySkill } from './skill-compat.js';\n\n/**\n * Extract name and description from a SKILL.md YAML frontmatter.\n * Expects `---\\nname: ...\\ndescription: \"...\"\\n---` at the top.\n */\nfunction parseSkillFrontmatter(\n content: string,\n fallbackName: string\n): { name: string; description: string } {\n const match = /^---\\n([\\s\\S]*?)\\n---/.exec(content);\n if (!match?.[1]) return { name: fallbackName, description: '' };\n\n const frontmatter = match[1];\n const nameMatch = /^name:\\s*[\"']?(.+?)[\"']?\\s*$/m.exec(frontmatter);\n const descMatch = /^description:\\s*[\"']?(.+?)[\"']?\\s*$/m.exec(frontmatter);\n\n const name = nameMatch?.[1]?.trim() ?? fallbackName;\n const description = descMatch?.[1]?.trim() ?? '';\n\n return { name, description };\n}\n\n/**\n * Infer the sourceAgent for a skill from its on-disk path.\n *\n * Paths anywhere under `.codex/` or `.agents/` (including plugin caches\n * like `.codex/plugins/cache/<marketplace>/<plugin>/<version>/skills/...`)\n * are tagged 'codex'. Paths anywhere under `.cursor/` or under the\n * Shipyard-owned cursor skills root (`~/.shipyard/cursor-skills/<taskId>/`,\n * written by `cursor-skill-writer.ts`) are tagged 'cursor'. Paths\n * anywhere under `.claude/` are tagged 'claude-code'. Anything else\n * (e.g. project `.claude/skills/` or `.agents/skills/`) is matched by the\n * same fragments. Defaults to 'claude-code' if no marker is found.\n */\nfunction sourceAgentForPath(path: string): 'claude-code' | 'codex' | 'cursor' {\n if (path.includes('/cursor-skills/') || path.includes('/.cursor/')) {\n return 'cursor';\n }\n if (path.includes('/.codex/') || path.includes('/.agents/')) {\n return 'codex';\n }\n return 'claude-code';\n}\n\n/**\n * Read a single skill directory (one level deep). Each child directory\n * must contain a `SKILL.md`. Per-file errors are swallowed (we accept the\n * skill with empty description if SKILL.md is missing/unreadable, to\n * preserve historical behaviour). Captures `bodyBytes` via stat.\n */\nasync function readSkillsFromDir(dirPath: string): Promise<SkillInfo[]> {\n try {\n const entries = await readdir(dirPath, { withFileTypes: true });\n const skills: SkillInfo[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const skillDir = join(dirPath, entry.name);\n const skillFile = join(skillDir, 'SKILL.md');\n try {\n const [content, st] = await Promise.all([readFile(skillFile, 'utf-8'), stat(skillFile)]);\n const parsed = parseSkillFrontmatter(content, entry.name);\n skills.push({\n ...parsed,\n path: skillFile,\n bodyBytes: st.size,\n sourceAgent: sourceAgentForPath(skillFile),\n });\n } catch {\n skills.push({\n name: entry.name,\n description: '',\n sourceAgent: sourceAgentForPath(skillDir),\n });\n }\n }\n\n return skills;\n } catch {\n return [];\n }\n}\n\n/**\n * Recursively walk a directory looking for SKILL.md files. Used for\n * plugin skill bundles which may nest by category. Capped at the natural\n * filesystem structure — readers swallow per-dir errors.\n */\nasync function readSkillsRecursive(dirPath: string, depth = 0, maxDepth = 4): Promise<SkillInfo[]> {\n if (depth > maxDepth) return [];\n const skills: SkillInfo[] = [];\n try {\n const entries = await readdir(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const subDir = join(dirPath, entry.name);\n const skillFile = join(subDir, 'SKILL.md');\n try {\n const [content, st] = await Promise.all([readFile(skillFile, 'utf-8'), stat(skillFile)]);\n const parsed = parseSkillFrontmatter(content, entry.name);\n skills.push({\n ...parsed,\n path: skillFile,\n bodyBytes: st.size,\n sourceAgent: sourceAgentForPath(skillFile),\n });\n } catch {\n const nested = await readSkillsRecursive(subDir, depth + 1, maxDepth);\n skills.push(...nested);\n }\n }\n } catch {}\n return skills;\n}\n\n/**\n * Read Claude plugin skills from the installed-plugins manifest.\n * Only enabled plugins contribute skills. Each plugin's `<installPath>/skills`\n * is walked recursively.\n */\nasync function readPluginSkills(): Promise<SkillInfo[]> {\n const skills: SkillInfo[] = [];\n try {\n const settingsPath = join(homedir(), '.claude', 'settings.json');\n const settingsRaw = await readFile(settingsPath, 'utf-8');\n // eslint-disable-next-line no-restricted-syntax -- JSON.parse returns unknown; checking enabledPlugins field only\n const settings = JSON.parse(settingsRaw) as { enabledPlugins?: Record<string, boolean> };\n if (!settings.enabledPlugins) return [];\n\n const installedPath = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');\n const installedRaw = await readFile(installedPath, 'utf-8');\n // eslint-disable-next-line no-restricted-syntax -- JSON.parse returns unknown; checking plugins field only\n const installed = JSON.parse(installedRaw) as {\n plugins?: Record<string, Array<{ installPath?: string }>>;\n };\n if (!installed.plugins) return [];\n\n for (const [pluginId, enabled] of Object.entries(settings.enabledPlugins)) {\n if (!enabled) continue;\n const installs = installed.plugins[pluginId];\n if (!installs || installs.length === 0) continue;\n const installPath = installs[0]?.installPath;\n if (!installPath) continue;\n\n const skillsDir = join(installPath, 'skills');\n const pluginSkills = await readSkillsRecursive(skillsDir);\n skills.push(...pluginSkills);\n }\n } catch {}\n return skills;\n}\n\n/**\n * Enumerate Codex plugin skill bundles under `~/.codex/plugins/cache/`.\n *\n * Walk one level into the cache root to find plugin dirs, then look for a\n * `skills/` subdir under each. We do NOT recursive-watch this tree (a\n * plugin cache can hold hundreds of files) — this enumeration runs at\n * detect time only. The skills/ subdir itself is read recursively, capped\n * at 2 levels per the plan.\n */\nasync function readCodexPluginCacheSkills(): Promise<SkillInfo[]> {\n const cacheRoot = join(homedir(), '.codex', 'plugins', 'cache');\n const out: SkillInfo[] = [];\n try {\n const entries = await readdir(cacheRoot, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const skillsDir = join(cacheRoot, entry.name, 'skills');\n const found = await readSkillsRecursive(skillsDir, 0, 2);\n out.push(...found);\n }\n } catch {}\n return out;\n}\n\n/**\n * Read `~/.codex/skills/` (deprecated user dir), skipping the `.system`\n * subdirectory — the system bundle is read separately via\n * `readCodexSystemBundle`. Without this skip, every system skill would be\n * surfaced twice.\n */\nasync function readCodexUserSkillsDir(): Promise<SkillInfo[]> {\n const dirPath = join(homedir(), '.codex', 'skills');\n try {\n const entries = await readdir(dirPath, { withFileTypes: true });\n const skills: SkillInfo[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name === '.system') continue;\n const skillDir = join(dirPath, entry.name);\n const skillFile = join(skillDir, 'SKILL.md');\n try {\n const [content, st] = await Promise.all([readFile(skillFile, 'utf-8'), stat(skillFile)]);\n const parsed = parseSkillFrontmatter(content, entry.name);\n skills.push({\n ...parsed,\n path: skillFile,\n bodyBytes: st.size,\n sourceAgent: 'codex',\n });\n } catch {\n skills.push({\n name: entry.name,\n description: '',\n sourceAgent: 'codex',\n });\n }\n }\n return skills;\n } catch {\n return [];\n }\n}\n\n/**\n * Walk parent directories from `startDir` to a filesystem boundary\n * looking for a `.agents/skills/` directory. Stops at a git root (file or\n * dir named `.git`) or the filesystem root. Returns skills from the first\n * `.agents/skills/` encountered; nested envs share their nearest ancestor\n * skill set.\n *\n * Pattern mirrors `findProjectRoot` in `apps/daemon/src/shared/fs-utils.ts`.\n */\nasync function readAncestorAgentsSkills(startDir: string): Promise<SkillInfo[]> {\n let dir = startDir;\n for (let i = 0; i < 32; i += 1) {\n const candidate = join(dir, '.agents', 'skills');\n const found = await readSkillsFromDir(candidate);\n if (found.length > 0) return found;\n /**\n * Stop at git boundary even if no .agents/skills found — don't escape\n * out of the repo into unrelated user/home directories.\n */\n try {\n const gitMarker = await stat(join(dir, '.git'));\n if (gitMarker) return [];\n } catch {}\n const parent = dirname(dir);\n if (parent === dir || parent === parse(dir).root) return [];\n dir = parent;\n }\n return [];\n}\n\n/**\n * Extract the plugin id from a skill path, IF the skill is provided by a\n * plugin. Returns null for user/project/system/pathless cases — those are\n * not plugin-namespaced as far as the agent runtimes are concerned.\n *\n * - `~/.claude/plugins/cache/<plugin-id>/skills/...` → `<plugin-id>`\n * - `~/.claude/plugins/cache/<marketplace>/<plugin-id>/<version>/skills/...` → `<plugin-id>`\n * - `~/.codex/plugins/cache/<marketplace>/<plugin-id>/<version>/skills/...` → `<plugin-id>`\n * - everything else → null\n *\n * The Claude Code SDK's `Options.skills` allowlist accepts bare skill names\n * or `plugin:name` for plugin-provided skills (sdk.d.ts:1629). Codex follows\n * the same convention. Synthetic prefixes like `project:` or `user:` are\n * NOT recognized by either runtime — emitting them produces \"Skill X is\n * not in this session's skills allowlist\" at invocation time. Returning\n * null here means the caller will not prefix the SDK name with a synthetic\n * tag.\n */\nfunction inferPluginName(path: string): string | null {\n const match = /\\/\\.(?:claude|codex)\\/plugins\\/cache\\/(.+?)\\/skills\\//.exec(path);\n if (!match?.[1]) return null;\n\n const segments = match[1].split('/').filter(Boolean);\n if (segments.length === 0) return null;\n if (segments.length === 1) return segments[0] ?? null;\n /** Legacy plugin/version caches still use the first segment as the id. */\n if (segments.length === 2) return segments[0] ?? null;\n\n /**\n * Current plugin caches are marketplace-scoped:\n * cache/<marketplace>/<plugin-id>/<version>/skills/...\n * The runtime allowlist wants `<plugin-id>:<skill>`, not the marketplace\n * owner. Legacy one-segment caches remain supported above.\n */\n return segments[1] ?? null;\n}\n\n/**\n * Detect skills across user/plugin/project sources for both agents.\n *\n * `lastKnown` preserves the previously-detected list when this function\n * throws (rare — inner readers swallow errors — but defense in depth).\n * Mirror of the `lastKnownAgents` pattern in `agents.ts`.\n *\n * `mirrorEnabled` controls how `classifySkill` computes `bodyResolution`\n * and `compatibleAgents` on the returned skills. Defaults to `false`.\n * TODO: wire from `skills.mirror.enabled` user pref once step 11 lands.\n */\nexport async function detectSkills(\n environments: GitRepoInfo[],\n lastKnown?: SkillInfo[],\n mirrorEnabled = false\n): Promise<SkillInfo[]> {\n try {\n return await detectSkillsInner(environments, mirrorEnabled);\n } catch (err) {\n /**\n * Preserve \"always resolves\" contract — inner readers already swallow\n * per-file errors. Never rethrow because the Promise.all in\n * detectCapabilities would tear down the whole refresh for a single\n * slice's failure.\n */\n const { logger } = await import('../logger.js');\n if (lastKnown && lastKnown.length > 0) {\n logger.warn(\n { err, lastKnownCount: lastKnown.length },\n 'detectSkills threw — preserving lastKnown'\n );\n return lastKnown;\n }\n logger.debug({ err }, 'detectSkills threw with no lastKnown — returning []');\n return [];\n }\n}\n\ntype Tier = 'user' | 'plugin' | 'project';\n\ninterface TieredSkill extends SkillInfo {\n __tier: Tier;\n}\n\n/**\n * Re-exported for unit tests in `skills.test.ts`. Underscored to flag\n * that the symbol is part of the test contract, not the public API.\n */\nexport type _TieredSkillForTesting = TieredSkill;\n\nfunction tag(skills: SkillInfo[], tier: Tier): TieredSkill[] {\n return skills.map((s) => ({ ...s, __tier: tier }));\n}\n\nasync function detectSkillsInner(\n environments: GitRepoInfo[],\n mirrorEnabled: boolean\n): Promise<SkillInfo[]> {\n const home = homedir();\n\n const codexSystemDir = join(home, '.codex', 'skills', '.system');\n const agentsUserDir = join(home, '.agents', 'skills');\n const claudeUserDir = join(home, '.claude', 'skills');\n\n const [userClaude, pluginClaude, codexSystem, codexUser, agentsUser, pluginCodex] =\n await Promise.all([\n readSkillsFromDir(claudeUserDir),\n readPluginSkills(),\n readSkillsFromDir(codexSystemDir),\n readCodexUserSkillsDir(),\n readSkillsFromDir(agentsUserDir),\n readCodexPluginCacheSkills(),\n ]);\n\n const projectClaude: SkillInfo[] = [];\n const projectCodex: SkillInfo[] = [];\n /*\n * Sequential per-environment scan with event-loop yield. Promise.all over\n * 16+ worktrees saturated libuv's I/O thread pool — each env triggers a\n * 32-deep ancestor walk plus per-skill-dir readFile fan-out. Yielding\n * between envs trades wall-clock for event-loop responsiveness, matching\n * the orphan-pruner.ts:71-73 tradeoff.\n */\n for (const env of environments) {\n const [claude, codex] = await Promise.all([\n readSkillsFromDir(join(env.path, '.claude', 'skills')),\n readAncestorAgentsSkills(env.path),\n ]);\n projectClaude.push(...claude);\n projectCodex.push(...codex);\n await new Promise<void>((resolve) => {\n setImmediate(resolve);\n });\n }\n\n /**\n * Tier order: user > plugin > project. Within \"user\" we keep both\n * Claude- and Codex-source entries (they live in different dirs and\n * have independent name spaces in the no-collision case).\n */\n const tiered: TieredSkill[] = [\n ...tag(userClaude, 'user'),\n ...tag(codexSystem, 'user'),\n ...tag(codexUser, 'user'),\n ...tag(agentsUser, 'user'),\n ...tag(pluginClaude, 'plugin'),\n ...tag(pluginCodex, 'plugin'),\n ...tag(projectClaude, 'project'),\n ...tag(projectCodex, 'project'),\n ];\n\n /**\n * Bucket candidates by name. Same-name appearances are either:\n * (a) the same skill at different tiers (user > plugin > project) —\n * drop lower tiers without namespacing.\n * (b) different sources at the same effective scope (two plugins,\n * or one Claude + one Codex) — preserve invocable variants.\n */\n const byName = new Map<string, TieredSkill[]>();\n for (const skill of tiered) {\n const bucket = byName.get(skill.name);\n if (bucket) {\n bucket.push(skill);\n } else {\n byName.set(skill.name, [skill]);\n }\n }\n\n const resolved: SkillInfo[] = [];\n for (const [, candidates] of byName) {\n resolved.push(...resolveBucket(candidates));\n }\n\n return resolved.map((skill) => {\n const { compatibleAgents, bodyResolution } = classifySkill(skill, mirrorEnabled);\n return { ...skill, compatibleAgents, bodyResolution };\n });\n}\n\n/**\n * Resolve a single name-bucket into one or more `SkillInfo` entries.\n *\n * - Single candidate → emit as-is.\n * - Multiple candidates with one highest-tier survivor → emit the survivor.\n * - Cross-agent collision after tier collapse → one canonical entry per agent.\n * - Same-agent collision after tier collapse → one canonical entry per SDK-valid plugin id,\n * plus one bare entry for non-plugin candidates.\n */\nfunction resolveBucket(candidates: TieredSkill[]): SkillInfo[] {\n if (candidates.length === 1) {\n const only = candidates[0];\n return only ? [stripTier(only)] : [];\n }\n\n const winners = pickHighestTier(candidates);\n if (winners.length === 1) {\n const w = winners[0];\n return w ? [stripTier(w)] : [];\n }\n\n const claudeWinners = winners.filter((c) => c.sourceAgent === 'claude-code');\n const codexWinners = winners.filter((c) => c.sourceAgent === 'codex');\n\n if (claudeWinners.length > 0 && codexWinners.length > 0) {\n return resolveCrossAgentCollision(claudeWinners, codexWinners);\n }\n\n return resolveSameAgentCollision(winners);\n}\n\n/**\n * Pick the lexicographically smallest path from a list of tiered skills.\n * Stable across different scan orders so the wire shape doesn't flap\n * between daemon restarts. Skills without a `path` (produced by the\n * SKILL.md-read-failure fallback in `readSkillsFromDir`) rank last —\n * a path-bearing entry is always preferred over a pathless one.\n */\nfunction canonicalWinner(candidates: TieredSkill[]): TieredSkill | undefined {\n let best: TieredSkill | undefined;\n for (const c of candidates) {\n if (!best) {\n best = c;\n } else if (!best.path && c.path) {\n best = c;\n } else if (best.path && c.path && c.path < best.path) {\n best = c;\n }\n }\n return best;\n}\n\nfunction resolveCrossAgentCollision(\n claudeWinners: TieredSkill[],\n codexWinners: TieredSkill[]\n): SkillInfo[] {\n const out: SkillInfo[] = [];\n /**\n * Lex-smallest path wins per agent so the choice is stable across scan\n * orders (e.g. different worktree enumeration sequences under Promise.all).\n * Each winner is emitted bare unless it actually lives under a plugin —\n * `sourceAgent` already differentiates the Claude vs Codex variant in the\n * UI, so a synthetic `claude-code:` / `codex:` prefix would be both\n * redundant and invalid at the SDK boundary.\n */\n const bestClaude = canonicalWinner(claudeWinners);\n const bestCodex = canonicalWinner(codexWinners);\n if (bestClaude) out.push(applyPluginNamespace(stripTier(bestClaude)));\n if (bestCodex) out.push(applyPluginNamespace(stripTier(bestCodex)));\n return out;\n}\n\n/**\n * Stamp `namespace` only when the skill lives under a plugin install. The\n * value is the SDK-recognized plugin id (matches `Options.skills` syntax:\n * bare `name` or `plugin:name`). Synthetic categories like 'user' or\n * 'project' are never written here — see `inferPluginName` for why.\n */\nfunction applyPluginNamespace(skill: SkillInfo): SkillInfo {\n if (!skill.path) return skill;\n const pluginId = inferPluginName(skill.path);\n if (pluginId === null) return skill;\n return { ...skill, namespace: pluginId };\n}\n\n/**\n * Dedup same-agent collisions. Two distinct collapse classes:\n *\n * - Plugin collisions: two plugins ship a skill with the same name. The\n * `plugin:name` form disambiguates them on the wire AND at SDK\n * invocation, so we keep one entry per plugin id.\n * - Non-plugin dedup: 700 worktrees each carry the same project-tier\n * skill (e.g. `code-review`) and the naive \"one entry per candidate\"\n * shape ballooned the capability snapshot past the 5 MiB daemon-control\n * channel cap, bricking the composer (#composer-bricked, 2026-05-22).\n * Collapse all of them to the canonical winner; no namespace stamp,\n * because synthetic prefixes like `project:` or `user:` are NOT valid\n * SDK skill identifiers (Claude SDK accepts only bare `name` or\n * `plugin:name` — sdk.d.ts:1629).\n *\n * Canonical winner per bucket = `canonicalWinner()` (lex-smallest path;\n * path-bearing beats pathless). Pathless candidates with no resolvable\n * plugin id collapse into the same non-plugin bucket as path-bearing ones;\n * if every SKILL.md failed to read, the canonical winner is the pathless\n * fallback and the skill remains visible (just without a namespace).\n */\nfunction resolveSameAgentCollision(winners: TieredSkill[]): SkillInfo[] {\n /**\n * Bucket key: the SDK-valid plugin id when the path is plugin-provided,\n * otherwise `null` for everything else (user/project/system/pathless).\n * All non-plugin candidates share the `null` bucket and collapse to one\n * canonical entry.\n */\n const buckets = new Map<string | null, TieredSkill[]>();\n for (const candidate of winners) {\n const pluginId = candidate.path ? inferPluginName(candidate.path) : null;\n const bucket = buckets.get(pluginId);\n if (bucket) {\n bucket.push(candidate);\n } else {\n buckets.set(pluginId, [candidate]);\n }\n }\n\n const out: SkillInfo[] = [];\n for (const [pluginId, candidates] of buckets) {\n const winner = canonicalWinner(candidates);\n if (!winner) continue;\n const base = stripTier(winner);\n out.push(pluginId !== null ? { ...base, namespace: pluginId } : base);\n }\n return out;\n}\n\nfunction pickHighestTier(candidates: TieredSkill[]): TieredSkill[] {\n const order: Record<Tier, number> = { user: 0, plugin: 1, project: 2 };\n let bestRank = Number.POSITIVE_INFINITY;\n for (const c of candidates) {\n const r = order[c.__tier];\n if (r < bestRank) bestRank = r;\n }\n return candidates.filter((c) => order[c.__tier] === bestRank);\n}\n\nfunction stripTier(skill: TieredSkill): SkillInfo {\n const { __tier, ...rest } = skill;\n void __tier;\n return rest;\n}\n\n/**\n * Re-exported for unit tests in `skills.test.ts`. Underscored to flag\n * that the symbol is part of the test contract, not the public API.\n */\nexport const _resolveBucketForTesting = resolveBucket;\n","import type { SkillBodyResolution, SkillCompatibleAgent, SkillInfo } from '@shipyard/session';\n\n/**\n * Classify a detected skill's reach across agent runtimes.\n *\n * Pure function — given a `SkillInfo` and whether the user has the skill\n * mirror enabled, returns the set of agents that can invoke this skill and\n * how the body is reached (native, pass-in, mirror, unreachable).\n *\n * The detector calls this on every surviving skill after dedup and\n * overwrites `compatibleAgents` + `bodyResolution` with the result.\n *\n * Source classification:\n * - Claude-pathed (`/.claude/skills/`) — Claude resolves natively. Codex\n * can reach via `[$name](skill://path)` pass-in (see\n * `codex-rs/core-skills/src/injection.rs`).\n * - Codex-pathed (`/.codex/skills/` or `/.agents/skills/`) — Codex\n * resolves natively. Claude's SDK accepts only names from its own\n * resolver's known set; without mirror, Claude cannot invoke it.\n * - Mirror on for path-backed skills — both runtimes resolve natively\n * because symlinks make every skill present in both indexed directories.\n *\n * TODO: wire `mirrorEnabled` from `skills.mirror.enabled` user pref once\n * step 11 lands (skills-mirror subsystem).\n */\nexport function classifySkill(\n skill: SkillInfo,\n mirrorEnabled: boolean\n): { compatibleAgents: SkillCompatibleAgent[]; bodyResolution: SkillBodyResolution } {\n const path = skill.path;\n if (!path) {\n const fallback: SkillCompatibleAgent =\n skill.sourceAgent === 'codex'\n ? 'codex'\n : skill.sourceAgent === 'cursor'\n ? 'cursor'\n : 'claude-code';\n return {\n compatibleAgents: [fallback],\n bodyResolution: 'unreachable',\n };\n }\n\n if (mirrorEnabled) {\n return {\n compatibleAgents: ['claude-code', 'codex', 'cursor'],\n bodyResolution: 'mirror',\n };\n }\n\n /**\n * Cursor first — `~/.shipyard/cursor-skills/<taskId>/...` is written by\n * the daemon at spawn time; tag those before falling through to codex/claude.\n */\n if (path.includes('/cursor-skills/') || path.includes('/.cursor/')) {\n return {\n compatibleAgents: ['cursor'],\n bodyResolution: 'native',\n };\n }\n\n /**\n * Codex first — `.codex/plugins/cache/.../skills/...` would otherwise be\n * missed because it does not contain `/.codex/skills/`.\n */\n if (path.includes('/.codex/') || path.includes('/.agents/')) {\n return {\n compatibleAgents: ['codex'],\n bodyResolution: 'native',\n };\n }\n\n if (path.includes('/.claude/')) {\n return {\n compatibleAgents: ['claude-code', 'codex', 'cursor'],\n bodyResolution: 'pass-in',\n };\n }\n\n const fallback: SkillCompatibleAgent =\n skill.sourceAgent === 'codex'\n ? 'codex'\n : skill.sourceAgent === 'cursor'\n ? 'cursor'\n : 'claude-code';\n return {\n compatibleAgents: [fallback],\n bodyResolution: 'unreachable',\n };\n}\n"],"mappings":"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","names":["fallback"]}
@@ -13772,7 +13772,6 @@ function date4(params) {
13772
13772
  config(en_default());
13773
13773
 
13774
13774
  export {
13775
- toJSONSchema,
13776
13775
  external_exports
13777
13776
  };
13778
- //# sourceMappingURL=chunk-VPMN47TL.js.map
13777
+ //# sourceMappingURL=chunk-CNR7O5YH.js.map
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getShipyardHome
4
- } from "./chunk-PI77CUEP.js";
4
+ } from "./chunk-KITSAHTX.js";
5
5
  import {
6
6
  external_exports
7
- } from "./chunk-VPMN47TL.js";
7
+ } from "./chunk-CNR7O5YH.js";
8
8
 
9
9
  // src/services/bootstrap/auth.ts
10
10
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
@@ -14,6 +14,7 @@ var ShipyardConfigSchema = external_exports.object({
14
14
  token: external_exports.string(),
15
15
  userId: external_exports.string(),
16
16
  displayName: external_exports.string(),
17
+ avatarUrl: external_exports.string().nullable().optional(),
17
18
  providers: external_exports.array(external_exports.string()),
18
19
  expiresAt: external_exports.number(),
19
20
  signalingUrl: external_exports.string()
@@ -51,6 +52,7 @@ async function loadAuthToken(envOverrides) {
51
52
  token: envToken,
52
53
  userId: envOverrides?.SHIPYARD_USER_ID ?? "",
53
54
  displayName: envOverrides?.SHIPYARD_USER_DISPLAY_NAME ?? "",
55
+ avatarUrl: null,
54
56
  signalingUrl: envOverrides?.SHIPYARD_SIGNALING_URL
55
57
  };
56
58
  }
@@ -64,9 +66,22 @@ async function loadAuthToken(envOverrides) {
64
66
  token: config.auth.token,
65
67
  userId: config.auth.userId,
66
68
  displayName: config.auth.displayName,
69
+ avatarUrl: config.auth.avatarUrl ?? extractTokenAvatarUrl(config.auth.token),
67
70
  signalingUrl: config.auth.signalingUrl
68
71
  };
69
72
  }
73
+ function extractTokenAvatarUrl(token) {
74
+ try {
75
+ const payloadB64 = token.split(".")[1];
76
+ if (!payloadB64) return null;
77
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
78
+ if (payload === null || typeof payload !== "object") return null;
79
+ const avatarUrl = Reflect.get(payload, "avatarUrl");
80
+ return typeof avatarUrl === "string" ? avatarUrl : null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
70
85
 
71
86
  export {
72
87
  getConfigPath,
@@ -75,4 +90,4 @@ export {
75
90
  deleteConfig,
76
91
  loadAuthToken
77
92
  };
78
- //# sourceMappingURL=chunk-2J3WSIAF.js.map
93
+ //# sourceMappingURL=chunk-EF2DAODF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/bootstrap/auth.ts"],"sourcesContent":["import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { z } from 'zod';\nimport { getShipyardHome } from '../../shared/env.js';\n\nconst ShipyardConfigSchema = z.object({\n auth: z.object({\n token: z.string(),\n userId: z.string(),\n displayName: z.string(),\n avatarUrl: z.string().nullable().optional(),\n providers: z.array(z.string()),\n expiresAt: z.number(),\n signalingUrl: z.string(),\n }),\n});\n\nexport type ShipyardConfig = z.infer<typeof ShipyardConfigSchema>;\n\nexport function getConfigPath(): string {\n return join(getShipyardHome(), 'config.json');\n}\n\nexport async function readConfig(): Promise<ShipyardConfig | null> {\n try {\n const raw = await readFile(getConfigPath(), 'utf-8');\n return ShipyardConfigSchema.parse(JSON.parse(raw));\n } catch {\n return null;\n }\n}\n\nexport async function writeConfig(config: ShipyardConfig): Promise<void> {\n await mkdir(dirname(getConfigPath()), { recursive: true, mode: 0o700 });\n await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}\\n`, { mode: 0o600 });\n}\n\nexport async function deleteConfig(): Promise<boolean> {\n try {\n await unlink(getConfigPath());\n return true;\n } catch {\n return false;\n }\n}\n\nexport type AuthResult =\n | {\n status: 'ok';\n token: string;\n userId: string;\n displayName: string;\n avatarUrl: string | null;\n signalingUrl?: string;\n }\n | { status: 'expired' }\n | { status: 'missing' };\n\nexport interface AuthEnvOverrides {\n SHIPYARD_USER_TOKEN?: string;\n SHIPYARD_USER_ID?: string;\n SHIPYARD_USER_DISPLAY_NAME?: string;\n SHIPYARD_SIGNALING_URL?: string;\n}\n\n/**\n * Load auth token from validated env (CI override) or config file.\n */\nexport async function loadAuthToken(envOverrides?: AuthEnvOverrides): Promise<AuthResult> {\n const envToken = envOverrides?.SHIPYARD_USER_TOKEN;\n if (envToken) {\n return {\n status: 'ok',\n token: envToken,\n userId: envOverrides?.SHIPYARD_USER_ID ?? '',\n displayName: envOverrides?.SHIPYARD_USER_DISPLAY_NAME ?? '',\n avatarUrl: null,\n signalingUrl: envOverrides?.SHIPYARD_SIGNALING_URL,\n };\n }\n\n const config = await readConfig();\n if (!config?.auth?.token) return { status: 'missing' };\n\n if (config.auth.expiresAt < Date.now()) {\n return { status: 'expired' };\n }\n\n return {\n status: 'ok',\n token: config.auth.token,\n userId: config.auth.userId,\n displayName: config.auth.displayName,\n avatarUrl: config.auth.avatarUrl ?? extractTokenAvatarUrl(config.auth.token),\n signalingUrl: config.auth.signalingUrl,\n };\n}\n\nfunction extractTokenAvatarUrl(token: string): string | null {\n try {\n const payloadB64 = token.split('.')[1];\n if (!payloadB64) return null;\n const payload: unknown = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());\n if (payload === null || typeof payload !== 'object') return null;\n const avatarUrl = Reflect.get(payload, 'avatarUrl');\n return typeof avatarUrl === 'string' ? avatarUrl : null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,OAAO,UAAU,QAAQ,iBAAiB;AACnD,SAAS,SAAS,YAAY;AAI9B,IAAM,uBAAuB,iBAAE,OAAO;AAAA,EACpC,MAAM,iBAAE,OAAO;AAAA,IACb,OAAO,iBAAE,OAAO;AAAA,IAChB,QAAQ,iBAAE,OAAO;AAAA,IACjB,aAAa,iBAAE,OAAO;AAAA,IACtB,WAAW,iBAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC1C,WAAW,iBAAE,MAAM,iBAAE,OAAO,CAAC;AAAA,IAC7B,WAAW,iBAAE,OAAO;AAAA,IACpB,cAAc,iBAAE,OAAO;AAAA,EACzB,CAAC;AACH,CAAC;AAIM,SAAS,gBAAwB;AACtC,SAAO,KAAK,gBAAgB,GAAG,aAAa;AAC9C;AAEA,eAAsB,aAA6C;AACjE,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,cAAc,GAAG,OAAO;AACnD,WAAO,qBAAqB,MAAM,KAAK,MAAM,GAAG,CAAC;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,YAAY,QAAuC;AACvE,QAAM,MAAM,QAAQ,cAAc,CAAC,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACtE,QAAM,UAAU,cAAc,GAAG,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAC1F;AAEA,eAAsB,eAAiC;AACrD,MAAI;AACF,UAAM,OAAO,cAAc,CAAC;AAC5B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAwBA,eAAsB,cAAc,cAAsD;AACxF,QAAM,WAAW,cAAc;AAC/B,MAAI,UAAU;AACZ,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,QAAQ,cAAc,oBAAoB;AAAA,MAC1C,aAAa,cAAc,8BAA8B;AAAA,MACzD,WAAW;AAAA,MACX,cAAc,cAAc;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,WAAW;AAChC,MAAI,CAAC,QAAQ,MAAM,MAAO,QAAO,EAAE,QAAQ,UAAU;AAErD,MAAI,OAAO,KAAK,YAAY,KAAK,IAAI,GAAG;AACtC,WAAO,EAAE,QAAQ,UAAU;AAAA,EAC7B;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO,OAAO,KAAK;AAAA,IACnB,QAAQ,OAAO,KAAK;AAAA,IACpB,aAAa,OAAO,KAAK;AAAA,IACzB,WAAW,OAAO,KAAK,aAAa,sBAAsB,OAAO,KAAK,KAAK;AAAA,IAC3E,cAAc,OAAO,KAAK;AAAA,EAC5B;AACF;AAEA,SAAS,sBAAsB,OAA8B;AAC3D,MAAI;AACF,UAAM,aAAa,MAAM,MAAM,GAAG,EAAE,CAAC;AACrC,QAAI,CAAC,WAAY,QAAO;AACxB,UAAM,UAAmB,KAAK,MAAM,OAAO,KAAK,YAAY,WAAW,EAAE,SAAS,CAAC;AACnF,QAAI,YAAY,QAAQ,OAAO,YAAY,SAAU,QAAO;AAC5D,UAAM,YAAY,QAAQ,IAAI,SAAS,WAAW;AAClD,WAAO,OAAO,cAAc,WAAW,YAAY;AAAA,EACrD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}