skilld 0.0.1 → 0.1.2

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -88
  3. package/dist/_chunks/config.mjs +20 -0
  4. package/dist/_chunks/config.mjs.map +1 -0
  5. package/dist/_chunks/llm.mjs +877 -0
  6. package/dist/_chunks/llm.mjs.map +1 -0
  7. package/dist/_chunks/releases.mjs +986 -0
  8. package/dist/_chunks/releases.mjs.map +1 -0
  9. package/dist/_chunks/storage.mjs +198 -0
  10. package/dist/_chunks/storage.mjs.map +1 -0
  11. package/dist/_chunks/sync-parallel.mjs +540 -0
  12. package/dist/_chunks/sync-parallel.mjs.map +1 -0
  13. package/dist/_chunks/types.d.mts +87 -0
  14. package/dist/_chunks/types.d.mts.map +1 -0
  15. package/dist/_chunks/utils.d.mts +352 -0
  16. package/dist/_chunks/utils.d.mts.map +1 -0
  17. package/dist/_chunks/version.d.mts +147 -0
  18. package/dist/_chunks/version.d.mts.map +1 -0
  19. package/dist/agent/index.d.mts +205 -0
  20. package/dist/agent/index.d.mts.map +1 -0
  21. package/dist/agent/index.mjs +2 -0
  22. package/dist/cache/index.d.mts +2 -0
  23. package/dist/cache/index.mjs +3 -0
  24. package/dist/cli.mjs +2650 -449
  25. package/dist/cli.mjs.map +1 -1
  26. package/dist/index.d.mts +5 -14
  27. package/dist/index.mjs +7 -181
  28. package/dist/retriv/index.d.mts +12 -0
  29. package/dist/retriv/index.d.mts.map +1 -0
  30. package/dist/retriv/index.mjs +76 -0
  31. package/dist/retriv/index.mjs.map +1 -0
  32. package/dist/sources/index.d.mts +2 -0
  33. package/dist/sources/index.mjs +3 -0
  34. package/dist/types.d.mts +4 -37
  35. package/package.json +39 -13
  36. package/dist/agents.d.mts +0 -56
  37. package/dist/agents.d.mts.map +0 -1
  38. package/dist/agents.mjs +0 -148
  39. package/dist/agents.mjs.map +0 -1
  40. package/dist/index.d.mts.map +0 -1
  41. package/dist/index.mjs.map +0 -1
  42. package/dist/npm.d.mts +0 -48
  43. package/dist/npm.d.mts.map +0 -1
  44. package/dist/npm.mjs +0 -90
  45. package/dist/npm.mjs.map +0 -1
  46. package/dist/split-text.d.mts +0 -24
  47. package/dist/split-text.d.mts.map +0 -1
  48. package/dist/split-text.mjs +0 -87
  49. package/dist/split-text.mjs.map +0 -1
  50. package/dist/types.d.mts.map +0 -1
package/dist/cli.mjs CHANGED
@@ -1,503 +1,2704 @@
1
1
  #!/usr/bin/env node
2
- import { generateSkill } from "./index.mjs";
3
- import { getInstalledSkillVersion, readLocalDependencies, resolvePackageDocs } from "./npm.mjs";
4
- import { agents, detectCurrentAgent, generateSkillMd, installSkillForAgents, sanitizeName } from "./agents.mjs";
5
- import { join } from "node:path";
6
- import { defineCommand, runMain } from "citty";
7
- import consola from "consola";
2
+ import { a as getCacheDir, i as getPackageDbPath, n as REFERENCES_DIR, s as getVersionKey, t as CACHE_DIR } from "./_chunks/config.mjs";
3
+ import { _ as writeToCache, a as getShippedSkills, c as linkGithub, d as linkReleases, f as linkShippedSkill, g as resolvePkgDir, h as readCachedDocs, i as getPkgKeyFiles, l as linkPkg, m as listReferenceFiles, n as clearCache, o as hasShippedDocs, r as ensureCacheDir, s as isCached, u as linkReferences } from "./_chunks/storage.mjs";
4
+ import "./cache/index.mjs";
5
+ import { createIndex, searchSnippets } from "./retriv/index.mjs";
6
+ import { A as resolveEntryFiles, E as parseGitHubUrl, F as isGhAvailable, M as formatDiscussionsAsMarkdown, N as fetchGitHubIssues, P as formatIssuesAsMarkdown, S as fetchReadmeContent, _ as normalizeLlmsLinks, a as fetchPkgDist, c as readLocalDependencies, d as resolvePackageDocs, f as resolvePackageDocsWithAttempts, h as fetchLlmsTxt, i as fetchNpmRegistryMeta, j as fetchGitHubDiscussions, n as fetchLatestVersion, p as downloadLlmsDocs, r as fetchNpmPackage, t as fetchReleaseNotes, u as resolveLocalPackageDocs, y as fetchGitDocs } from "./_chunks/releases.mjs";
7
+ import "./sources/index.mjs";
8
+ import { c as sanitizeName, d as detectTargetAgent, f as getAgentVersion, i as generateSkillMd, l as detectImportedPackages, n as getModelName, p as agents, r as optimizeDocs, t as getAvailableModels, u as detectInstalledAgents } from "./_chunks/llm.mjs";
9
+ import "./agent/index.mjs";
10
+ import { createRequire } from "node:module";
11
+ import { homedir } from "node:os";
12
+ import { join, relative, resolve } from "node:path";
13
+ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
8
14
  import { execSync } from "node:child_process";
9
- async function getAvailableModels() {
10
- const models = [];
11
- if ((() => {
15
+ import * as p from "@clack/prompts";
16
+ import { defineCommand, runMain } from "citty";
17
+ import pLimit from "p-limit";
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+ const defaultFeatures = {
20
+ search: true,
21
+ issues: false,
22
+ discussions: false,
23
+ releases: true
24
+ };
25
+ const CONFIG_DIR = join(homedir(), ".skilld");
26
+ const CONFIG_PATH = join(CONFIG_DIR, "config.yaml");
27
+ function hasConfig() {
28
+ return existsSync(CONFIG_PATH);
29
+ }
30
+ function readConfig() {
31
+ if (!existsSync(CONFIG_PATH)) return {};
32
+ const content = readFileSync(CONFIG_PATH, "utf-8");
33
+ const config = {};
34
+ let inBlock = null;
35
+ const projects = [];
36
+ const features = {};
37
+ for (const line of content.split("\n")) {
38
+ if (line.startsWith("projects:")) {
39
+ inBlock = "projects";
40
+ continue;
41
+ }
42
+ if (line.startsWith("features:")) {
43
+ inBlock = "features";
44
+ continue;
45
+ }
46
+ if (inBlock === "projects") {
47
+ if (line.startsWith(" - ")) {
48
+ projects.push(line.slice(4).trim().replace(/^["']|["']$/g, ""));
49
+ continue;
50
+ }
51
+ inBlock = null;
52
+ }
53
+ if (inBlock === "features") {
54
+ const m = line.match(/^ {2}(\w+):\s*(.+)/);
55
+ if (m) {
56
+ const key = m[1];
57
+ if (key in defaultFeatures) features[key] = m[2] === "true";
58
+ continue;
59
+ }
60
+ inBlock = null;
61
+ }
62
+ const [key, ...rest] = line.split(":");
63
+ const value = rest.join(":").trim().replace(/^["']|["']$/g, "");
64
+ if (key === "model" && value) config.model = value;
65
+ if (key === "agent" && value) config.agent = value;
66
+ if (key === "skipLlm") config.skipLlm = value === "true";
67
+ }
68
+ if (projects.length > 0) config.projects = projects;
69
+ if (Object.keys(features).length > 0) config.features = {
70
+ ...defaultFeatures,
71
+ ...features
72
+ };
73
+ return config;
74
+ }
75
+ function writeConfig(config) {
76
+ mkdirSync(CONFIG_DIR, { recursive: true });
77
+ let yaml = "";
78
+ if (config.model) yaml += `model: ${config.model}\n`;
79
+ if (config.agent) yaml += `agent: ${config.agent}\n`;
80
+ if (config.skipLlm) yaml += `skipLlm: true\n`;
81
+ if (config.features) {
82
+ yaml += "features:\n";
83
+ for (const [k, v] of Object.entries(config.features)) yaml += ` ${k}: ${v}\n`;
84
+ }
85
+ if (config.projects?.length) {
86
+ yaml += "projects:\n";
87
+ for (const p of config.projects) yaml += ` - ${p}\n`;
88
+ }
89
+ writeFileSync(CONFIG_PATH, yaml);
90
+ }
91
+ function updateConfig(updates) {
92
+ writeConfig({
93
+ ...readConfig(),
94
+ ...updates
95
+ });
96
+ }
97
+ function registerProject(projectPath) {
98
+ const config = readConfig();
99
+ const projects = new Set(config.projects || []);
100
+ projects.add(projectPath);
101
+ writeConfig({
102
+ ...config,
103
+ projects: [...projects]
104
+ });
105
+ }
106
+ function unregisterProject(projectPath) {
107
+ const config = readConfig();
108
+ const projects = (config.projects || []).filter((p) => p !== projectPath);
109
+ writeConfig({
110
+ ...config,
111
+ projects
112
+ });
113
+ }
114
+ function getRegisteredProjects() {
115
+ return readConfig().projects || [];
116
+ }
117
+ async function configCommand() {
118
+ const config = readConfig();
119
+ const features = config.features ?? defaultFeatures;
120
+ const enabledCount = Object.values(features).filter(Boolean).length;
121
+ const action = await p.select({
122
+ message: "Settings",
123
+ options: [
124
+ {
125
+ label: "Change features",
126
+ value: "features",
127
+ hint: `${enabledCount}/4 enabled`
128
+ },
129
+ {
130
+ label: "Change model",
131
+ value: "model",
132
+ hint: config.model || "auto"
133
+ },
134
+ {
135
+ label: "Change agent",
136
+ value: "agent",
137
+ hint: config.agent || "auto-detect"
138
+ }
139
+ ]
140
+ });
141
+ if (p.isCancel(action)) {
142
+ p.cancel("Cancelled");
143
+ return;
144
+ }
145
+ switch (action) {
146
+ case "features": {
147
+ const selected = await p.multiselect({
148
+ message: "Enable features",
149
+ options: [
150
+ {
151
+ label: "Semantic + token search",
152
+ value: "search",
153
+ hint: "local query engine to cut token costs and speed up grep"
154
+ },
155
+ {
156
+ label: "Release notes",
157
+ value: "releases",
158
+ hint: "track changelogs for installed packages"
159
+ },
160
+ {
161
+ label: "GitHub issues",
162
+ value: "issues",
163
+ hint: "surface common problems and solutions"
164
+ },
165
+ {
166
+ label: "GitHub discussions",
167
+ value: "discussions",
168
+ hint: "include Q&A and community knowledge"
169
+ }
170
+ ].map((f) => ({
171
+ label: f.label,
172
+ value: f.value,
173
+ hint: f.hint
174
+ })),
175
+ initialValues: Object.entries(features).filter(([, v]) => v).map(([k]) => k),
176
+ required: false
177
+ });
178
+ if (p.isCancel(selected)) return;
179
+ updateConfig({ features: {
180
+ search: selected.includes("search"),
181
+ issues: selected.includes("issues"),
182
+ discussions: selected.includes("discussions"),
183
+ releases: selected.includes("releases")
184
+ } });
185
+ p.log.success(`Features updated: ${selected.length} enabled`);
186
+ break;
187
+ }
188
+ case "model": {
189
+ const available = await getAvailableModels();
190
+ if (available.length === 0) {
191
+ p.log.warn("No LLM CLIs found");
192
+ return;
193
+ }
194
+ const model = await p.select({
195
+ message: "Select default model",
196
+ options: [{
197
+ label: "Auto (prompt each time)",
198
+ value: ""
199
+ }, ...available.map((m) => ({
200
+ label: m.recommended ? `${m.name} (Recommended)` : m.name,
201
+ value: m.id,
202
+ hint: m.hint
203
+ }))],
204
+ initialValue: config.model || ""
205
+ });
206
+ if (p.isCancel(model)) return;
207
+ updateConfig({ model: model || void 0 });
208
+ p.log.success(model ? `Default model set to ${model}` : "Model will be prompted each time");
209
+ break;
210
+ }
211
+ case "agent": {
212
+ const agentChoice = await p.select({
213
+ message: "Select default agent",
214
+ options: [{
215
+ label: "Auto-detect",
216
+ value: ""
217
+ }, ...Object.entries(agents).map(([id, a]) => ({
218
+ label: a.displayName,
219
+ value: id,
220
+ hint: a.skillsDir
221
+ }))],
222
+ initialValue: config.agent || ""
223
+ });
224
+ if (p.isCancel(agentChoice)) return;
225
+ updateConfig({ agent: agentChoice || void 0 });
226
+ p.log.success(agentChoice ? `Default agent set to ${agentChoice}` : "Agent will be auto-detected");
227
+ break;
228
+ }
229
+ }
230
+ }
231
+ function parseSkillFrontmatter(skillPath) {
232
+ if (!existsSync(skillPath)) return null;
233
+ const match = readFileSync(skillPath, "utf-8").match(/^---\n([\s\S]*?)\n---/);
234
+ if (!match) return null;
235
+ const info = {};
236
+ const lines = match[1].split("\n");
237
+ for (const line of lines) {
238
+ const [key, ...rest] = line.split(":");
239
+ const value = rest.join(":").trim().replace(/^["']|["']$/g, "");
240
+ if (key === "packageName") info.packageName = value;
241
+ if (key === "version") info.version = value;
242
+ if (key === "source") info.source = value;
243
+ if (key === "syncedAt") info.syncedAt = value;
244
+ if (key === "generator") info.generator = value;
245
+ }
246
+ return info;
247
+ }
248
+ function readLock(skillsDir) {
249
+ const lockPath = join(skillsDir, "skilld-lock.yaml");
250
+ if (!existsSync(lockPath)) return null;
251
+ const content = readFileSync(lockPath, "utf-8");
252
+ const skills = {};
253
+ let currentSkill = null;
254
+ for (const line of content.split("\n")) {
255
+ const skillMatch = line.match(/^ {2}(\S+):$/);
256
+ if (skillMatch) {
257
+ currentSkill = skillMatch[1];
258
+ skills[currentSkill] = {};
259
+ continue;
260
+ }
261
+ if (currentSkill && line.startsWith(" ")) {
262
+ const [key, ...rest] = line.trim().split(":");
263
+ const value = rest.join(":").trim().replace(/^["']|["']$/g, "");
264
+ if (key && value) skills[currentSkill][key] = value;
265
+ }
266
+ }
267
+ return { skills };
268
+ }
269
+ function writeLock(skillsDir, skillName, info) {
270
+ const lockPath = join(skillsDir, "skilld-lock.yaml");
271
+ let lock = { skills: {} };
272
+ if (existsSync(lockPath)) lock = readLock(skillsDir) || { skills: {} };
273
+ lock.skills[skillName] = info;
274
+ let yaml = "skills:\n";
275
+ for (const [name, skill] of Object.entries(lock.skills)) {
276
+ yaml += ` ${name}:\n`;
277
+ if (skill.packageName) yaml += ` packageName: "${skill.packageName}"\n`;
278
+ if (skill.version) yaml += ` version: "${skill.version}"\n`;
279
+ if (skill.source) yaml += ` source: "${skill.source}"\n`;
280
+ if (skill.syncedAt) yaml += ` syncedAt: "${skill.syncedAt}"\n`;
281
+ if (skill.generator) yaml += ` generator: "${skill.generator}"\n`;
282
+ }
283
+ writeFileSync(lockPath, yaml);
284
+ }
285
+ function removeLockEntry(skillsDir, skillName) {
286
+ const lockPath = join(skillsDir, "skilld-lock.yaml");
287
+ const lock = readLock(skillsDir);
288
+ if (!lock) return;
289
+ delete lock.skills[skillName];
290
+ if (Object.keys(lock.skills).length === 0) {
291
+ unlinkSync(lockPath);
292
+ return;
293
+ }
294
+ let yaml = "skills:\n";
295
+ for (const [name, skill] of Object.entries(lock.skills)) {
296
+ yaml += ` ${name}:\n`;
297
+ if (skill.packageName) yaml += ` packageName: "${skill.packageName}"\n`;
298
+ if (skill.version) yaml += ` version: "${skill.version}"\n`;
299
+ if (skill.source) yaml += ` source: "${skill.source}"\n`;
300
+ if (skill.syncedAt) yaml += ` syncedAt: "${skill.syncedAt}"\n`;
301
+ if (skill.generator) yaml += ` generator: "${skill.generator}"\n`;
302
+ }
303
+ writeFileSync(lockPath, yaml);
304
+ }
305
+ async function installCommand(opts) {
306
+ const cwd = process.cwd();
307
+ const agent = agents[opts.agent];
308
+ const skillsDir = opts.global ? join(__require("node:os").homedir(), ".skilld", "skills") : join(cwd, agent.skillsDir);
309
+ const lock = readLock(skillsDir);
310
+ if (!lock || Object.keys(lock.skills).length === 0) {
311
+ p.log.warn("No skilld-lock.yaml found. Run `skilld` to sync skills first.");
312
+ return;
313
+ }
314
+ const skills = Object.entries(lock.skills);
315
+ const toRestore = [];
316
+ for (const [name, info] of skills) {
317
+ if (!info.version) continue;
318
+ if (info.source === "shipped") {
319
+ if (!existsSync(join(skillsDir, name))) toRestore.push({
320
+ name,
321
+ info
322
+ });
323
+ continue;
324
+ }
325
+ const skillDir = join(skillsDir, name);
326
+ const referencesPath = join(skillDir, ".skilld");
327
+ const skillMdPath = join(skillDir, "SKILL.md");
328
+ if (!existsSync(skillDir) || !existsSync(skillMdPath) || !existsSync(referencesPath) || lstatSync(referencesPath).isSymbolicLink() && !existsSync(referencesPath) || existsSync(skillMdPath) && lstatSync(skillMdPath).isSymbolicLink() && !existsSync(skillMdPath)) toRestore.push({
329
+ name,
330
+ info
331
+ });
332
+ }
333
+ if (toRestore.length === 0) {
334
+ p.log.success("All references already linked");
335
+ return;
336
+ }
337
+ p.log.info(`Restoring ${toRestore.length} references`);
338
+ ensureCacheDir();
339
+ for (const { name, info } of toRestore) {
340
+ const version = info.version;
341
+ const pkgName = info.packageName || unsanitizeName(name, info.source);
342
+ if (info.source === "shipped") {
343
+ const match = getShippedSkills(pkgName, cwd, version).find((s) => s.skillName === name);
344
+ if (match) {
345
+ linkShippedSkill(skillsDir, name, match.skillDir);
346
+ p.log.success(`Linked ${name}`);
347
+ } else p.log.warn(`${name}: package ${pkgName} no longer ships this skill`);
348
+ continue;
349
+ }
350
+ const skillDir = join(skillsDir, name);
351
+ const referencesPath = join(skillDir, ".skilld");
352
+ const globalCachePath = getCacheDir(pkgName, version);
353
+ const spin = p.spinner();
354
+ if (isCached(pkgName, version)) {
355
+ spin.start(`Linking ${name}`);
356
+ mkdirSync(skillDir, { recursive: true });
357
+ mkdirSync(referencesPath, { recursive: true });
358
+ linkPkgSymlink(referencesPath, pkgName, cwd, version);
359
+ if (!pkgHasShippedDocs(pkgName, cwd, version) && !isReadmeOnly(globalCachePath)) {
360
+ const docsLink = join(referencesPath, "docs");
361
+ const cachedDocs = join(globalCachePath, "docs");
362
+ if (existsSync(docsLink)) unlinkSync(docsLink);
363
+ if (existsSync(cachedDocs)) symlinkSync(cachedDocs, docsLink, "junction");
364
+ }
365
+ const githubLink = join(referencesPath, "github");
366
+ const cachedGithub = join(globalCachePath, "github");
367
+ if (existsSync(githubLink)) unlinkSync(githubLink);
368
+ if (existsSync(cachedGithub)) symlinkSync(cachedGithub, githubLink, "junction");
369
+ const releasesLink = join(referencesPath, "releases");
370
+ const cachedReleases = join(globalCachePath, "releases");
371
+ if (existsSync(releasesLink)) unlinkSync(releasesLink);
372
+ if (existsSync(cachedReleases)) symlinkSync(cachedReleases, releasesLink, "junction");
373
+ spin.stop(`Linked ${name}`);
374
+ continue;
375
+ }
376
+ spin.start(`Downloading ${name}@${version}`);
377
+ const resolved = await resolvePackageDocs(pkgName, { version });
378
+ if (!resolved) {
379
+ spin.stop(`Could not resolve: ${name}`);
380
+ continue;
381
+ }
382
+ const cachedDocs = [];
383
+ const docsToIndex = [];
384
+ if (resolved.gitDocsUrl && resolved.repoUrl) {
385
+ const gh = parseGitHubUrl(resolved.repoUrl);
386
+ if (gh) {
387
+ const gitDocs = await fetchGitDocs(gh.owner, gh.repo, version, pkgName);
388
+ if (gitDocs?.files.length) {
389
+ const BATCH_SIZE = 20;
390
+ for (let i = 0; i < gitDocs.files.length; i += BATCH_SIZE) {
391
+ const batch = gitDocs.files.slice(i, i + BATCH_SIZE);
392
+ const results = await Promise.all(batch.map(async (file) => {
393
+ const url = `${gitDocs.baseUrl}/${file}`;
394
+ const res = await fetch(url, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
395
+ if (!res?.ok) return null;
396
+ return {
397
+ file,
398
+ content: await res.text()
399
+ };
400
+ }));
401
+ for (const r of results) if (r) {
402
+ const cachePath = gitDocs.docsPrefix ? r.file.replace(gitDocs.docsPrefix, "") : r.file;
403
+ cachedDocs.push({
404
+ path: cachePath,
405
+ content: r.content
406
+ });
407
+ docsToIndex.push({
408
+ id: cachePath,
409
+ content: r.content,
410
+ metadata: {
411
+ package: pkgName,
412
+ source: cachePath,
413
+ type: "doc"
414
+ }
415
+ });
416
+ }
417
+ }
418
+ }
419
+ }
420
+ }
421
+ if (resolved.llmsUrl && cachedDocs.length === 0) {
422
+ const llmsContent = await fetchLlmsTxt(resolved.llmsUrl);
423
+ if (llmsContent) {
424
+ cachedDocs.push({
425
+ path: "llms.txt",
426
+ content: normalizeLlmsLinks(llmsContent.raw)
427
+ });
428
+ if (llmsContent.links.length > 0) {
429
+ const docs = await downloadLlmsDocs(llmsContent, resolved.docsUrl || new URL(resolved.llmsUrl).origin);
430
+ for (const doc of docs) {
431
+ const cachePath = join("docs", ...(doc.url.startsWith("/") ? doc.url.slice(1) : doc.url).split("/"));
432
+ cachedDocs.push({
433
+ path: cachePath,
434
+ content: doc.content
435
+ });
436
+ docsToIndex.push({
437
+ id: doc.url,
438
+ content: doc.content,
439
+ metadata: {
440
+ package: pkgName,
441
+ source: cachePath,
442
+ type: "doc"
443
+ }
444
+ });
445
+ }
446
+ }
447
+ }
448
+ }
449
+ if (resolved.readmeUrl && cachedDocs.length === 0) {
450
+ const content = await fetchReadmeContent(resolved.readmeUrl);
451
+ if (content) {
452
+ cachedDocs.push({
453
+ path: "docs/README.md",
454
+ content
455
+ });
456
+ docsToIndex.push({
457
+ id: "README.md",
458
+ content,
459
+ metadata: {
460
+ package: pkgName,
461
+ source: "docs/README.md",
462
+ type: "doc"
463
+ }
464
+ });
465
+ }
466
+ }
467
+ if (cachedDocs.length > 0) {
468
+ writeToCache(pkgName, version, cachedDocs);
469
+ mkdirSync(referencesPath, { recursive: true });
470
+ linkPkgSymlink(referencesPath, pkgName, cwd, version);
471
+ if (!isReadmeOnly(globalCachePath)) {
472
+ const docsLink = join(referencesPath, "docs");
473
+ const cachedDocsDir = join(globalCachePath, "docs");
474
+ if (existsSync(docsLink)) unlinkSync(docsLink);
475
+ if (existsSync(cachedDocsDir)) symlinkSync(cachedDocsDir, docsLink, "junction");
476
+ }
477
+ if (docsToIndex.length > 0) await createIndex(docsToIndex, { dbPath: getPackageDbPath(pkgName, version) });
478
+ const pkgDir = resolvePkgDir(pkgName, cwd, version);
479
+ const entryFiles = pkgDir ? await resolveEntryFiles(pkgDir) : [];
480
+ if (entryFiles.length > 0) await createIndex(entryFiles.map((e) => ({
481
+ id: e.path,
482
+ content: e.content,
483
+ metadata: {
484
+ package: pkgName,
485
+ source: `pkg/${e.path}`,
486
+ type: e.type
487
+ }
488
+ })), { dbPath: getPackageDbPath(pkgName, version) });
489
+ spin.stop(`Downloaded and linked ${name}`);
490
+ } else spin.stop(`No docs found for ${name}`);
491
+ }
492
+ p.outro("Install complete");
493
+ }
494
+ function unsanitizeName(sanitized, source) {
495
+ if (source?.includes("ungh://")) {
496
+ const match = source.match(/ungh:\/\/([^/]+)\/(.+)/);
497
+ if (match) return `@${match[1]}/${match[2]}`;
498
+ }
499
+ if (sanitized.startsWith("antfu-")) return `@antfu/${sanitized.slice(6)}`;
500
+ if (sanitized.startsWith("clack-")) return `@clack/${sanitized.slice(6)}`;
501
+ if (sanitized.startsWith("nuxt-")) return `@nuxt/${sanitized.slice(5)}`;
502
+ if (sanitized.startsWith("vue-")) return `@vue/${sanitized.slice(4)}`;
503
+ if (sanitized.startsWith("vueuse-")) return `@vueuse/${sanitized.slice(7)}`;
504
+ return sanitized;
505
+ }
506
+ function linkPkgSymlink(referencesDir, name, cwd, version) {
507
+ const pkgPath = resolvePkgDir(name, cwd, version);
508
+ if (!pkgPath) return;
509
+ const pkgLink = join(referencesDir, "pkg");
510
+ if (existsSync(pkgLink)) unlinkSync(pkgLink);
511
+ symlinkSync(pkgPath, pkgLink, "junction");
512
+ }
513
+ function isReadmeOnly(cacheDir) {
514
+ const docsDir = join(cacheDir, "docs");
515
+ if (!existsSync(docsDir)) return false;
516
+ const files = readdirSync(docsDir);
517
+ return files.length === 1 && files[0] === "README.md";
518
+ }
519
+ function pkgHasShippedDocs(name, cwd, version) {
520
+ const pkgPath = resolvePkgDir(name, cwd, version);
521
+ if (!pkgPath) return false;
522
+ for (const candidate of [
523
+ "docs",
524
+ "documentation",
525
+ "doc"
526
+ ]) if (existsSync(join(pkgPath, candidate))) return true;
527
+ return false;
528
+ }
529
+ function* iterateSkills(opts = {}) {
530
+ const { scope = "all", cwd = process.cwd() } = opts;
531
+ const agentTypes = opts.agents ?? Object.keys(agents);
532
+ for (const agentType of agentTypes) {
533
+ const agent = agents[agentType];
534
+ if (scope === "local" || scope === "all") {
535
+ const localDir = join(cwd, agent.skillsDir);
536
+ if (existsSync(localDir)) {
537
+ const lock = readLock(localDir);
538
+ const entries = readdirSync(localDir).filter((f) => !f.startsWith(".") && f !== "skilld-lock.yaml");
539
+ for (const name of entries) {
540
+ const dir = join(localDir, name);
541
+ if (lock?.skills[name]) yield {
542
+ name,
543
+ dir,
544
+ agent: agentType,
545
+ info: lock.skills[name],
546
+ scope: "local"
547
+ };
548
+ else {
549
+ const info = parseSkillFrontmatter(join(dir, "_SKILL.md"));
550
+ if (info?.generator === "skilld") yield {
551
+ name,
552
+ dir,
553
+ agent: agentType,
554
+ info,
555
+ scope: "local"
556
+ };
557
+ }
558
+ }
559
+ }
560
+ }
561
+ if ((scope === "global" || scope === "all") && agent.globalSkillsDir) {
562
+ const globalDir = agent.globalSkillsDir;
563
+ if (existsSync(globalDir)) {
564
+ const lock = readLock(globalDir);
565
+ const entries = readdirSync(globalDir).filter((f) => !f.startsWith(".") && f !== "skilld-lock.yaml");
566
+ for (const name of entries) {
567
+ const dir = join(globalDir, name);
568
+ if (lock?.skills[name]) yield {
569
+ name,
570
+ dir,
571
+ agent: agentType,
572
+ info: lock.skills[name],
573
+ scope: "global"
574
+ };
575
+ else {
576
+ const info = parseSkillFrontmatter(join(dir, "_SKILL.md"));
577
+ if (info?.generator === "skilld") yield {
578
+ name,
579
+ dir,
580
+ agent: agentType,
581
+ info,
582
+ scope: "global"
583
+ };
584
+ }
585
+ }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ function isOutdated(skill, depVersion) {
591
+ if (!skill.info?.version) return true;
592
+ return skill.info.version.split(".").slice(0, 2).join(".") !== depVersion.replace(/^[\^~]/, "").split(".").slice(0, 2).join(".");
593
+ }
594
+ async function getProjectState(cwd = process.cwd()) {
595
+ const skills = [...iterateSkills({
596
+ scope: "local",
597
+ cwd
598
+ })];
599
+ const localDeps = await readLocalDependencies(cwd).catch(() => []);
600
+ const deps = new Map(localDeps.map((d) => [d.name, d.version]));
601
+ const skillByName = new Map(skills.map((s) => [s.name, s]));
602
+ const skillByPkgName = /* @__PURE__ */ new Map();
603
+ for (const s of skills) if (s.info?.packageName) skillByPkgName.set(s.info.packageName, s);
604
+ const missing = [];
605
+ const outdated = [];
606
+ const synced = [];
607
+ const matchedSkillNames = /* @__PURE__ */ new Set();
608
+ for (const [pkgName, version] of deps) {
609
+ const normalizedName = pkgName.replace(/^@/, "").replace(/\//g, "-");
610
+ const skill = skillByName.get(normalizedName) || skillByName.get(pkgName) || skillByPkgName.get(pkgName);
611
+ if (!skill) missing.push(pkgName);
612
+ else {
613
+ matchedSkillNames.add(skill.name);
614
+ if (isOutdated(skill, version)) outdated.push({
615
+ ...skill,
616
+ packageName: pkgName,
617
+ latestVersion: version
618
+ });
619
+ else synced.push({
620
+ ...skill,
621
+ packageName: pkgName,
622
+ latestVersion: version
623
+ });
624
+ }
625
+ }
626
+ return {
627
+ skills,
628
+ deps,
629
+ missing,
630
+ outdated,
631
+ synced,
632
+ unmatched: skills.filter((s) => !matchedSkillNames.has(s.name))
633
+ };
634
+ }
635
+ function getSkillsDir(agent, scope, cwd = process.cwd()) {
636
+ const agentConfig = agents[agent];
637
+ if (scope === "global") {
638
+ if (!agentConfig.globalSkillsDir) throw new Error(`Agent ${agent} does not support global skills`);
639
+ return agentConfig.globalSkillsDir;
640
+ }
641
+ return join(cwd, agentConfig.skillsDir);
642
+ }
643
+ async function removeCommand(state, opts) {
644
+ const scope = opts.global ? "global" : "local";
645
+ const allSkills = [...iterateSkills({ scope })];
646
+ const skills = opts.packages ? allSkills.filter((s) => opts.packages.includes(s.name)) : await pickSkillsToRemove(allSkills, scope);
647
+ if (!skills || skills.length === 0) {
648
+ p.log.info("No skills selected");
649
+ return;
650
+ }
651
+ if (!opts.yes) {
652
+ const confirmed = await p.confirm({ message: `Remove ${skills.length} skill(s)? ${skills.map((s) => s.name).join(", ")}` });
653
+ if (p.isCancel(confirmed) || !confirmed) {
654
+ p.cancel("Cancelled");
655
+ return;
656
+ }
657
+ }
658
+ for (const skill of skills) {
659
+ const skillsDir = getSkillsDir(skill.agent, skill.scope);
660
+ if (existsSync(skill.dir)) {
661
+ rmSync(skill.dir, {
662
+ recursive: true,
663
+ force: true
664
+ });
665
+ removeLockEntry(skillsDir, skill.name);
666
+ p.log.success(`Removed ${skill.name}`);
667
+ } else p.log.warn(`${skill.name} not found`);
668
+ }
669
+ p.outro(`Removed ${skills.length} skill(s)`);
670
+ }
671
+ async function pickSkillsToRemove(skills, scope) {
672
+ if (skills.length === 0) {
673
+ p.log.warn(`No ${scope} skills installed`);
674
+ return null;
675
+ }
676
+ const options = skills.map((skill) => ({
677
+ label: skill.name,
678
+ value: skill.name,
679
+ hint: skill.info?.version ? `@${skill.info.version}` : void 0
680
+ }));
681
+ const selected = await p.multiselect({
682
+ message: "Select skills to remove",
683
+ options,
684
+ required: false
685
+ });
686
+ if (p.isCancel(selected)) {
687
+ p.cancel("Cancelled");
688
+ return null;
689
+ }
690
+ const selectedSet = new Set(selected);
691
+ return skills.filter((s) => selectedSet.has(s.name));
692
+ }
693
+ function highlightTerms(content, terms) {
694
+ if (terms.length === 0) return content;
695
+ const sorted = [...terms].sort((a, b) => b.length - a.length);
696
+ const pattern = new RegExp(`(${sorted.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "gi");
697
+ return content.replace(pattern, "\x1B[33m$1\x1B[0m");
698
+ }
699
+ function formatSnippet(r) {
700
+ const refPath = `.claude/skills/${r.package}/.skilld/${r.source}`;
701
+ const lineRange = r.lineStart === r.lineEnd ? `L${r.lineStart}` : `L${r.lineStart}-${r.lineEnd}`;
702
+ const score = `\x1B[90m${r.score.toFixed(2)}\x1B[0m`;
703
+ const scopeStr = r.scope?.length ? `${r.scope.map((e) => e.name).join(".")} → ` : "";
704
+ const entityStr = r.entities?.map((e) => e.signature || `${e.type} ${e.name}`).join(", ");
705
+ const highlighted = highlightTerms(r.content, r.highlights);
706
+ return [
707
+ `${r.package} ${score}${entityStr ? ` \x1B[36m${scopeStr}${entityStr}\x1B[0m` : ""}`,
708
+ `\x1B[90m${refPath}:${lineRange}\x1B[0m`,
709
+ ` ${highlighted.replace(/\n/g, "\n ")}`
710
+ ].join("\n");
711
+ }
712
+ function findPackageDbs(packageFilter) {
713
+ if (!existsSync(REFERENCES_DIR)) return [];
714
+ const normalize = (s) => s.toLowerCase().replace(/[-_]/g, "");
715
+ return readdirSync(REFERENCES_DIR).filter((name) => name.includes("@")).filter((name) => {
716
+ if (!packageFilter) return true;
717
+ const pkg = name.split("@")[0];
718
+ const filter = normalize(packageFilter);
719
+ return normalize(pkg).includes(filter) || pkg.startsWith(packageFilter);
720
+ }).map((dir) => join(REFERENCES_DIR, dir, "search.db")).filter((db) => existsSync(db));
721
+ }
722
+ async function searchCommand(rawQuery, packageFilter) {
723
+ const dbs = findPackageDbs(packageFilter);
724
+ if (dbs.length === 0) {
725
+ if (packageFilter) p.log.warn(`No docs indexed for "${packageFilter}". Run \`skilld add ${packageFilter}\` first.`);
726
+ else p.log.warn("No docs indexed yet. Run `skilld add <package>` first.");
727
+ return;
728
+ }
729
+ let query = rawQuery;
730
+ let filter;
731
+ const prefixMatch = rawQuery.match(/^(issues?|docs?|releases?):(.+)$/i);
732
+ if (prefixMatch) {
733
+ const prefix = prefixMatch[1].toLowerCase();
734
+ query = prefixMatch[2];
735
+ if (prefix.startsWith("issue")) filter = { type: "issue" };
736
+ else if (prefix.startsWith("release")) filter = { type: "release" };
737
+ else filter = { type: { $in: ["doc", "docs"] } };
738
+ }
739
+ const start = performance.now();
740
+ const merged = (await Promise.all(dbs.map((dbPath) => searchSnippets(query, { dbPath }, {
741
+ limit: filter ? 10 : 5,
742
+ filter
743
+ })))).flat().sort((a, b) => b.score - a.score).slice(0, 5);
744
+ const elapsed = ((performance.now() - start) / 1e3).toFixed(2);
745
+ if (merged.length === 0) {
746
+ p.log.warn(`No results for "${query}"`);
747
+ return;
748
+ }
749
+ const output = merged.map((r) => formatSnippet(r)).join("\n\n");
750
+ p.log.message(`${output}\n\n${merged.length} results (${elapsed}s)`);
751
+ }
752
+ const require$1 = createRequire(import.meta.url);
753
+ const { version: skilldVersion } = require$1("../package.json");
754
+ function countDocs(packageName, version) {
755
+ if (!version) return 0;
756
+ const cacheDir = getCacheDir(packageName, version);
757
+ if (!existsSync(cacheDir)) return 0;
758
+ let count = 0;
759
+ const walk = (dir, depth = 0) => {
760
+ if (depth > 3) return;
12
761
  try {
13
- execSync("which claude", { stdio: "ignore" });
14
- return true;
15
- } catch {
16
- return false;
17
- }
18
- })()) models.push({
19
- id: "haiku",
20
- name: "Haiku",
21
- description: "Fast and cheap",
22
- available: true
23
- }, {
24
- id: "sonnet",
25
- name: "Sonnet",
26
- description: "Balanced",
27
- available: true
28
- }, {
29
- id: "opus",
30
- name: "Opus",
31
- description: "Most capable",
32
- available: true
762
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
763
+ if (entry.name === "search.db") continue;
764
+ if (entry.isDirectory()) walk(join(dir, entry.name), depth + 1);
765
+ else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) count++;
766
+ }
767
+ } catch {}
768
+ };
769
+ walk(cacheDir);
770
+ return count;
771
+ }
772
+ function countEmbeddings(packageName, version) {
773
+ if (!version) return null;
774
+ const dbPath = getPackageDbPath(packageName, version);
775
+ if (!existsSync(dbPath)) return null;
776
+ try {
777
+ const { DatabaseSync } = require$1("node:sqlite");
778
+ const db = new DatabaseSync(dbPath, {
779
+ open: true,
780
+ readOnly: true
781
+ });
782
+ const row = db.prepare("SELECT count(*) as cnt FROM vector_metadata").get();
783
+ db.close();
784
+ return row?.cnt ?? null;
785
+ } catch {
786
+ return null;
787
+ }
788
+ }
789
+ function countRefDocs(skillDir) {
790
+ const refsDir = join(skillDir, ".skilld");
791
+ if (!existsSync(refsDir)) return 0;
792
+ let count = 0;
793
+ const walk = (dir, depth = 0) => {
794
+ if (depth > 3) return;
795
+ try {
796
+ for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory() || entry.isSymbolicLink()) try {
797
+ if (statSync(join(dir, entry.name)).isDirectory()) walk(join(dir, entry.name), depth + 1);
798
+ } catch {
799
+ continue;
800
+ }
801
+ else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) count++;
802
+ } catch {}
803
+ };
804
+ walk(refsDir);
805
+ return count;
806
+ }
807
+ function timeAgo(iso) {
808
+ if (!iso) return "";
809
+ const diff = Date.now() - new Date(iso).getTime();
810
+ const days = Math.floor(diff / 864e5);
811
+ if (days <= 0) return "today";
812
+ if (days === 1) return "1d ago";
813
+ if (days < 7) return `${days}d ago`;
814
+ if (days < 30) return `${Math.floor(days / 7)}w ago`;
815
+ return `${Math.floor(days / 30)}mo ago`;
816
+ }
817
+ function formatSource(source) {
818
+ if (!source) return "";
819
+ if (source === "shipped") return "shipped";
820
+ if (source.includes("llms.txt")) return "llms.txt";
821
+ if (source.includes("github.com")) return source.replace(/https?:\/\/github\.com\//, "");
822
+ return source;
823
+ }
824
+ const dim = (s) => `\x1B[90m${s}\x1B[0m`;
825
+ const bold = (s) => `\x1B[1m${s}\x1B[0m`;
826
+ const green = (s) => `\x1B[32m${s}\x1B[0m`;
827
+ function getLastSynced$1() {
828
+ let latest = null;
829
+ for (const skill of iterateSkills()) if (skill.info?.syncedAt) {
830
+ const d = new Date(skill.info.syncedAt);
831
+ if (!latest || d > latest) latest = d;
832
+ }
833
+ if (!latest) return null;
834
+ return timeAgo(latest.toISOString());
835
+ }
836
+ function buildConfigLines() {
837
+ const config = readConfig();
838
+ const lines = [];
839
+ lines.push(`Version v${skilldVersion}`);
840
+ const lastSynced = getLastSynced$1();
841
+ if (lastSynced) lines.push(`Synced ${dim(lastSynced)}`);
842
+ lines.push(`Config ${dim(join(CACHE_DIR, "config.yaml"))}${hasConfig() ? "" : dim(" (not created)")}`);
843
+ lines.push(`Cache ${dim(CACHE_DIR)}`);
844
+ const withCli = Object.entries(agents).filter(([_, a]) => a.cli);
845
+ const installed = [];
846
+ for (const [id, agent] of withCli) {
847
+ const ver = getAgentVersion(id);
848
+ if (ver) installed.push(`${agent.displayName} v${ver}`);
849
+ }
850
+ if (installed.length > 0) lines.push(`Agents ${installed.join(", ")}`);
851
+ if (config.model) lines.push(`Model ${config.model}`);
852
+ const features = {
853
+ ...defaultFeatures,
854
+ ...config.features
855
+ };
856
+ const parts = Object.entries(features).map(([k, v]) => `${k}: ${v ? green("on") : dim("off")}`);
857
+ lines.push(`Features ${parts.join(", ")}`);
858
+ if (config.projects?.length) lines.push(`Projects ${config.projects.length} registered`);
859
+ return lines;
860
+ }
861
+ function statusCommand(opts = {}) {
862
+ const allSkills = [...iterateSkills({ scope: opts.global ? "global" : "all" })];
863
+ p.log.step(bold("Skilld Config"));
864
+ p.log.message(buildConfigLines().join("\n"));
865
+ if (allSkills.length === 0) {
866
+ p.log.step(bold("Skills"));
867
+ p.log.message(`${dim("(none)")}\n\nRun ${bold("skilld add <package>")} to install skills`);
868
+ return;
869
+ }
870
+ const localPkgs = /* @__PURE__ */ new Map();
871
+ const globalPkgs = /* @__PURE__ */ new Map();
872
+ for (const skill of allSkills) {
873
+ const key = skill.info?.packageName || skill.name;
874
+ const map = skill.scope === "local" ? localPkgs : globalPkgs;
875
+ if (!map.has(key)) map.set(key, {
876
+ name: skill.name,
877
+ info: skill.info || {},
878
+ agents: new Set([skill.agent]),
879
+ scope: skill.scope
880
+ });
881
+ else map.get(key).agents.add(skill.agent);
882
+ }
883
+ const buildPackageLines = (pkgs) => {
884
+ const lines = [];
885
+ for (const [, pkg] of pkgs) {
886
+ const { info } = pkg;
887
+ const parts = [`${info.source === "shipped" ? "▶" : "◆"} ${bold(pkg.name)}`];
888
+ if (info.version) parts.push(dim(info.version));
889
+ const source = formatSource(info.source);
890
+ if (source && source !== "shipped") parts.push(dim(source));
891
+ lines.push(parts.join(" "));
892
+ const meta = [];
893
+ const pkgName = info.packageName || pkg.name;
894
+ const docs = countDocs(pkgName, info.version) || countRefDocs(join(pkg.scope === "global" ? agents[pkg.agents.values().next().value].globalSkillsDir : join(process.cwd(), agents[pkg.agents.values().next().value].skillsDir), pkg.name));
895
+ if (docs > 0) meta.push(`${docs} docs`);
896
+ const embeddings = countEmbeddings(pkgName, info.version);
897
+ if (embeddings !== null) meta.push(`${embeddings} chunks`);
898
+ const ago = timeAgo(info.syncedAt);
899
+ if (ago) meta.push(`synced ${ago}`);
900
+ if (pkg.agents.size > 0) {
901
+ const agentNames = [...pkg.agents].map((a) => agents[a].displayName);
902
+ meta.push(agentNames.join(", "));
903
+ }
904
+ if (meta.length > 0) lines.push(` ${dim(meta.join(" · "))}`);
905
+ }
906
+ return lines;
907
+ };
908
+ if (!opts.global && localPkgs.size > 0) {
909
+ p.log.step(`${bold("Local")} (project)`);
910
+ p.log.message(buildPackageLines(localPkgs).join("\n"));
911
+ }
912
+ if (globalPkgs.size > 0) {
913
+ p.log.step(bold("Global"));
914
+ p.log.message(buildPackageLines(globalPkgs).join("\n"));
915
+ }
916
+ if (!opts.global && localPkgs.size === 0) {
917
+ p.log.step(`${bold("Local")} (project)`);
918
+ p.log.message(dim("(none)"));
919
+ }
920
+ const total = localPkgs.size + globalPkgs.size;
921
+ p.log.info(`${total} package${total !== 1 ? "s" : ""}`);
922
+ }
923
+ const RESOLVE_STEP_LABELS = {
924
+ "npm": "npm registry",
925
+ "github-docs": "GitHub docs",
926
+ "github-meta": "GitHub meta",
927
+ "github-search": "GitHub search",
928
+ "readme": "README",
929
+ "llms.txt": "llms.txt",
930
+ "local": "node_modules"
931
+ };
932
+ function showResolveAttempts(attempts) {
933
+ if (attempts.length === 0) return;
934
+ p.log.message("\x1B[90mResolution attempts:\x1B[0m");
935
+ for (const attempt of attempts) {
936
+ const icon = attempt.status === "success" ? "\x1B[32m✓\x1B[0m" : "\x1B[90m✗\x1B[0m";
937
+ const source = `\x1B[90m${attempt.source}\x1B[0m`;
938
+ const msg = attempt.message ? ` - ${attempt.message}` : "";
939
+ p.log.message(` ${icon} ${source}${msg}`);
940
+ }
941
+ }
942
+ function formatTaskResults(results) {
943
+ return results.map((r) => {
944
+ if (r.status === "error") return `\x1B[31m✖\x1B[0m ${r.msg}`;
945
+ if (r.status === "warn") return `\x1B[33m▲\x1B[0m ${r.msg}`;
946
+ return `\x1B[32m✓\x1B[0m ${r.msg}`;
947
+ }).join("\n");
948
+ }
949
+ async function ensureGitignore(skillsDir, cwd, isGlobal) {
950
+ if (isGlobal) return;
951
+ const gitignorePath = join(cwd, ".gitignore");
952
+ const pattern = ".skilld";
953
+ if (existsSync(gitignorePath)) {
954
+ if (readFileSync(gitignorePath, "utf-8").split("\n").some((line) => line.trim() === pattern)) return;
955
+ }
956
+ p.log.info(`\x1B[1mGit guidance:\x1B[0m\n \x1B[32m✓\x1B[0m Commit: \x1B[36m${skillsDir}/*/SKILL.md\x1B[0m\n \x1B[32m✓\x1B[0m Commit: \x1B[36m${skillsDir}/skilld-lock.yaml\x1B[0m\n \x1B[31m✗\x1B[0m Ignore: \x1B[36m${pattern}\x1B[0m \x1B[90m(recreated by \`skilld install\`)\x1B[0m`);
957
+ const add = await p.confirm({
958
+ message: `Add \`${pattern}\` to .gitignore?`,
959
+ initialValue: true
960
+ });
961
+ if (p.isCancel(add) || !add) return;
962
+ const entry = `\n# Skilld references (recreated by \`skilld install\`)\n${pattern}\n`;
963
+ if (existsSync(gitignorePath)) appendFileSync(gitignorePath, `${readFileSync(gitignorePath, "utf-8").endsWith("\n") ? "" : "\n"}${entry}`);
964
+ else writeFileSync(gitignorePath, entry);
965
+ p.log.success("Updated .gitignore");
966
+ }
967
+ async function syncCommand(state, opts) {
968
+ if (opts.packages && opts.packages.length > 0) {
969
+ if (opts.packages.length > 1) {
970
+ const { syncPackagesParallel } = await import("./_chunks/sync-parallel.mjs");
971
+ return syncPackagesParallel({
972
+ packages: opts.packages,
973
+ global: opts.global,
974
+ agent: opts.agent,
975
+ model: opts.model,
976
+ yes: opts.yes,
977
+ force: opts.force
978
+ });
979
+ }
980
+ await syncSinglePackage(opts.packages[0], opts);
981
+ return;
982
+ }
983
+ const packages = await interactivePicker(state);
984
+ if (!packages || packages.length === 0) {
985
+ p.outro("No packages selected");
986
+ return;
987
+ }
988
+ if (packages.length > 1) {
989
+ const { syncPackagesParallel } = await import("./_chunks/sync-parallel.mjs");
990
+ return syncPackagesParallel({
991
+ packages,
992
+ global: opts.global,
993
+ agent: opts.agent,
994
+ model: opts.model,
995
+ yes: opts.yes,
996
+ force: opts.force
997
+ });
998
+ }
999
+ await syncSinglePackage(packages[0], opts);
1000
+ }
1001
+ async function interactivePicker(state) {
1002
+ const spin = p.spinner();
1003
+ spin.start("Detecting imports...");
1004
+ const { packages: detected, error } = await detectImportedPackages(process.cwd());
1005
+ const declaredMap = state.deps;
1006
+ if (error || detected.length === 0) {
1007
+ spin.stop(error ? `Detection failed: ${error}` : "No imports detected");
1008
+ if (declaredMap.size === 0) {
1009
+ p.log.warn("No dependencies found");
1010
+ return null;
1011
+ }
1012
+ return pickFromList([...declaredMap.entries()].map(([name, version]) => ({
1013
+ name,
1014
+ version: maskPatch(version),
1015
+ count: 0,
1016
+ inPkgJson: true
1017
+ })), state);
1018
+ }
1019
+ spin.stop(`Loaded ${detected.length} project skills`);
1020
+ return pickFromList(detected.map((pkg) => ({
1021
+ name: pkg.name,
1022
+ version: declaredMap.get(pkg.name),
1023
+ count: pkg.count,
1024
+ inPkgJson: declaredMap.has(pkg.name)
1025
+ })), state);
1026
+ }
1027
+ function maskPatch(version) {
1028
+ if (!version) return void 0;
1029
+ const parts = version.split(".");
1030
+ if (parts.length >= 3) {
1031
+ parts[2] = "x";
1032
+ return parts.slice(0, 3).join(".");
1033
+ }
1034
+ return version;
1035
+ }
1036
+ async function pickFromList(packages, state) {
1037
+ const missingSet = new Set(state.missing);
1038
+ const outdatedSet = new Set(state.outdated.map((s) => s.name));
1039
+ const options = packages.map((pkg) => ({
1040
+ label: pkg.inPkgJson ? `${pkg.name} ★` : pkg.name,
1041
+ value: pkg.name,
1042
+ hint: [maskPatch(pkg.version), pkg.count > 0 ? `${pkg.count} imports` : null].filter(Boolean).join(" · ") || void 0
1043
+ }));
1044
+ const initialValues = packages.filter((pkg) => missingSet.has(pkg.name) || outdatedSet.has(pkg.name)).map((pkg) => pkg.name);
1045
+ const selected = await p.multiselect({
1046
+ message: "Select packages to sync",
1047
+ options,
1048
+ required: false,
1049
+ initialValues
1050
+ });
1051
+ if (p.isCancel(selected)) {
1052
+ p.cancel("Cancelled");
1053
+ return null;
1054
+ }
1055
+ return selected;
1056
+ }
1057
+ async function selectModel(skipPrompt) {
1058
+ const config = readConfig();
1059
+ const available = await getAvailableModels();
1060
+ if (available.length === 0) {
1061
+ p.log.warn("No LLM CLIs found (claude, gemini, codex)");
1062
+ return null;
1063
+ }
1064
+ if (config.model && available.some((m) => m.id === config.model)) return config.model;
1065
+ if (skipPrompt) return available.find((m) => m.recommended)?.id ?? available[0].id;
1066
+ const modelChoice = await p.select({
1067
+ message: "Model for SKILL.md generation",
1068
+ options: available.map((m) => ({
1069
+ label: m.recommended ? `${m.name} (Recommended)` : m.name,
1070
+ value: m.id,
1071
+ hint: `${m.agentName} · ${m.hint}`
1072
+ })),
1073
+ initialValue: available.find((m) => m.recommended)?.id ?? available[0].id
1074
+ });
1075
+ if (p.isCancel(modelChoice)) {
1076
+ p.cancel("Cancelled");
1077
+ return null;
1078
+ }
1079
+ updateConfig({ model: modelChoice });
1080
+ return modelChoice;
1081
+ }
1082
+ async function selectSkillSections() {
1083
+ const selected = await p.multiselect({
1084
+ message: "Generate SKILL.md with LLM",
1085
+ options: [
1086
+ {
1087
+ label: "Best practices",
1088
+ value: "best-practices",
1089
+ hint: "gotchas, pitfalls, patterns"
1090
+ },
1091
+ {
1092
+ label: "API reference",
1093
+ value: "api",
1094
+ hint: "exported functions & composables"
1095
+ },
1096
+ {
1097
+ label: "Custom prompt",
1098
+ value: "custom",
1099
+ hint: "add your own instructions"
1100
+ }
1101
+ ],
1102
+ initialValues: ["best-practices", "api"],
1103
+ required: false
1104
+ });
1105
+ if (p.isCancel(selected)) return {
1106
+ sections: [],
1107
+ cancelled: true
1108
+ };
1109
+ const sections = selected;
1110
+ if (sections.length === 0) return {
1111
+ sections: [],
1112
+ cancelled: false
1113
+ };
1114
+ let customPrompt;
1115
+ if (sections.includes("custom")) {
1116
+ const text = await p.text({
1117
+ message: "Custom instructions",
1118
+ placeholder: "e.g. \"Focus on SSR patterns\" or \"Include migration notes from v2 to v3\""
1119
+ });
1120
+ if (p.isCancel(text)) return {
1121
+ sections: [],
1122
+ cancelled: true
1123
+ };
1124
+ customPrompt = text;
1125
+ }
1126
+ return {
1127
+ sections,
1128
+ customPrompt,
1129
+ cancelled: false
1130
+ };
1131
+ }
1132
+ async function syncSinglePackage(packageName, config) {
1133
+ const spin = p.spinner();
1134
+ spin.start(`Resolving ${packageName}`);
1135
+ const cwd = process.cwd();
1136
+ const localVersion = (await readLocalDependencies(cwd).catch(() => [])).find((d) => d.name === packageName)?.version;
1137
+ const resolveResult = await resolvePackageDocsWithAttempts(packageName, {
1138
+ version: localVersion,
1139
+ cwd,
1140
+ onProgress: (step) => spin.message(`${packageName}: ${RESOLVE_STEP_LABELS[step]}`)
1141
+ });
1142
+ let resolved = resolveResult.package;
1143
+ if (!resolved) {
1144
+ const { readFileSync, existsSync } = await import("node:fs");
1145
+ const { join, resolve } = await import("node:path");
1146
+ const pkgPath = join(cwd, "package.json");
1147
+ if (existsSync(pkgPath)) {
1148
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1149
+ const depVersion = {
1150
+ ...pkg.dependencies,
1151
+ ...pkg.devDependencies
1152
+ }[packageName];
1153
+ if (depVersion?.startsWith("link:")) {
1154
+ spin.message(`Resolving local package: ${packageName}`);
1155
+ resolved = await resolveLocalPackageDocs(resolve(cwd, depVersion.slice(5)));
1156
+ }
1157
+ }
1158
+ }
1159
+ if (!resolved) {
1160
+ spin.stop(`Could not find docs for: ${packageName}`);
1161
+ showResolveAttempts(resolveResult.attempts);
1162
+ return;
1163
+ }
1164
+ const version = localVersion || resolved.version || "latest";
1165
+ const versionKey = getVersionKey(version);
1166
+ if (!existsSync(join(cwd, "node_modules", packageName))) {
1167
+ spin.message(`Downloading ${packageName}@${version} dist`);
1168
+ await fetchPkgDist(packageName, version);
1169
+ }
1170
+ const shippedSkills = getShippedSkills(packageName, cwd, version);
1171
+ if (shippedSkills.length > 0) {
1172
+ const agent = agents[config.agent];
1173
+ const baseDir = config.global ? join(CACHE_DIR, "skills") : join(cwd, agent.skillsDir);
1174
+ mkdirSync(baseDir, { recursive: true });
1175
+ for (const shipped of shippedSkills) {
1176
+ linkShippedSkill(baseDir, shipped.skillName, shipped.skillDir);
1177
+ writeLock(baseDir, shipped.skillName, {
1178
+ packageName,
1179
+ version,
1180
+ source: "shipped",
1181
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1182
+ generator: "skilld"
1183
+ });
1184
+ p.log.success(`Linked shipped skill: ${shipped.skillName} → ${relative(cwd, shipped.skillDir)}`);
1185
+ }
1186
+ if (!config.global) registerProject(cwd);
1187
+ spin.stop(`Shipped ${shippedSkills.length} skill(s) from ${packageName}`);
1188
+ return;
1189
+ }
1190
+ if (config.force) {
1191
+ clearCache(packageName, version);
1192
+ const forcedDbPath = getPackageDbPath(packageName, version);
1193
+ if (existsSync(forcedDbPath)) rmSync(forcedDbPath, {
1194
+ recursive: true,
1195
+ force: true
1196
+ });
1197
+ }
1198
+ const useCache = isCached(packageName, version);
1199
+ spin.stop(`Resolved ${packageName}@${useCache ? versionKey : version}${config.force ? " (force)" : useCache ? " (cached)" : ""}`);
1200
+ ensureCacheDir();
1201
+ const agent = agents[config.agent];
1202
+ const baseDir = config.global ? join(CACHE_DIR, "skills") : join(cwd, agent.skillsDir);
1203
+ const skillDir = join(baseDir, sanitizeName(packageName));
1204
+ mkdirSync(skillDir, { recursive: true });
1205
+ let docSource = resolved.readmeUrl || "readme";
1206
+ let docsType = "readme";
1207
+ const fetchedDocs = [];
1208
+ const fetchedIssues = [];
1209
+ const fetchedDiscussions = [];
1210
+ const fetchedReleases = [];
1211
+ const resourceTasks = [];
1212
+ if (!useCache) resourceTasks.push({
1213
+ title: "Fetching documentation",
1214
+ task: async (message) => {
1215
+ const cachedDocs = [];
1216
+ if (resolved.gitDocsUrl && resolved.repoUrl) {
1217
+ const gh = parseGitHubUrl(resolved.repoUrl);
1218
+ if (gh) {
1219
+ const gitDocs = await fetchGitDocs(gh.owner, gh.repo, version, packageName);
1220
+ if (gitDocs && gitDocs.files.length > 0) {
1221
+ message(`Downloading ${gitDocs.files.length} docs from ${gitDocs.ref}`);
1222
+ const BATCH_SIZE = 20;
1223
+ const results = [];
1224
+ for (let i = 0; i < gitDocs.files.length; i += BATCH_SIZE) {
1225
+ const batch = gitDocs.files.slice(i, i + BATCH_SIZE);
1226
+ const batchResults = await Promise.all(batch.map(async (file) => {
1227
+ const url = `${gitDocs.baseUrl}/${file}`;
1228
+ const res = await fetch(url, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
1229
+ if (!res?.ok) return null;
1230
+ return {
1231
+ file,
1232
+ content: await res.text()
1233
+ };
1234
+ }));
1235
+ results.push(...batchResults);
1236
+ }
1237
+ for (const r of results) if (r) {
1238
+ const cachePath = gitDocs.docsPrefix ? r.file.replace(gitDocs.docsPrefix, "") : r.file;
1239
+ cachedDocs.push({
1240
+ path: cachePath,
1241
+ content: r.content
1242
+ });
1243
+ fetchedDocs.push({
1244
+ id: cachePath,
1245
+ content: r.content,
1246
+ metadata: {
1247
+ package: packageName,
1248
+ source: cachePath,
1249
+ type: "doc"
1250
+ }
1251
+ });
1252
+ }
1253
+ const downloaded = results.filter(Boolean).length;
1254
+ if (downloaded > 0) {
1255
+ docSource = `${resolved.repoUrl}/tree/${gitDocs.ref}/docs`;
1256
+ docsType = "docs";
1257
+ writeToCache(packageName, version, cachedDocs);
1258
+ return `Downloaded ${downloaded} git docs`;
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+ if (resolved.llmsUrl && cachedDocs.length === 0) {
1264
+ message("Fetching llms.txt");
1265
+ const llmsContent = await fetchLlmsTxt(resolved.llmsUrl);
1266
+ if (llmsContent) {
1267
+ docSource = resolved.llmsUrl;
1268
+ docsType = "llms.txt";
1269
+ const baseUrl = resolved.docsUrl || new URL(resolved.llmsUrl).origin;
1270
+ cachedDocs.push({
1271
+ path: "llms.txt",
1272
+ content: normalizeLlmsLinks(llmsContent.raw, baseUrl)
1273
+ });
1274
+ if (llmsContent.links.length > 0) {
1275
+ message(`Downloading ${llmsContent.links.length} linked docs`);
1276
+ const docs = await downloadLlmsDocs(llmsContent, baseUrl);
1277
+ for (const doc of docs) {
1278
+ const cachePath = join("docs", ...(doc.url.startsWith("/") ? doc.url.slice(1) : doc.url).split("/"));
1279
+ cachedDocs.push({
1280
+ path: cachePath,
1281
+ content: doc.content
1282
+ });
1283
+ fetchedDocs.push({
1284
+ id: doc.url,
1285
+ content: doc.content,
1286
+ metadata: {
1287
+ package: packageName,
1288
+ source: cachePath,
1289
+ type: "doc"
1290
+ }
1291
+ });
1292
+ }
1293
+ writeToCache(packageName, version, cachedDocs);
1294
+ return `Saved ${docs.length + 1} docs from llms.txt`;
1295
+ }
1296
+ writeToCache(packageName, version, cachedDocs);
1297
+ return "Saved llms.txt";
1298
+ }
1299
+ }
1300
+ if (resolved.readmeUrl && cachedDocs.length === 0) {
1301
+ message("Fetching README");
1302
+ const content = await fetchReadmeContent(resolved.readmeUrl);
1303
+ if (content) {
1304
+ cachedDocs.push({
1305
+ path: "docs/README.md",
1306
+ content
1307
+ });
1308
+ fetchedDocs.push({
1309
+ id: "README.md",
1310
+ content,
1311
+ metadata: {
1312
+ package: packageName,
1313
+ source: "docs/README.md",
1314
+ type: "doc"
1315
+ }
1316
+ });
1317
+ writeToCache(packageName, version, cachedDocs);
1318
+ return "Saved README.md";
1319
+ }
1320
+ }
1321
+ return "No docs found";
1322
+ }
33
1323
  });
34
- if (process.env.ANTHROPIC_API_KEY) {
35
- if (models.length === 0) models.push({
36
- id: "haiku",
37
- name: "Haiku (API)",
38
- description: "Fast and cheap",
39
- available: true
40
- }, {
41
- id: "sonnet",
42
- name: "Sonnet (API)",
43
- description: "Balanced",
44
- available: true
1324
+ const features = readConfig().features ?? defaultFeatures;
1325
+ const issuesPath = join(getCacheDir(packageName, version), "github", "RECENT-ISSUES.md");
1326
+ if (features.issues && resolved.repoUrl && isGhAvailable() && !existsSync(issuesPath)) {
1327
+ const gh = parseGitHubUrl(resolved.repoUrl);
1328
+ if (gh) resourceTasks.push({
1329
+ title: "Fetching GitHub issues",
1330
+ task: async () => {
1331
+ const issues = await fetchGitHubIssues(gh.owner, gh.repo, 20);
1332
+ if (issues.length > 0) {
1333
+ writeToCache(packageName, version, [{
1334
+ path: "github/RECENT-ISSUES.md",
1335
+ content: formatIssuesAsMarkdown(issues)
1336
+ }]);
1337
+ for (const issue of issues) fetchedIssues.push({
1338
+ id: `issue-${issue.number}`,
1339
+ content: `#${issue.number}: ${issue.title}\n\n${issue.body || ""}`,
1340
+ metadata: {
1341
+ package: packageName,
1342
+ source: "github/RECENT-ISSUES.md",
1343
+ type: "issue",
1344
+ number: issue.number
1345
+ }
1346
+ });
1347
+ return `Cached ${issues.length} issues`;
1348
+ }
1349
+ return "No issues found";
1350
+ }
1351
+ });
1352
+ }
1353
+ const discussionsPath = join(getCacheDir(packageName, version), "github", "RECENT-DISCUSSIONS.md");
1354
+ if (features.discussions && resolved.repoUrl && isGhAvailable() && !existsSync(discussionsPath)) {
1355
+ const gh = parseGitHubUrl(resolved.repoUrl);
1356
+ if (gh) resourceTasks.push({
1357
+ title: "Fetching GitHub discussions",
1358
+ task: async () => {
1359
+ const discussions = await fetchGitHubDiscussions(gh.owner, gh.repo, 20);
1360
+ if (discussions.length > 0) {
1361
+ writeToCache(packageName, version, [{
1362
+ path: "github/RECENT-DISCUSSIONS.md",
1363
+ content: formatDiscussionsAsMarkdown(discussions)
1364
+ }]);
1365
+ for (const d of discussions) fetchedDiscussions.push({
1366
+ id: `discussion-${d.number}`,
1367
+ content: `#${d.number}: ${d.title}\n\n${d.body || ""}`,
1368
+ metadata: {
1369
+ package: packageName,
1370
+ source: "github/RECENT-DISCUSSIONS.md",
1371
+ type: "discussion",
1372
+ number: d.number
1373
+ }
1374
+ });
1375
+ return `Cached ${discussions.length} discussions`;
1376
+ }
1377
+ return "No discussions found";
1378
+ }
1379
+ });
1380
+ }
1381
+ const releasesPath = join(getCacheDir(packageName, version), "releases");
1382
+ if (features.releases && resolved.repoUrl && !existsSync(releasesPath)) {
1383
+ const gh = parseGitHubUrl(resolved.repoUrl);
1384
+ if (gh) resourceTasks.push({
1385
+ title: "Fetching release notes",
1386
+ task: async () => {
1387
+ const releaseDocs = await fetchReleaseNotes(gh.owner, gh.repo, version, resolved.gitRef, packageName);
1388
+ if (releaseDocs.length > 0) {
1389
+ writeToCache(packageName, version, releaseDocs);
1390
+ for (const doc of releaseDocs) fetchedReleases.push({
1391
+ id: doc.path,
1392
+ content: doc.content,
1393
+ metadata: {
1394
+ package: packageName,
1395
+ source: doc.path,
1396
+ type: "release"
1397
+ }
1398
+ });
1399
+ return `Cached ${releaseDocs.length} release note(s)`;
1400
+ }
1401
+ return "No releases found";
1402
+ }
1403
+ });
1404
+ }
1405
+ if (resourceTasks.length > 0) {
1406
+ const resSpin = p.spinner();
1407
+ resSpin.start("Finding resources");
1408
+ const resResults = [];
1409
+ for (const task of resourceTasks) {
1410
+ resSpin.message(task.title);
1411
+ try {
1412
+ const result = await task.task((msg) => resSpin.message(msg));
1413
+ if (result === "No discussions found") continue;
1414
+ resResults.push({
1415
+ msg: result,
1416
+ status: result.startsWith("No ") ? "warn" : "ok"
1417
+ });
1418
+ } catch {
1419
+ resResults.push({
1420
+ msg: `${task.title} failed`,
1421
+ status: "error"
1422
+ });
1423
+ }
1424
+ }
1425
+ resSpin.stop("Fetched resources");
1426
+ p.log.message(formatTaskResults(resResults));
1427
+ }
1428
+ try {
1429
+ linkPkg(skillDir, packageName, cwd, version);
1430
+ if (!hasShippedDocs(packageName, cwd, version) && docsType !== "readme") linkReferences(skillDir, packageName, version);
1431
+ linkGithub(skillDir, packageName, version);
1432
+ linkReleases(skillDir, packageName, version);
1433
+ } catch {}
1434
+ const dbPath = getPackageDbPath(packageName, version);
1435
+ const indexTasks = [];
1436
+ if (!existsSync(dbPath)) if (fetchedDocs.length > 0 || fetchedIssues.length > 0 || fetchedDiscussions.length > 0 || fetchedReleases.length > 0) {
1437
+ if (fetchedDocs.length > 0) indexTasks.push({
1438
+ title: `Indexing ${fetchedDocs.length} docs`,
1439
+ task: async (message) => {
1440
+ await createIndex(fetchedDocs, {
1441
+ dbPath,
1442
+ onProgress: (current, total, doc) => {
1443
+ message(`Indexing doc ${doc?.id ? doc.id.split("/").pop() : ""} - ${current}/${total}`);
1444
+ }
1445
+ });
1446
+ return `Indexed ${fetchedDocs.length} docs`;
1447
+ }
1448
+ });
1449
+ if (fetchedIssues.length > 0) indexTasks.push({
1450
+ title: `Indexing ${fetchedIssues.length} issues`,
1451
+ task: async (message) => {
1452
+ await createIndex(fetchedIssues, {
1453
+ dbPath,
1454
+ onProgress: (current, total, doc) => {
1455
+ message(`Indexing doc ${doc?.id ? doc.id.split("/").pop() : ""} - ${current}/${total}`);
1456
+ }
1457
+ });
1458
+ return `Indexed ${fetchedIssues.length} issues`;
1459
+ }
1460
+ });
1461
+ if (fetchedDiscussions.length > 0) indexTasks.push({
1462
+ title: `Indexing ${fetchedDiscussions.length} discussions`,
1463
+ task: async (message) => {
1464
+ await createIndex(fetchedDiscussions, {
1465
+ dbPath,
1466
+ onProgress: (current, total, doc) => {
1467
+ message(`Indexing doc ${doc?.id ? doc.id.split("/").pop() : ""} - ${current}/${total}`);
1468
+ }
1469
+ });
1470
+ return `Indexed ${fetchedDiscussions.length} discussions`;
1471
+ }
1472
+ });
1473
+ if (fetchedReleases.length > 0) indexTasks.push({
1474
+ title: `Indexing ${fetchedReleases.length} releases`,
1475
+ task: async (message) => {
1476
+ await createIndex(fetchedReleases, {
1477
+ dbPath,
1478
+ onProgress: (current, total, doc) => {
1479
+ message(`Indexing doc ${doc?.id ? doc.id.split("/").pop() : ""} - ${current}/${total}`);
1480
+ }
1481
+ });
1482
+ return `Indexed ${fetchedReleases.length} releases`;
1483
+ }
1484
+ });
1485
+ } else indexTasks.push({
1486
+ title: "Indexing cached docs",
1487
+ task: async (message) => {
1488
+ const cachedDocs = readCachedDocs(packageName, version);
1489
+ if (cachedDocs.length === 0) return "No docs to index";
1490
+ const docsToIndex = cachedDocs.filter((doc) => !doc.path.startsWith("github/")).map((doc) => ({
1491
+ id: doc.path,
1492
+ content: doc.content,
1493
+ metadata: {
1494
+ package: packageName,
1495
+ source: doc.path,
1496
+ type: "doc"
1497
+ }
1498
+ }));
1499
+ const issuesDoc = cachedDocs.find((doc) => doc.path === "github/RECENT-ISSUES.md");
1500
+ if (issuesDoc) {
1501
+ const issueBlocks = issuesDoc.content.split(/\n---\n/).filter(Boolean);
1502
+ for (const block of issueBlocks) {
1503
+ const match = block.match(/## #(\d+): (.+)/);
1504
+ if (match) docsToIndex.push({
1505
+ id: `issue-${match[1]}`,
1506
+ content: block,
1507
+ metadata: {
1508
+ package: packageName,
1509
+ source: "github/RECENT-ISSUES.md",
1510
+ type: "issue",
1511
+ number: Number(match[1])
1512
+ }
1513
+ });
1514
+ }
1515
+ }
1516
+ const discussionsDoc = cachedDocs.find((doc) => doc.path === "github/RECENT-DISCUSSIONS.md");
1517
+ if (discussionsDoc) {
1518
+ const discussionBlocks = discussionsDoc.content.split(/\n---\n/).filter(Boolean);
1519
+ for (const block of discussionBlocks) {
1520
+ const match = block.match(/## #(\d+): (.+)/);
1521
+ if (match) docsToIndex.push({
1522
+ id: `discussion-${match[1]}`,
1523
+ content: block,
1524
+ metadata: {
1525
+ package: packageName,
1526
+ source: "github/RECENT-DISCUSSIONS.md",
1527
+ type: "discussion",
1528
+ number: Number(match[1])
1529
+ }
1530
+ });
1531
+ }
1532
+ }
1533
+ await createIndex(docsToIndex, {
1534
+ dbPath,
1535
+ onProgress: (current, total, doc) => {
1536
+ message(`Indexing ${doc?.type === "source" || doc?.type === "types" ? "code" : "doc"} ${doc?.id ? doc.id.split("/").pop() : ""} - ${current}/${total}`);
1537
+ }
1538
+ });
1539
+ return `Indexed ${docsToIndex.length} docs`;
1540
+ }
1541
+ });
1542
+ const pkgDir = resolvePkgDir(packageName, cwd, version);
1543
+ const entryFiles = features.search && pkgDir ? await resolveEntryFiles(pkgDir) : [];
1544
+ if (entryFiles.length > 0) {
1545
+ const entryLabel = entryFiles.length === 1 ? entryFiles[0].path : `${entryFiles.length} entry files`;
1546
+ indexTasks.push({
1547
+ title: `Indexing ${entryLabel}`,
1548
+ task: async (message) => {
1549
+ await createIndex(entryFiles.map((e) => ({
1550
+ id: e.path,
1551
+ content: e.content,
1552
+ metadata: {
1553
+ package: packageName,
1554
+ source: `pkg/${e.path}`,
1555
+ type: e.type
1556
+ }
1557
+ })), {
1558
+ dbPath,
1559
+ onProgress: (current, total, doc) => {
1560
+ message(`Indexing code ${doc?.id ? doc.id.split("/").pop() : ""} - ${current}/${total}`);
1561
+ }
1562
+ });
1563
+ return `Indexed ${entryLabel}`;
1564
+ }
45
1565
  });
46
1566
  }
47
- return models;
1567
+ if (indexTasks.length > 0) {
1568
+ const idxSpin = p.spinner();
1569
+ idxSpin.start("Creating search index");
1570
+ const idxResults = [];
1571
+ for (const task of indexTasks) {
1572
+ idxSpin.message(task.title);
1573
+ try {
1574
+ const result = await task.task((msg) => idxSpin.message(msg));
1575
+ idxResults.push({
1576
+ msg: result,
1577
+ status: result.startsWith("No ") ? "warn" : "ok"
1578
+ });
1579
+ } catch {
1580
+ idxResults.push({
1581
+ msg: `${task.title} failed`,
1582
+ status: "error"
1583
+ });
1584
+ }
1585
+ }
1586
+ idxSpin.stop("Search index ready");
1587
+ p.log.message(formatTaskResults(idxResults));
1588
+ }
1589
+ const cacheDir = getCacheDir(packageName, version);
1590
+ if (useCache) {
1591
+ if (existsSync(join(cacheDir, "docs", "index.md")) || existsSync(join(cacheDir, "docs", "guide"))) {
1592
+ docSource = resolved.repoUrl ? `${resolved.repoUrl}/tree/v${version}/docs` : "git";
1593
+ docsType = "docs";
1594
+ } else if (existsSync(join(cacheDir, "llms.txt"))) {
1595
+ docSource = resolved.llmsUrl || "llms.txt";
1596
+ docsType = "llms.txt";
1597
+ } else if (existsSync(join(cacheDir, "docs", "README.md"))) docsType = "readme";
1598
+ }
1599
+ const hasGithub = existsSync(join(getCacheDir(packageName, version), "github"));
1600
+ const hasReleases = existsSync(releasesPath);
1601
+ const hasChangelog = pkgDir ? ["CHANGELOG.md", "changelog.md"].find((f) => existsSync(join(pkgDir, f))) || false : false;
1602
+ const relatedSkills = await findRelatedSkills(packageName, baseDir);
1603
+ const shippedDocs = hasShippedDocs(packageName, cwd, version);
1604
+ const pkgFiles = getPkgKeyFiles(packageName, cwd, version);
1605
+ const baseSkillMd = generateSkillMd({
1606
+ name: packageName,
1607
+ version,
1608
+ releasedAt: resolved.releasedAt,
1609
+ description: resolved.description,
1610
+ dependencies: resolved.dependencies,
1611
+ distTags: resolved.distTags,
1612
+ relatedSkills,
1613
+ hasGithub,
1614
+ hasReleases,
1615
+ hasChangelog,
1616
+ docsType,
1617
+ hasShippedDocs: shippedDocs,
1618
+ pkgFiles
1619
+ });
1620
+ writeFileSync(join(skillDir, "SKILL.md"), baseSkillMd);
1621
+ writeLock(baseDir, sanitizeName(packageName), {
1622
+ packageName,
1623
+ version,
1624
+ source: docSource,
1625
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1626
+ generator: "skilld"
1627
+ });
1628
+ p.log.success(`Created base skill: ${relative(cwd, skillDir)}`);
1629
+ if (!readConfig().skipLlm && (!config.yes || config.model)) {
1630
+ const { sections, customPrompt, cancelled } = config.model ? {
1631
+ sections: ["best-practices", "api"],
1632
+ customPrompt: void 0,
1633
+ cancelled: false
1634
+ } : await selectSkillSections();
1635
+ if (!cancelled && sections.length > 0) {
1636
+ const model = config.model ?? await selectModel(false);
1637
+ if (model) await enhanceSkillWithLLM({
1638
+ packageName,
1639
+ version,
1640
+ skillDir,
1641
+ model,
1642
+ resolved,
1643
+ relatedSkills,
1644
+ hasGithub,
1645
+ hasReleases,
1646
+ hasChangelog,
1647
+ docsType,
1648
+ hasShippedDocs: shippedDocs,
1649
+ pkgFiles,
1650
+ force: config.force,
1651
+ sections,
1652
+ customPrompt
1653
+ });
1654
+ }
1655
+ }
1656
+ if (!config.global) registerProject(cwd);
1657
+ await ensureGitignore(agent.skillsDir, cwd, config.global);
1658
+ p.outro(`Synced ${packageName} to ${relative(cwd, skillDir)}`);
48
1659
  }
49
- const OPTIMIZE_PROMPT = `You are a technical documentation optimizer for coding agents.
50
-
51
- Your task: Transform raw package documentation into a concise skill reference focused on what an AI coding assistant needs.
52
-
53
- ## EXTRACT THESE (IMPORTANT):
54
-
55
- ### 1. Public API Reference
56
- - All exported functions, components, composables with their signatures
57
- - Required vs optional parameters and their types
58
- - Return types and what they represent
59
- - Export names exactly as they should be imported
60
-
61
- ### 2. Non-Obvious Best Practices
62
- - Gotchas and common mistakes developers make
63
- - Edge cases that cause bugs
64
- - Performance considerations (what's expensive, what to cache)
65
- - Order-of-operations requirements
66
- - Initialization patterns that aren't obvious
67
- - When NOT to use certain features
68
-
69
- ### 3. Integration Patterns
70
- - How this integrates with common frameworks/tools
71
- - Required peer dependencies and version constraints
72
- - Configuration that's easy to get wrong
73
-
74
- ## REMOVE THESE:
75
- - Installation instructions (npm install, etc.)
76
- - Badges, shields, contributor info
77
- - License information
78
- - Changelog/version history
79
- - Generic "what is this" introductions
80
- - Marketing language
81
- - Links to external resources (keep inline code examples)
82
-
83
- ## OUTPUT FORMAT:
84
- Use clear markdown with code blocks. Be dense and direct - no filler. Prefer tables for API references.
85
- Organize as: Quick Reference → API → Best Practices → Gotchas
86
-
87
- ## INPUT DOCUMENTATION:
88
- `;
89
- async function optimizeDocs(content, packageName, agent, model = "haiku") {
90
- if (agent === "claude-code" || !agent) {
91
- const result = await tryClaudeCode(content, packageName, model);
92
- if (result) return {
93
- optimized: result,
94
- wasOptimized: true
95
- };
1660
+ async function enhanceSkillWithLLM(opts) {
1661
+ const { packageName, version, skillDir, model, resolved, relatedSkills, hasGithub, hasReleases, hasChangelog, docsType, hasShippedDocs: shippedDocs, pkgFiles, force, sections, customPrompt } = opts;
1662
+ const llmSpin = p.spinner();
1663
+ llmSpin.start(`Agent exploring ${packageName}`);
1664
+ const { optimized, wasOptimized } = await optimizeDocs({
1665
+ packageName,
1666
+ skillDir,
1667
+ model,
1668
+ version,
1669
+ hasGithub,
1670
+ hasReleases,
1671
+ hasChangelog,
1672
+ docFiles: listReferenceFiles(skillDir),
1673
+ noCache: force,
1674
+ sections,
1675
+ customPrompt,
1676
+ onProgress: ({ type, chunk }) => {
1677
+ if (type === "reasoning" && chunk.startsWith("[")) llmSpin.message(chunk);
1678
+ else if (type === "text") llmSpin.message(`Writing...`);
1679
+ }
1680
+ });
1681
+ if (wasOptimized) {
1682
+ llmSpin.stop("Generated best practices");
1683
+ const body = cleanSkillMd(optimized);
1684
+ const skillMd = generateSkillMd({
1685
+ name: packageName,
1686
+ version,
1687
+ releasedAt: resolved.releasedAt,
1688
+ dependencies: resolved.dependencies,
1689
+ distTags: resolved.distTags,
1690
+ body,
1691
+ relatedSkills,
1692
+ hasGithub,
1693
+ hasReleases,
1694
+ hasChangelog,
1695
+ docsType,
1696
+ hasShippedDocs: shippedDocs,
1697
+ pkgFiles
1698
+ });
1699
+ writeFileSync(join(skillDir, "SKILL.md"), skillMd);
1700
+ } else llmSpin.stop("LLM optimization failed");
1701
+ }
1702
+ async function findRelatedSkills(packageName, skillsDir) {
1703
+ const related = [];
1704
+ const npmInfo = await fetchNpmPackage(packageName);
1705
+ if (!npmInfo?.dependencies) return related;
1706
+ const deps = Object.keys(npmInfo.dependencies);
1707
+ if (!existsSync(skillsDir)) return related;
1708
+ const installedSkills = readdirSync(skillsDir);
1709
+ for (const skill of installedSkills) if (deps.some((d) => sanitizeName(d) === skill)) related.push(skill);
1710
+ return related.slice(0, 5);
1711
+ }
1712
+ function cleanSkillMd(content) {
1713
+ let cleaned = content.replace(/^```markdown\n?/m, "").replace(/\n?```$/m, "").trim();
1714
+ const fmMatch = cleaned.match(/^-{3,}\n/);
1715
+ if (fmMatch) {
1716
+ const afterOpen = fmMatch[0].length;
1717
+ const closeMatch = cleaned.slice(afterOpen).match(/\n-{3,}/);
1718
+ if (closeMatch) cleaned = cleaned.slice(afterOpen + closeMatch.index + closeMatch[0].length).trim();
1719
+ else cleaned = cleaned.slice(afterOpen).trim();
96
1720
  }
97
- if (process.env.ANTHROPIC_API_KEY) {
98
- const result = await tryAnthropicSDK(content, packageName, model);
99
- if (result) return {
100
- optimized: result,
101
- wasOptimized: true
102
- };
1721
+ return cleaned;
1722
+ }
1723
+ async function uninstallCommand(opts) {
1724
+ let scope = opts.scope;
1725
+ const registeredProjects = getRegisteredProjects();
1726
+ if (!scope) {
1727
+ const allHint = registeredProjects.length > 0 ? `${registeredProjects.length} projects + global + cache` : "global skills + cache";
1728
+ const selected = await p.select({
1729
+ message: "What do you want to uninstall?",
1730
+ options: [{
1731
+ label: "This project",
1732
+ value: "project",
1733
+ hint: "current project only"
1734
+ }, {
1735
+ label: "Everything",
1736
+ value: "all",
1737
+ hint: allHint
1738
+ }]
1739
+ });
1740
+ if (p.isCancel(selected)) {
1741
+ p.cancel("Cancelled");
1742
+ return;
1743
+ }
1744
+ scope = selected;
103
1745
  }
104
- return {
105
- optimized: content,
106
- wasOptimized: false
1746
+ const toRemove = [];
1747
+ const seenPaths = /* @__PURE__ */ new Set();
1748
+ const projectsToUnregister = [];
1749
+ const agentFilter = opts.agent ? [opts.agent] : void 0;
1750
+ const addToRemove = (label, path, version) => {
1751
+ if (seenPaths.has(path)) return;
1752
+ seenPaths.add(path);
1753
+ toRemove.push({
1754
+ label,
1755
+ path,
1756
+ version
1757
+ });
107
1758
  };
108
- }
109
- async function tryClaudeCode(content, packageName, model) {
110
- try {
111
- execSync("which claude", { stdio: "ignore" });
112
- const prompt = `${OPTIMIZE_PROMPT}\n\nPackage: ${packageName}\n\n${content}`;
113
- const tempFile = `/tmp/skilld-optimize-${Date.now()}.txt`;
114
- const { writeFileSync, unlinkSync } = await import("node:fs");
115
- writeFileSync(tempFile, prompt);
116
- try {
117
- return execSync(`claude --model ${model} --print < "${tempFile}"`, {
118
- encoding: "utf-8",
119
- maxBuffer: 10 * 1024 * 1024,
120
- timeout: 18e4
121
- }).trim();
122
- } finally {
123
- unlinkSync(tempFile);
1759
+ const addSkillsFromLock = (skillsDir, label) => {
1760
+ const trackedNames = [];
1761
+ const lock = readLock(skillsDir);
1762
+ if (lock?.skills) {
1763
+ for (const [skillName, info] of Object.entries(lock.skills)) {
1764
+ trackedNames.push(skillName);
1765
+ const skillDir = join(skillsDir, skillName);
1766
+ if (existsSync(skillDir)) {
1767
+ const version = info.version ? `${info.version.split(".").slice(0, 2).join(".")}.x` : void 0;
1768
+ addToRemove(`${label}: ${skillName}`, skillDir, version);
1769
+ }
1770
+ }
1771
+ const lockPath = join(skillsDir, "skilld-lock.yaml");
1772
+ if (existsSync(lockPath)) addToRemove(`${label}: skilld-lock.yaml`, lockPath);
1773
+ }
1774
+ return trackedNames;
1775
+ };
1776
+ const findUntrackedSkills = (skillsDir, trackedNames) => {
1777
+ if (!existsSync(skillsDir)) return [];
1778
+ const tracked = new Set(trackedNames);
1779
+ return readdirSync(skillsDir).filter((f) => !f.startsWith(".") && f !== "skilld-lock.yaml" && !tracked.has(f));
1780
+ };
1781
+ const untrackedByDir = /* @__PURE__ */ new Map();
1782
+ const processedDirs = /* @__PURE__ */ new Set();
1783
+ const processSkillsDir = (skillsDir, label) => {
1784
+ if (processedDirs.has(skillsDir)) return;
1785
+ processedDirs.add(skillsDir);
1786
+ const untracked = findUntrackedSkills(skillsDir, addSkillsFromLock(skillsDir, label));
1787
+ if (untracked.length > 0) untrackedByDir.set(skillsDir, {
1788
+ label,
1789
+ skills: untracked
1790
+ });
1791
+ };
1792
+ if (scope === "project") {
1793
+ for (const [name, agent] of Object.entries(agents)) {
1794
+ if (agentFilter && !agentFilter.includes(name)) continue;
1795
+ processSkillsDir(join(process.cwd(), agent.skillsDir), "project");
1796
+ }
1797
+ projectsToUnregister.push(process.cwd());
1798
+ }
1799
+ if (scope === "all") {
1800
+ const projectPaths = registeredProjects.length > 0 ? registeredProjects : [process.cwd()];
1801
+ if (registeredProjects.length > 0) {
1802
+ p.log.info("Projects to uninstall from:");
1803
+ for (const proj of projectPaths) p.log.message(` ${proj}`);
1804
+ }
1805
+ for (const projectPath of projectPaths) {
1806
+ if (!existsSync(projectPath)) continue;
1807
+ const shortPath = projectPath.replace(process.env.HOME || "", "~");
1808
+ for (const [name, agent] of Object.entries(agents)) {
1809
+ if (agentFilter && !agentFilter.includes(name)) continue;
1810
+ processSkillsDir(join(projectPath, agent.skillsDir), shortPath);
1811
+ }
1812
+ projectsToUnregister.push(projectPath);
1813
+ }
1814
+ for (const [name, agent] of Object.entries(agents)) {
1815
+ if (agentFilter && !agentFilter.includes(name)) continue;
1816
+ if (!agent.globalSkillsDir) continue;
1817
+ processSkillsDir(agent.globalSkillsDir, "user");
1818
+ }
1819
+ if (existsSync(CACHE_DIR)) addToRemove("~/.skilld cache", CACHE_DIR);
1820
+ }
1821
+ if (untrackedByDir.size > 0) {
1822
+ const groupedUntracked = /* @__PURE__ */ new Map();
1823
+ for (const [_dir, { label, skills }] of untrackedByDir) {
1824
+ if (!groupedUntracked.has(label)) groupedUntracked.set(label, /* @__PURE__ */ new Set());
1825
+ for (const s of skills) groupedUntracked.get(label).add(s);
1826
+ }
1827
+ const totalUntracked = [...groupedUntracked.values()].reduce((sum, s) => sum + s.size, 0);
1828
+ p.log.warn(`${totalUntracked} untracked skill(s) will remain (not managed by skilld):`);
1829
+ for (const [label, skills] of groupedUntracked) p.log.message(` ${label}: ${[...skills].join(", ")}`);
1830
+ }
1831
+ if (toRemove.length === 0) {
1832
+ p.log.info("Nothing to uninstall");
1833
+ return;
1834
+ }
1835
+ const groups = /* @__PURE__ */ new Map();
1836
+ for (const item of toRemove) {
1837
+ const [prefix, name] = item.label.includes(": ") ? item.label.split(": ", 2) : ["other", item.label];
1838
+ if (!groups.has(prefix)) groups.set(prefix, []);
1839
+ groups.get(prefix).push({
1840
+ name,
1841
+ version: item.version
1842
+ });
1843
+ }
1844
+ const formatGroup = (items) => items.map((i) => i.version ? `${i.name}@${i.version}` : i.name).join(", ");
1845
+ p.log.info(`Will remove ${toRemove.length} items:`);
1846
+ for (const [prefix, items] of groups) p.log.message(` ${prefix}: ${formatGroup(items)}`);
1847
+ if (!opts.yes) {
1848
+ const confirmed = await p.confirm({ message: "Proceed with uninstall?" });
1849
+ if (p.isCancel(confirmed) || !confirmed) {
1850
+ p.cancel("Cancelled");
1851
+ return;
124
1852
  }
125
- } catch {
126
- return null;
127
1853
  }
1854
+ for (const item of toRemove) rmSync(item.path, {
1855
+ recursive: true,
1856
+ force: true
1857
+ });
1858
+ for (const [prefix, items] of groups) p.log.success(`Removed ${prefix}: ${formatGroup(items)}`);
1859
+ if (scope !== "all") for (const proj of projectsToUnregister) unregisterProject(proj);
1860
+ p.outro("skilld uninstalled");
128
1861
  }
129
- const MODEL_MAP = {
130
- haiku: "claude-3-5-haiku-latest",
131
- sonnet: "claude-sonnet-4-20250514",
132
- opus: "claude-opus-4-20250514"
133
- };
134
- async function tryAnthropicSDK(content, packageName, model) {
1862
+ function hasGhCli() {
1863
+ if (process.env.SKILLD_NO_GH) return false;
135
1864
  try {
136
- const { default: Anthropic } = await import("@anthropic-ai/sdk");
137
- return (await new Anthropic().messages.create({
138
- model: MODEL_MAP[model],
139
- max_tokens: 8192,
140
- messages: [{
141
- role: "user",
142
- content: `${OPTIMIZE_PROMPT}\n\nPackage: ${packageName}\n\n${content}`
143
- }]
144
- })).content.find((b) => b.type === "text")?.text || null;
1865
+ execSync("gh --version", { stdio: "ignore" });
1866
+ return true;
145
1867
  } catch {
146
- return null;
1868
+ return false;
1869
+ }
1870
+ }
1871
+ async function runWizard() {
1872
+ p.note("Skilld gives your AI agent skill knowledge on your NPM\ndependencies gathered from versioned docs, source code\nand GitHub issues.", "Welcome to skilld");
1873
+ const ghInstalled = hasGhCli();
1874
+ if (ghInstalled) p.log.success("GitHub CLI detected — will use it to pull issues and discussions.");
1875
+ else p.log.warn("GitHub CLI not found. Install it to enable issues/discussions:\n \x1B[36mhttps://cli.github.com\x1B[0m");
1876
+ const selected = await p.multiselect({
1877
+ message: "Which features would you like to enable?",
1878
+ options: [
1879
+ {
1880
+ label: "Semantic + token search",
1881
+ value: "search",
1882
+ hint: "local query engine to cut token costs and speed up grep"
1883
+ },
1884
+ {
1885
+ label: "Release notes",
1886
+ value: "releases",
1887
+ hint: "track changelogs for installed packages"
1888
+ },
1889
+ {
1890
+ label: "GitHub issues",
1891
+ value: "issues",
1892
+ hint: "surface common problems and solutions",
1893
+ disabled: !ghInstalled
1894
+ },
1895
+ {
1896
+ label: "GitHub discussions",
1897
+ value: "discussions",
1898
+ hint: "include Q&A and community knowledge",
1899
+ disabled: !ghInstalled
1900
+ }
1901
+ ],
1902
+ initialValues: [...Object.entries(defaultFeatures).filter(([, v]) => v).map(([k]) => k), ...ghInstalled ? ["issues", "discussions"] : []],
1903
+ required: false
1904
+ });
1905
+ if (p.isCancel(selected)) {
1906
+ p.cancel("Setup cancelled");
1907
+ process.exit(0);
1908
+ }
1909
+ const features = {
1910
+ search: selected.includes("search"),
1911
+ issues: selected.includes("issues"),
1912
+ discussions: selected.includes("discussions"),
1913
+ releases: selected.includes("releases")
1914
+ };
1915
+ const allModels = process.env.SKILLD_NO_AGENTS ? [] : await getAvailableModels();
1916
+ let modelId;
1917
+ if (allModels.length > 0) {
1918
+ p.note("Skills work without an LLM, but one can rewrite your\nSKILL.md files with best practices and better structure.\n\x1B[90mThis is separate from the agent where skills are installed —\nthe target agent is auto-detected from your project files.\x1B[0m", "Optional: LLM optimization");
1919
+ const modelChoice = await p.select({
1920
+ message: "Model for generating SKILL.md",
1921
+ options: [{
1922
+ label: "Skip",
1923
+ value: "",
1924
+ hint: "use raw docs, no LLM needed"
1925
+ }, ...allModels.map((m) => ({
1926
+ label: m.recommended ? `${m.name} (Recommended)` : m.name,
1927
+ value: m.id,
1928
+ hint: `${m.agentName} · ${m.hint}`
1929
+ }))]
1930
+ });
1931
+ if (p.isCancel(modelChoice)) {
1932
+ p.cancel("Setup cancelled");
1933
+ process.exit(0);
1934
+ }
1935
+ modelId = modelChoice || void 0;
1936
+ } else {
1937
+ p.log.warn("No supported LLM CLIs detected (claude, gemini, codex).\n Skills will still work, but won't be LLM-optimized.");
1938
+ const proceed = await p.confirm({
1939
+ message: "Continue without LLM optimization?",
1940
+ initialValue: true
1941
+ });
1942
+ if (p.isCancel(proceed) || !proceed) {
1943
+ p.cancel("Setup cancelled");
1944
+ process.exit(0);
1945
+ }
1946
+ }
1947
+ updateConfig({
1948
+ features,
1949
+ ...modelId ? { model: modelId } : { skipLlm: true }
1950
+ });
1951
+ p.outro("Thanks, you're all set! Change config anytime with `skilld config`.");
1952
+ }
1953
+ const _emit = process.emit;
1954
+ process.emit = (event, ...args) => event === "warning" && args[0]?.name === "ExperimentalWarning" && args[0]?.message?.includes("SQLite") ? false : _emit.apply(process, [event, ...args]);
1955
+ const { version } = createRequire(import.meta.url)("../package.json");
1956
+ function getRepoHint(name, cwd) {
1957
+ const pkgJsonPath = join(cwd, "node_modules", name, "package.json");
1958
+ if (!existsSync(pkgJsonPath)) return void 0;
1959
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
1960
+ const url = typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url;
1961
+ if (!url) return void 0;
1962
+ return url.replace(/^git\+/, "").replace(/\.git$/, "").replace(/^git:\/\//, "https://").replace(/^ssh:\/\/git@github\.com/, "https://github.com").replace(/^https?:\/\/(www\.)?github\.com\//, "");
1963
+ }
1964
+ function formatStatus(synced, outdated) {
1965
+ const parts = [];
1966
+ if (synced > 0) parts.push(`\x1B[32m${synced} synced\x1B[0m`);
1967
+ if (outdated > 0) parts.push(`\x1B[33m${outdated} outdated\x1B[0m`);
1968
+ return `Skills: ${parts.join(" · ")}`;
1969
+ }
1970
+ function relativeTime(date) {
1971
+ const diff = Date.now() - date.getTime();
1972
+ const mins = Math.floor(diff / 6e4);
1973
+ const hours = Math.floor(diff / 36e5);
1974
+ const days = Math.floor(diff / 864e5);
1975
+ if (mins < 1) return "just now";
1976
+ if (mins < 60) return `${mins}m ago`;
1977
+ if (hours < 24) return `${hours}h ago`;
1978
+ return `${days}d ago`;
1979
+ }
1980
+ function getLastSynced(state) {
1981
+ let latest = null;
1982
+ for (const skill of state.skills) if (skill.info?.syncedAt) {
1983
+ const d = new Date(skill.info.syncedAt);
1984
+ if (!latest || d > latest) latest = d;
1985
+ }
1986
+ return latest ? relativeTime(latest) : null;
1987
+ }
1988
+ const NOISE_CHARS = "⣿⡿⣷⣾⣽⣻⢿⡷⣯⣟⡾⣵⣳⢾⡽⣞⡷⣝⢯";
1989
+ function djb2(s) {
1990
+ let h = 5381;
1991
+ for (let i = 0; i < s.length; i++) h = (h << 5) + h + s.charCodeAt(i) >>> 0;
1992
+ return h;
1993
+ }
1994
+ function hueToChannel(p, q, t) {
1995
+ const t1 = t < 0 ? t + 1 : t > 1 ? t - 1 : t;
1996
+ if (t1 < 1 / 6) return p + (q - p) * 6 * t1;
1997
+ if (t1 < 1 / 2) return q;
1998
+ if (t1 < 2 / 3) return p + (q - p) * (2 / 3 - t1) * 6;
1999
+ return p;
2000
+ }
2001
+ function hsl(h, s, l) {
2002
+ const q = l < .5 ? l * (1 + s) : l + s - l * s;
2003
+ const p = 2 * l - q;
2004
+ return [
2005
+ Math.round(hueToChannel(p, q, h + 1 / 3) * 255),
2006
+ Math.round(hueToChannel(p, q, h) * 255),
2007
+ Math.round(hueToChannel(p, q, h - 1 / 3) * 255)
2008
+ ];
2009
+ }
2010
+ const BRAND_HUE = djb2(process.cwd()) % 360 / 360;
2011
+ function noiseChar(brightness, density = 0) {
2012
+ if (brightness < .08) return " ";
2013
+ const b = Math.min(brightness, 1);
2014
+ const ch = Math.random() < density ? "⣿" : NOISE_CHARS[Math.floor(Math.random() * 19)];
2015
+ const [r, g, bl] = hsl(BRAND_HUE, .4 + b * .15, .35 + b * .25);
2016
+ return `\x1B[38;2;${r};${g};${bl}m${ch}`;
2017
+ }
2018
+ function noiseLine(len, brightnessFn, density = 0) {
2019
+ let s = "";
2020
+ for (let i = 0; i < len; i++) s += noiseChar(brightnessFn(i), density);
2021
+ return `${s}\x1B[0m`;
2022
+ }
2023
+ function brandFrame(t, floor = 0, density = 0) {
2024
+ const cx = 5;
2025
+ const cy = 1;
2026
+ const brightness = (x, y) => {
2027
+ const d = Math.sqrt((x - cx) ** 2 + ((y - cy) * 3) ** 2);
2028
+ let val = 0;
2029
+ for (let ring = 0; ring < 3; ring++) {
2030
+ const rt = t - ring * .5;
2031
+ if (rt <= 0) continue;
2032
+ const front = rt * 4;
2033
+ const proximity = Math.abs(d - front);
2034
+ val += Math.exp(-proximity * proximity * .8) * Math.exp(-rt * .4);
2035
+ }
2036
+ const base = Math.max(0, (t - 1.5) * .3) * (Math.random() * .3 + .1);
2037
+ return Math.min(1, Math.max(floor, val + base));
2038
+ };
2039
+ return [
2040
+ noiseLine(10, (x) => brightness(x, 0), density),
2041
+ `${noiseLine(2, (x) => brightness(x, 1), density)} %NAME% ${noiseLine(2, (x) => brightness(x + 8, 1), density)} %VER%`,
2042
+ noiseLine(10, (x) => brightness(x, 2), density)
2043
+ ].join("\n");
2044
+ }
2045
+ async function brandLoader(work, minMs = 1500) {
2046
+ if (process.env.SKILLD_EFFECT === "none") return work();
2047
+ const logUpdate = (await import("log-update")).default;
2048
+ const name = "\x1B[1m\x1B[38;2;255;255;255mskilld\x1B[0m";
2049
+ const ver = `\x1B[2mv${version}\x1B[0m`;
2050
+ const status = "\x1B[2mSetting up your environment\x1B[0m";
2051
+ const start = Date.now();
2052
+ const sub = (raw) => raw.replace("%NAME%", name).replace("%VER%", ver);
2053
+ let done = false;
2054
+ const result = Promise.all([work(), new Promise((r) => setTimeout(r, minMs))]).then(([v]) => {
2055
+ done = true;
2056
+ return v;
2057
+ });
2058
+ while (!done) {
2059
+ logUpdate(`\n ${sub(brandFrame((Date.now() - start) / 1e3))}\n\n ${status}`);
2060
+ await new Promise((r) => setTimeout(r, 60));
2061
+ }
2062
+ const outroMs = 500;
2063
+ const outroStart = Date.now();
2064
+ const tFinal = (outroStart - start) / 1e3;
2065
+ while (Date.now() - outroStart < outroMs) {
2066
+ const p = (Date.now() - outroStart) / outroMs;
2067
+ const eased = p * p;
2068
+ logUpdate(`\n ${sub(brandFrame(tFinal + p * .5, eased * .9, eased))}\n`);
2069
+ await new Promise((r) => setTimeout(r, 40));
147
2070
  }
2071
+ logUpdate(`\n ${sub(brandFrame(tFinal + 1, .9, 1))}\n`);
2072
+ logUpdate.done();
2073
+ return result;
148
2074
  }
149
- const main = defineCommand({
2075
+ function introLine({ state, generators, modelId }) {
2076
+ const name = "\x1B[1m\x1B[35mskilld\x1B[0m";
2077
+ const ver = `\x1B[90mv${version}\x1B[0m`;
2078
+ const lastSynced = getLastSynced(state);
2079
+ const synced = lastSynced ? ` · \x1B[90msynced ${lastSynced}\x1B[0m` : "";
2080
+ const modelStr = modelId ? ` · ${getModelName(modelId)}` : "";
2081
+ const genStr = generators?.length ? generators.map((g) => `${g.name} v${g.version}`).join(", ") : "";
2082
+ return `${name} ${ver}${synced}${genStr ? `\n\x1B[90m↳ ${genStr}${modelStr}\x1B[0m` : ""}`;
2083
+ }
2084
+ function getInstalledGenerators() {
2085
+ return detectInstalledAgents().filter((id) => agents[id].cli).map((id) => {
2086
+ const version = getAgentVersion(id);
2087
+ return version ? {
2088
+ name: agents[id].displayName,
2089
+ version
2090
+ } : null;
2091
+ }).filter((a) => a !== null);
2092
+ }
2093
+ async function prepareSync(cwd, agentFlag) {
2094
+ const agent = resolveAgent(agentFlag);
2095
+ if (!agent) return;
2096
+ const state = await getProjectState(cwd);
2097
+ if (state.outdated.length === 0) {
2098
+ p.log.success("Skills up to date");
2099
+ return;
2100
+ }
2101
+ await syncCommand(state, {
2102
+ packages: state.outdated.map((s) => s.packageName || s.name),
2103
+ global: false,
2104
+ agent,
2105
+ yes: true
2106
+ });
2107
+ }
2108
+ function resolveAgent(agentFlag) {
2109
+ return agentFlag ?? detectTargetAgent() ?? readConfig().agent ?? null;
2110
+ }
2111
+ const sharedArgs = {
2112
+ global: {
2113
+ type: "boolean",
2114
+ alias: "g",
2115
+ description: "Install globally to ~/.claude/skills",
2116
+ default: false
2117
+ },
2118
+ agent: {
2119
+ type: "string",
2120
+ alias: "a",
2121
+ description: "Agent where skills are installed (claude-code, cursor, windsurf, etc.)"
2122
+ },
2123
+ yes: {
2124
+ type: "boolean",
2125
+ alias: "y",
2126
+ description: "Skip prompts, use defaults",
2127
+ default: false
2128
+ },
2129
+ force: {
2130
+ type: "boolean",
2131
+ alias: "f",
2132
+ description: "Ignore all caches, re-fetch docs and regenerate",
2133
+ default: false
2134
+ }
2135
+ };
2136
+ const SUBCOMMAND_NAMES = [
2137
+ "add",
2138
+ "update",
2139
+ "status",
2140
+ "config",
2141
+ "remove",
2142
+ "install",
2143
+ "uninstall",
2144
+ "search"
2145
+ ];
2146
+ const addCommand = defineCommand({
150
2147
  meta: {
151
- name: "skilld",
152
- description: "Generate searchable skills from documentation sites or package.json dependencies"
2148
+ name: "add",
2149
+ description: "Add skills for package(s)"
153
2150
  },
154
2151
  args: {
155
- url: {
2152
+ package: {
156
2153
  type: "positional",
157
- description: "URL or package name (omit to process package.json deps)",
158
- required: false
159
- },
160
- output: {
161
- type: "string",
162
- alias: "o",
163
- description: "Output directory (legacy mode)"
2154
+ description: "Package(s) to sync, comma-separated (e.g., vue,nuxt,pinia)",
2155
+ required: true
164
2156
  },
165
- maxPages: {
166
- type: "string",
167
- alias: "m",
168
- description: "Max pages to fetch",
169
- default: "100"
170
- },
171
- chunkSize: {
172
- type: "string",
173
- description: "Chunk size in characters",
174
- default: "1000"
175
- },
176
- model: {
177
- type: "string",
178
- description: "Embedding model",
179
- default: "Xenova/bge-small-en-v1.5"
2157
+ ...sharedArgs
2158
+ },
2159
+ async run({ args }) {
2160
+ const cwd = process.cwd();
2161
+ const agent = resolveAgent(args.agent);
2162
+ if (!agent) {
2163
+ p.log.warn("Could not detect agent. Use --agent <name>");
2164
+ return;
2165
+ }
2166
+ const state = await getProjectState(cwd);
2167
+ p.intro(introLine({ state }));
2168
+ return syncCommand(state, {
2169
+ packages: args.package.split(",").map((s) => s.trim()).filter(Boolean),
2170
+ global: args.global,
2171
+ agent,
2172
+ yes: args.yes,
2173
+ force: args.force
2174
+ });
2175
+ }
2176
+ });
2177
+ const updateSubCommand = defineCommand({
2178
+ meta: {
2179
+ name: "update",
2180
+ description: "Update outdated skills"
2181
+ },
2182
+ args: {
2183
+ package: {
2184
+ type: "positional",
2185
+ description: "Package(s) to update, comma-separated. Without args, syncs all outdated.",
2186
+ required: false
180
2187
  },
181
- crawl: {
182
- type: "boolean",
183
- description: "Skip llms.txt and always crawl",
184
- default: false
2188
+ ...sharedArgs
2189
+ },
2190
+ async run({ args }) {
2191
+ const cwd = process.cwd();
2192
+ const agent = resolveAgent(args.agent);
2193
+ if (!agent) {
2194
+ p.log.warn("Could not detect agent. Use --agent <name>");
2195
+ return;
2196
+ }
2197
+ const state = await getProjectState(cwd);
2198
+ const generators = getInstalledGenerators();
2199
+ const config = readConfig();
2200
+ p.intro(introLine({
2201
+ state,
2202
+ generators,
2203
+ modelId: config.model
2204
+ }));
2205
+ if (args.package) return syncCommand(state, {
2206
+ packages: args.package.split(",").map((s) => s.trim()).filter(Boolean),
2207
+ global: args.global,
2208
+ agent,
2209
+ yes: args.yes,
2210
+ force: args.force
2211
+ });
2212
+ if (state.outdated.length === 0) {
2213
+ p.log.success("All skills up to date");
2214
+ return;
2215
+ }
2216
+ return syncCommand(state, {
2217
+ packages: state.outdated.map((s) => s.packageName || s.name),
2218
+ global: args.global,
2219
+ agent,
2220
+ yes: args.yes,
2221
+ force: args.force
2222
+ });
2223
+ }
2224
+ });
2225
+ const statusSubCommand = defineCommand({
2226
+ meta: {
2227
+ name: "status",
2228
+ description: "Show skill status"
2229
+ },
2230
+ args: { global: sharedArgs.global },
2231
+ run({ args }) {
2232
+ return statusCommand({ global: args.global });
2233
+ }
2234
+ });
2235
+ const configSubCommand = defineCommand({
2236
+ meta: {
2237
+ name: "config",
2238
+ description: "Edit settings"
2239
+ },
2240
+ args: {},
2241
+ async run() {
2242
+ const state = await getProjectState(process.cwd());
2243
+ const generators = getInstalledGenerators();
2244
+ const config = readConfig();
2245
+ p.intro(introLine({
2246
+ state,
2247
+ generators,
2248
+ modelId: config.model
2249
+ }));
2250
+ return configCommand();
2251
+ }
2252
+ });
2253
+ const removeSubCommand = defineCommand({
2254
+ meta: {
2255
+ name: "remove",
2256
+ description: "Remove installed skills"
2257
+ },
2258
+ args: { ...sharedArgs },
2259
+ async run({ args }) {
2260
+ const cwd = process.cwd();
2261
+ const agent = resolveAgent(args.agent);
2262
+ if (!agent) {
2263
+ p.log.warn("Could not detect agent. Use --agent <name>");
2264
+ return;
2265
+ }
2266
+ const state = await getProjectState(cwd);
2267
+ const generators = getInstalledGenerators();
2268
+ const config = readConfig();
2269
+ const scope = args.global ? "global" : "project";
2270
+ const intro = {
2271
+ state,
2272
+ generators,
2273
+ modelId: config.model
2274
+ };
2275
+ p.intro(`${introLine(intro)} · remove (${scope})`);
2276
+ return removeCommand(state, {
2277
+ global: args.global,
2278
+ agent,
2279
+ yes: args.yes
2280
+ });
2281
+ }
2282
+ });
2283
+ const installSubCommand = defineCommand({
2284
+ meta: {
2285
+ name: "install",
2286
+ description: "Restore references from lockfile"
2287
+ },
2288
+ args: {
2289
+ global: sharedArgs.global,
2290
+ agent: sharedArgs.agent
2291
+ },
2292
+ async run({ args }) {
2293
+ const agent = resolveAgent(args.agent);
2294
+ if (!agent) {
2295
+ p.log.warn("Could not detect agent. Use --agent <name>");
2296
+ return;
2297
+ }
2298
+ p.intro(`\x1B[1m\x1B[35mskilld\x1B[0m install`);
2299
+ return installCommand({
2300
+ global: args.global,
2301
+ agent
2302
+ });
2303
+ }
2304
+ });
2305
+ const uninstallSubCommand = defineCommand({
2306
+ meta: {
2307
+ name: "uninstall",
2308
+ description: "Remove skilld data"
2309
+ },
2310
+ args: { ...sharedArgs },
2311
+ async run({ args }) {
2312
+ p.intro(`\x1B[1m\x1B[35mskilld\x1B[0m uninstall`);
2313
+ return uninstallCommand({
2314
+ scope: args.global ? "all" : void 0,
2315
+ agent: args.agent,
2316
+ yes: args.yes
2317
+ });
2318
+ }
2319
+ });
2320
+ const searchSubCommand = defineCommand({
2321
+ meta: {
2322
+ name: "search",
2323
+ description: "Search indexed docs"
2324
+ },
2325
+ args: {
2326
+ query: {
2327
+ type: "positional",
2328
+ description: "Search query (e.g., \"useFetch options\")",
2329
+ required: true
185
2330
  },
186
- concurrency: {
2331
+ package: {
187
2332
  type: "string",
188
- alias: "c",
189
- description: "Concurrent package processing",
190
- default: "3"
191
- },
192
- global: {
2333
+ alias: "p",
2334
+ description: "Filter by package name"
2335
+ }
2336
+ },
2337
+ async run({ args }) {
2338
+ return searchCommand(args.query, args.package || void 0);
2339
+ }
2340
+ });
2341
+ runMain(defineCommand({
2342
+ meta: {
2343
+ name: "skilld",
2344
+ description: "Sync package documentation for agentic use"
2345
+ },
2346
+ args: {
2347
+ prepare: {
193
2348
  type: "boolean",
194
- alias: "g",
195
- description: "Install skills globally",
2349
+ description: "Non-interactive sync for pnpm prepare hook (outdated only, no LLM, always exits 0)",
196
2350
  default: false
197
2351
  },
198
- agent: {
199
- type: "string",
200
- alias: "a",
201
- description: "Target specific agent (claude-code, cursor, windsurf, etc.)"
202
- },
203
- force: {
2352
+ background: {
204
2353
  type: "boolean",
205
- alias: "f",
206
- description: "Force regenerate even if version matches",
2354
+ alias: "b",
2355
+ description: "Run --prepare in background (detached process)",
207
2356
  default: false
208
2357
  },
209
- optimize: {
210
- type: "boolean",
211
- description: "Optimize docs with LLM (extracts APIs & best practices)",
212
- default: true
213
- },
214
- yes: {
215
- type: "boolean",
216
- alias: "y",
217
- description: "Skip prompts, use defaults",
218
- default: false
219
- }
2358
+ agent: sharedArgs.agent
2359
+ },
2360
+ subCommands: {
2361
+ add: addCommand,
2362
+ update: updateSubCommand,
2363
+ status: statusSubCommand,
2364
+ config: configSubCommand,
2365
+ remove: removeSubCommand,
2366
+ install: installSubCommand,
2367
+ uninstall: uninstallSubCommand,
2368
+ search: searchSubCommand
220
2369
  },
221
2370
  async run({ args }) {
222
- const maxPages = Number.parseInt(args.maxPages, 10);
223
- const chunkSize = Number.parseInt(args.chunkSize, 10);
224
- const concurrency = Number.parseInt(args.concurrency, 10);
225
- const currentAgent = detectCurrentAgent();
226
- if (!currentAgent && !args.agent && !args.output) {
227
- consola.warn("Could not detect which agent is running. Use --agent <name> or --output <dir>");
228
- consola.info("Supported agents: " + Object.keys(agents).join(", "));
229
- return;
230
- }
231
- const targetAgents = args.agent ? [args.agent] : currentAgent ? [currentAgent] : [];
232
- if (targetAgents.length > 0) consola.info(`Target agent: ${targetAgents.map((a) => agents[a].displayName).join(", ")}`);
233
- if (args.url) {
234
- await processUrl(args.url, {
235
- outputDir: args.output,
236
- maxPages,
237
- chunkSize,
238
- skipLlmsTxt: args.crawl,
239
- model: args.model,
240
- global: args.global,
241
- agents: targetAgents,
242
- optimize: args.optimize,
243
- currentAgent
244
- });
2371
+ const firstArg = process.argv[2];
2372
+ if (firstArg && !firstArg.startsWith("-") && SUBCOMMAND_NAMES.includes(firstArg)) return;
2373
+ const cwd = process.cwd();
2374
+ if (args.prepare) {
2375
+ if (args.background) {
2376
+ const { spawn } = await import("node:child_process");
2377
+ spawn(process.execPath, [
2378
+ process.argv[1],
2379
+ "--prepare",
2380
+ ...args.agent ? ["--agent", args.agent] : []
2381
+ ], {
2382
+ cwd,
2383
+ detached: true,
2384
+ stdio: "ignore"
2385
+ }).unref();
2386
+ return;
2387
+ }
2388
+ await prepareSync(cwd, args.agent).catch(() => {});
245
2389
  return;
246
2390
  }
247
- consola.start("Reading package.json dependencies...");
248
- const allDeps = await readLocalDependencies(process.cwd());
249
- if (allDeps.length === 0) {
250
- consola.warn("No dependencies found in package.json");
2391
+ const currentAgent = resolveAgent(args.agent);
2392
+ if (!currentAgent) {
2393
+ p.log.warn("Could not detect agent. Use --agent <name> or `skilld config`");
2394
+ p.log.info(`Supported: ${Object.keys(agents).join(", ")}`);
251
2395
  return;
252
2396
  }
253
- const needsUpdate = [];
254
- const upToDate = [];
255
- for (const dep of allDeps) {
256
- if (args.force) {
257
- needsUpdate.push(dep);
258
- continue;
2397
+ const { state, selfUpdate } = await brandLoader(async () => {
2398
+ const config = readConfig();
2399
+ const state = await getProjectState(cwd);
2400
+ let selfUpdate = null;
2401
+ const tasks = [];
2402
+ if (!(process.env.npm_command === "exec")) tasks.push(fetchNpmRegistryMeta("skilld", version).then((meta) => {
2403
+ const latestTag = meta.distTags?.latest;
2404
+ if (latestTag && latestTag.version !== version) selfUpdate = {
2405
+ latest: latestTag.version,
2406
+ releasedAt: latestTag.releasedAt
2407
+ };
2408
+ }).catch(() => {}));
2409
+ if (state.unmatched.length > 0) {
2410
+ const limit = pLimit(5);
2411
+ tasks.push(Promise.all(state.unmatched.map((skill) => limit(async () => {
2412
+ const pkgName = skill.info?.packageName || skill.name;
2413
+ const latest = await fetchLatestVersion(pkgName);
2414
+ if (latest && isOutdated(skill, latest)) state.outdated.push({
2415
+ ...skill,
2416
+ packageName: pkgName,
2417
+ latestVersion: latest
2418
+ });
2419
+ else if (latest) state.synced.push({
2420
+ ...skill,
2421
+ packageName: pkgName,
2422
+ latestVersion: latest
2423
+ });
2424
+ }))).then(() => {}));
259
2425
  }
260
- if (targetAgents.length > 0) {
261
- const agent = agents[targetAgents[0]];
262
- const installedVersion = await getInstalledSkillVersion(join(process.cwd(), agent.skillsDir, sanitizeName(dep.name)));
263
- if (installedVersion && installedVersion === dep.version) upToDate.push(dep);
264
- else needsUpdate.push(dep);
265
- } else needsUpdate.push(dep);
266
- }
267
- if (needsUpdate.length === 0) {
268
- consola.success(`All ${allDeps.length} skills are up-to-date`);
269
- return;
2426
+ await Promise.all(tasks);
2427
+ return {
2428
+ config,
2429
+ state,
2430
+ selfUpdate
2431
+ };
2432
+ });
2433
+ if (selfUpdate) {
2434
+ const released = selfUpdate.releasedAt ? `\x1B[90m · ${relativeTime(new Date(selfUpdate.releasedAt))}\x1B[0m` : "";
2435
+ const cmd = `npx nypm add${realpathSync(process.argv[1]).startsWith(resolve(cwd, "node_modules")) ? "" : " -g"} skilld@${selfUpdate.latest}`;
2436
+ p.note(`\x1B[90m${version}\x1B[0m → \x1B[1m\x1B[32m${selfUpdate.latest}\x1B[0m${released}\n\x1B[36m${cmd}\x1B[0m`, "\x1B[33mUpdate available\x1B[0m");
270
2437
  }
271
- consola.info(`Found ${needsUpdate.length} packages to sync (${upToDate.length} up-to-date)`);
272
- let selectedDeps = needsUpdate;
273
- if (!args.yes && needsUpdate.length > 1) {
274
- const choices = needsUpdate.map((d) => ({
275
- label: `${d.name}@${d.version}`,
276
- value: d.name
277
- }));
278
- const selected = await consola.prompt("Select packages to sync:", {
279
- type: "multiselect",
280
- options: choices,
281
- initial: choices.map((c) => c.value)
2438
+ if (state.skills.length === 0) {
2439
+ if (!hasConfig()) await runWizard();
2440
+ const pkgJsonPath = join(cwd, "package.json");
2441
+ const projectName = existsSync(pkgJsonPath) ? JSON.parse(readFileSync(pkgJsonPath, "utf-8")).name : void 0;
2442
+ const projectLabel = projectName ? `Generating skills for \x1B[36m${projectName}\x1B[0m` : "Generating skills for current directory";
2443
+ p.log.step(projectLabel);
2444
+ p.log.info("Tip: Only generate skills for packages your agent struggles with.\n The fewer skills, the more context you have for everything else :)");
2445
+ const source = await p.select({
2446
+ message: "How should I find packages?",
2447
+ options: [
2448
+ {
2449
+ label: "Scan source files",
2450
+ value: "imports",
2451
+ hint: "Find actually used imports"
2452
+ },
2453
+ {
2454
+ label: "Use package.json",
2455
+ value: "deps",
2456
+ hint: `All ${state.deps.size} dependencies`
2457
+ },
2458
+ {
2459
+ label: "Enter manually",
2460
+ value: "manual"
2461
+ }
2462
+ ]
282
2463
  });
283
- if (!selected || selected.length === 0) {
284
- consola.warn("No packages selected");
2464
+ if (p.isCancel(source)) {
2465
+ p.cancel("Setup cancelled");
285
2466
  return;
286
2467
  }
287
- selectedDeps = needsUpdate.filter((d) => selected.includes(d.name));
288
- }
289
- let optimizeModel = null;
290
- if (args.optimize && !args.yes) {
291
- const availableModels = await getAvailableModels();
292
- if (availableModels.length > 0) {
293
- const modelChoice = await consola.prompt("Optimize with LLM?", {
294
- type: "select",
295
- options: [...availableModels.map((m) => ({
296
- label: `${m.name} ${m.available ? "(available)" : ""}`,
297
- value: m.id,
298
- hint: m.description
299
- })), {
300
- label: "Skip optimization",
301
- value: "skip"
302
- }],
303
- initial: availableModels[0]?.id || "skip"
2468
+ let selected;
2469
+ if (source === "manual") {
2470
+ const input = await p.text({
2471
+ message: "Enter package names (comma-separated)",
2472
+ placeholder: "vue, nuxt, pinia"
304
2473
  });
305
- optimizeModel = modelChoice === "skip" ? null : modelChoice;
306
- }
307
- } else if (args.optimize) optimizeModel = "haiku";
308
- const queue = [...selectedDeps];
309
- let processed = 0;
310
- let failed = 0;
311
- const worker = async () => {
312
- while (queue.length > 0) {
313
- const dep = queue.shift();
314
- if (!dep) break;
315
- const { name: packageName, version: localVersion } = dep;
316
- try {
317
- consola.start(`[${processed + failed + 1}/${selectedDeps.length}] ${packageName}@${localVersion}`);
318
- const resolved = await resolvePackageDocs(packageName);
319
- if (!resolved) {
320
- consola.warn(` No docs found for ${packageName}`);
321
- failed++;
322
- continue;
323
- }
324
- const url = resolved.llmsUrl || resolved.docsUrl || resolved.readmeUrl;
325
- if (!url) {
326
- consola.warn(` No URL resolved for ${packageName}`);
327
- failed++;
328
- continue;
2474
+ if (p.isCancel(input) || !input) {
2475
+ p.cancel("No packages entered");
2476
+ return;
2477
+ }
2478
+ selected = input.split(",").map((s) => s.trim()).filter(Boolean);
2479
+ } else {
2480
+ let usages;
2481
+ if (source === "imports") {
2482
+ const spinner = p.spinner();
2483
+ spinner.start("Scanning imports...");
2484
+ const result = await detectImportedPackages(cwd);
2485
+ spinner.stop(`Found ${result.packages.length} imported packages`);
2486
+ if (result.packages.length === 0) {
2487
+ p.log.warn("No imports found, falling back to package.json");
2488
+ usages = [...state.deps.keys()].map((name) => ({
2489
+ name,
2490
+ count: 0
2491
+ }));
2492
+ } else {
2493
+ const depSet = new Set(state.deps.keys());
2494
+ usages = result.packages.filter((pkg) => depSet.has(pkg.name) || pkg.source === "preset");
2495
+ if (usages.length === 0) {
2496
+ p.log.warn("No matching dependencies, using all imports");
2497
+ usages = result.packages;
2498
+ }
329
2499
  }
330
- await processUrl(url, {
331
- outputDir: args.output,
332
- maxPages,
333
- chunkSize,
334
- skipLlmsTxt: args.crawl,
335
- model: args.model,
336
- global: args.global,
337
- agents: targetAgents,
338
- packageName,
339
- packageVersion: localVersion,
340
- quiet: true,
341
- optimize: !!optimizeModel,
342
- optimizeModel,
343
- currentAgent
344
- });
345
- processed++;
346
- consola.success(` Generated skill for ${packageName}@${localVersion}`);
347
- } catch (e) {
348
- consola.error(` Failed: ${e.message}`);
349
- failed++;
2500
+ } else usages = [...state.deps.keys()].map((name) => ({
2501
+ name,
2502
+ count: 0
2503
+ }));
2504
+ const packages = usages.map((u) => u.name);
2505
+ const sourceMap = new Map(usages.map((u) => [u.name, u.source]));
2506
+ const maxLen = Math.max(...packages.map((n) => n.length));
2507
+ const choice = await p.multiselect({
2508
+ message: `Select packages (${packages.length} found)`,
2509
+ options: packages.map((name) => {
2510
+ const ver = state.deps.get(name)?.replace(/^[\^~>=<]/, "") || "";
2511
+ const repo = getRepoHint(name, cwd);
2512
+ const hint = sourceMap.get(name) === "preset" ? "nuxt module" : void 0;
2513
+ const pad = " ".repeat(maxLen - name.length + 2);
2514
+ const meta = [
2515
+ ver,
2516
+ hint,
2517
+ repo
2518
+ ].filter(Boolean).join(" ");
2519
+ return {
2520
+ label: meta ? `${name}${pad}\x1B[90m${meta}\x1B[39m` : name,
2521
+ value: name
2522
+ };
2523
+ }),
2524
+ initialValues: packages
2525
+ });
2526
+ if (p.isCancel(choice) || choice.length === 0) {
2527
+ p.cancel("No packages selected");
2528
+ return;
350
2529
  }
2530
+ selected = choice;
351
2531
  }
352
- };
353
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
354
- consola.box(`Generated ${processed} skills (${upToDate.length} up-to-date, ${failed} failed)`);
355
- }
356
- });
357
- async function processUrl(url, config) {
358
- let resolvedUrl = url;
359
- let skillName = config.packageName;
360
- let skillVersion = config.packageVersion;
361
- let isRawReadme = false;
362
- let description = "";
363
- if (!url.includes("://") && !url.startsWith("http")) {
364
- if (!config.quiet) consola.start(`Resolving docs for package: ${url}`);
365
- skillName = url;
366
- const resolved = await resolvePackageDocs(url);
367
- if (!resolved) {
368
- consola.error(`Could not find docs for package: ${url}`);
369
- return;
370
- }
371
- description = resolved.description || "";
372
- skillVersion = skillVersion || resolved.version;
373
- if (resolved.llmsUrl) resolvedUrl = resolved.llmsUrl;
374
- else if (resolved.docsUrl) resolvedUrl = resolved.docsUrl;
375
- else if (resolved.readmeUrl) {
376
- resolvedUrl = resolved.readmeUrl;
377
- isRawReadme = true;
378
- } else {
379
- consola.error(`No documentation found for ${url}`);
380
- return;
2532
+ return syncCommand(state, {
2533
+ packages: selected,
2534
+ global: false,
2535
+ agent: currentAgent,
2536
+ yes: false
2537
+ });
381
2538
  }
382
- if (!config.quiet) consola.info(`Resolved to: ${resolvedUrl}${isRawReadme ? " (README)" : ""}`);
383
- }
384
- if (isRawReadme || resolvedUrl.includes("raw.githubusercontent.com") || resolvedUrl.startsWith("ungh://")) {
385
- if (!config.quiet) consola.start(`Fetching README for ${skillName || "package"}`);
386
- let readmeContent;
387
- if (resolvedUrl.startsWith("ungh://")) {
388
- const parts = resolvedUrl.replace("ungh://", "").split("/");
389
- const owner = parts[0];
390
- const repo = parts[1];
391
- const subdir = parts.slice(2).join("/");
392
- const unghUrl = subdir ? `https://ungh.cc/repos/${owner}/${repo}/files/main/${subdir}/README.md` : `https://ungh.cc/repos/${owner}/${repo}/readme`;
393
- const res = await fetch(unghUrl, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
394
- if (!res?.ok) {
395
- consola.error(`Failed to fetch README from ungh: ${res?.status}`);
2539
+ const status = formatStatus(state.synced.length, state.outdated.length);
2540
+ p.log.info(status);
2541
+ while (true) {
2542
+ const options = [];
2543
+ options.push({
2544
+ label: "Add new skills",
2545
+ value: "install"
2546
+ });
2547
+ if (state.outdated.length > 0) options.push({
2548
+ label: "Update skills",
2549
+ value: "update",
2550
+ hint: `\x1B[33m${state.outdated.length} outdated\x1B[0m`
2551
+ });
2552
+ options.push({
2553
+ label: "Remove skills",
2554
+ value: "remove"
2555
+ }, {
2556
+ label: "Status",
2557
+ value: "status"
2558
+ }, {
2559
+ label: "Configure",
2560
+ value: "config"
2561
+ });
2562
+ const action = await p.select({
2563
+ message: "What would you like to do?",
2564
+ options
2565
+ });
2566
+ if (p.isCancel(action)) {
2567
+ p.cancel("Cancelled");
396
2568
  return;
397
2569
  }
398
- const text = await res.text();
399
- try {
400
- const json = JSON.parse(text);
401
- readmeContent = json.markdown || json.file?.contents || "";
402
- } catch {
403
- readmeContent = text;
404
- }
405
- } else {
406
- const res = await fetch(resolvedUrl, { headers: { "User-Agent": "skilld/1.0" } }).catch(() => null);
407
- if (!res?.ok) {
408
- consola.error(`Failed to fetch README: ${res?.status}`);
409
- return;
2570
+ switch (action) {
2571
+ case "install": {
2572
+ const installedNames = new Set(state.skills.map((s) => s.packageName || s.name));
2573
+ const uninstalledDeps = [...state.deps.keys()].filter((d) => !installedNames.has(d));
2574
+ const allDepsInstalled = uninstalledDeps.length === 0;
2575
+ const source = await p.select({
2576
+ message: "How should I find packages?",
2577
+ options: [
2578
+ {
2579
+ label: "Scan source files",
2580
+ value: "imports",
2581
+ hint: allDepsInstalled ? "all installed" : "find actually used imports",
2582
+ disabled: allDepsInstalled
2583
+ },
2584
+ {
2585
+ label: "Use package.json",
2586
+ value: "deps",
2587
+ hint: allDepsInstalled ? "all installed" : `${uninstalledDeps.length} uninstalled`,
2588
+ disabled: allDepsInstalled
2589
+ },
2590
+ {
2591
+ label: "Enter manually",
2592
+ value: "manual"
2593
+ }
2594
+ ]
2595
+ });
2596
+ if (p.isCancel(source)) continue;
2597
+ let selected;
2598
+ if (source === "manual") {
2599
+ const input = await p.text({
2600
+ message: "Enter package names (comma-separated)",
2601
+ placeholder: "vue, nuxt, pinia"
2602
+ });
2603
+ if (p.isCancel(input) || !input) continue;
2604
+ selected = input.split(",").map((s) => s.trim()).filter(Boolean);
2605
+ if (selected.length === 0) continue;
2606
+ } else {
2607
+ let usages;
2608
+ if (source === "imports") {
2609
+ const spinner = p.spinner();
2610
+ spinner.start("Scanning imports...");
2611
+ const result = await detectImportedPackages(cwd);
2612
+ spinner.stop(`Found ${result.packages.length} imported packages`);
2613
+ if (result.packages.length === 0) {
2614
+ p.log.warn("No imports found, falling back to package.json");
2615
+ usages = uninstalledDeps.map((name) => ({
2616
+ name,
2617
+ count: 0
2618
+ }));
2619
+ } else {
2620
+ const depSet = new Set(state.deps.keys());
2621
+ usages = result.packages.filter((pkg) => depSet.has(pkg.name) || pkg.source === "preset").filter((pkg) => !installedNames.has(pkg.name));
2622
+ if (usages.length === 0) {
2623
+ p.log.warn("All detected imports already have skills");
2624
+ continue;
2625
+ }
2626
+ }
2627
+ } else usages = uninstalledDeps.map((name) => ({
2628
+ name,
2629
+ count: 0
2630
+ }));
2631
+ const packages = usages.map((u) => u.name);
2632
+ const sourceMap = new Map(usages.map((u) => [u.name, u.source]));
2633
+ const maxLen = Math.max(...packages.map((n) => n.length));
2634
+ const choice = await p.multiselect({
2635
+ message: `Select packages (${packages.length} found)`,
2636
+ options: packages.map((name) => {
2637
+ const ver = state.deps.get(name)?.replace(/^[\^~>=<]/, "") || "";
2638
+ const repo = getRepoHint(name, cwd);
2639
+ const hint = sourceMap.get(name) === "preset" ? "nuxt module" : void 0;
2640
+ const pad = " ".repeat(maxLen - name.length + 2);
2641
+ const meta = [
2642
+ ver,
2643
+ hint,
2644
+ repo
2645
+ ].filter(Boolean).join(" ");
2646
+ return {
2647
+ label: meta ? `${name}${pad}\x1B[90m${meta}\x1B[39m` : name,
2648
+ value: name
2649
+ };
2650
+ }),
2651
+ initialValues: packages
2652
+ });
2653
+ if (p.isCancel(choice) || choice.length === 0) continue;
2654
+ selected = choice;
2655
+ }
2656
+ return syncCommand(state, {
2657
+ packages: selected,
2658
+ global: false,
2659
+ agent: currentAgent,
2660
+ yes: false
2661
+ });
2662
+ }
2663
+ case "update": {
2664
+ if (state.outdated.length === 0) {
2665
+ p.log.success("All skills up to date");
2666
+ return;
2667
+ }
2668
+ const selected = await p.multiselect({
2669
+ message: "Select packages to update",
2670
+ options: state.outdated.map((s) => ({
2671
+ label: s.name,
2672
+ value: s.packageName || s.name,
2673
+ hint: `${s.info?.version ?? "unknown"} → ${s.latestVersion}`
2674
+ })),
2675
+ initialValues: state.outdated.map((s) => s.packageName || s.name)
2676
+ });
2677
+ if (p.isCancel(selected) || selected.length === 0) continue;
2678
+ return syncCommand(state, {
2679
+ packages: selected,
2680
+ global: false,
2681
+ agent: currentAgent,
2682
+ yes: false
2683
+ });
2684
+ }
2685
+ case "remove":
2686
+ await removeCommand(state, {
2687
+ global: false,
2688
+ agent: currentAgent,
2689
+ yes: false
2690
+ });
2691
+ continue;
2692
+ case "status":
2693
+ await statusCommand({ global: false });
2694
+ continue;
2695
+ case "config":
2696
+ await configCommand();
2697
+ continue;
410
2698
  }
411
- readmeContent = await res.text();
412
- }
413
- if (!readmeContent) {
414
- consola.error("README content is empty");
415
- return;
416
2699
  }
417
- const name = skillName || "package";
418
- let finalContent = readmeContent;
419
- if (config.optimize !== false && config.optimizeModel) {
420
- !config.quiet && consola.start(`Optimizing ${name} with ${config.optimizeModel}...`);
421
- const { optimized, wasOptimized } = await optimizeDocs(readmeContent, name, config.currentAgent || null, config.optimizeModel);
422
- finalContent = optimized;
423
- if (!config.quiet && wasOptimized) consola.success(`Optimized: ${readmeContent.length} → ${optimized.length} chars`);
424
- else if (!config.quiet && !wasOptimized) consola.warn(`Optimization skipped (no LLM available)`);
425
- }
426
- const { installed, paths } = installSkillForAgents(name, generateSkillMd({
427
- name,
428
- version: skillVersion,
429
- description
430
- }, finalContent), {
431
- global: config.global,
432
- agents: config.agents
433
- });
434
- if (!config.quiet) {
435
- consola.success(`Installed skill: ${name}`);
436
- consola.info(` Agents: ${installed.map((a) => agents[a].displayName).join(", ")}`);
437
- consola.info(` Paths: ${paths.join(", ")}`);
438
- }
439
- return;
440
- }
441
- if (!config.quiet) consola.start(`Generating skill from ${resolvedUrl}`);
442
- if (config.outputDir) {
443
- const result = await generateSkill({
444
- url: resolvedUrl,
445
- outputDir: config.outputDir,
446
- maxPages: config.maxPages,
447
- chunkSize: config.chunkSize,
448
- skipLlmsTxt: config.skipLlmsTxt,
449
- model: config.model
450
- }, config.quiet ? void 0 : ({ url: pageUrl, count, phase }) => {
451
- const icon = phase === "fetch" ? "📄" : "🔍";
452
- consola.info(`${icon} [${count}] ${pageUrl}`);
453
- });
454
- if (!config.quiet) {
455
- consola.success(`Generated skill: ${result.siteName}`);
456
- consola.info(` SKILL.md: ${result.skillPath}`);
457
- consola.info(` References: ${result.referencesDir}`);
458
- consola.info(` Database: ${result.dbPath}`);
459
- consola.info(` Chunks: ${result.chunkCount}`);
460
- }
461
- return;
462
- }
463
- const result = await generateSkill({
464
- url: resolvedUrl,
465
- outputDir: ".skilld-temp",
466
- maxPages: config.maxPages,
467
- chunkSize: config.chunkSize,
468
- skipLlmsTxt: config.skipLlmsTxt,
469
- model: config.model
470
- }, config.quiet ? void 0 : ({ url: pageUrl, count, phase }) => {
471
- if (phase === "fetch" && count % 5 === 0) consola.info(` [${count}] ${pageUrl}`);
472
- });
473
- const name = skillName || result.siteName;
474
- const body = `# ${name}
475
-
476
- This skill provides searchable documentation for ${name}.
477
-
478
- ## Search Database
479
- - Path: ${result.dbPath}
480
- - Chunks indexed: ${result.chunkCount}
481
-
482
- ## References
483
- Individual documentation chunks are available in the references directory.
484
- `;
485
- const { installed, paths } = installSkillForAgents(name, generateSkillMd({
486
- name,
487
- version: skillVersion,
488
- description
489
- }, body), {
490
- global: config.global,
491
- agents: config.agents
492
- });
493
- if (!config.quiet) {
494
- consola.success(`Installed skill: ${name}`);
495
- consola.info(` Agents: ${installed.map((a) => agents[a].displayName).join(", ")}`);
496
- consola.info(` Paths: ${paths.join(", ")}`);
497
- consola.info(` Chunks: ${result.chunkCount}`);
498
2700
  }
499
- }
500
- runMain(main);
501
- export {};
2701
+ }));
2702
+ export { defaultFeatures as a, writeLock as i, selectModel as n, readConfig as o, selectSkillSections as r, registerProject as s, ensureGitignore as t };
502
2703
 
503
2704
  //# sourceMappingURL=cli.mjs.map