skilld 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +21 -20
  2. package/dist/_chunks/agent.mjs +471 -17
  3. package/dist/_chunks/agent.mjs.map +1 -1
  4. package/dist/_chunks/assemble.mjs +2 -2
  5. package/dist/_chunks/assemble.mjs.map +1 -1
  6. package/dist/_chunks/cache.mjs +8 -2
  7. package/dist/_chunks/cache.mjs.map +1 -1
  8. package/dist/_chunks/cache2.mjs +2 -2
  9. package/dist/_chunks/cache2.mjs.map +1 -1
  10. package/dist/_chunks/cli-helpers.mjs +421 -0
  11. package/dist/_chunks/cli-helpers.mjs.map +1 -0
  12. package/dist/_chunks/detect.mjs +51 -22
  13. package/dist/_chunks/detect.mjs.map +1 -1
  14. package/dist/_chunks/detect2.mjs +2 -0
  15. package/dist/_chunks/embedding-cache.mjs +13 -4
  16. package/dist/_chunks/embedding-cache.mjs.map +1 -1
  17. package/dist/_chunks/formatting.mjs +1 -286
  18. package/dist/_chunks/formatting.mjs.map +1 -1
  19. package/dist/_chunks/index.d.mts.map +1 -1
  20. package/dist/_chunks/index2.d.mts.map +1 -1
  21. package/dist/_chunks/install.mjs +6 -4
  22. package/dist/_chunks/install.mjs.map +1 -1
  23. package/dist/_chunks/list.mjs +3 -2
  24. package/dist/_chunks/list.mjs.map +1 -1
  25. package/dist/_chunks/pool.mjs +3 -2
  26. package/dist/_chunks/pool.mjs.map +1 -1
  27. package/dist/_chunks/prompts.mjs +38 -4
  28. package/dist/_chunks/prompts.mjs.map +1 -1
  29. package/dist/_chunks/sanitize.mjs +7 -0
  30. package/dist/_chunks/sanitize.mjs.map +1 -1
  31. package/dist/_chunks/search-interactive.mjs +3 -2
  32. package/dist/_chunks/search-interactive.mjs.map +1 -1
  33. package/dist/_chunks/search.mjs +4 -3
  34. package/dist/_chunks/search.mjs.map +1 -1
  35. package/dist/_chunks/setup.mjs +27 -0
  36. package/dist/_chunks/setup.mjs.map +1 -0
  37. package/dist/_chunks/shared.mjs +6 -2
  38. package/dist/_chunks/shared.mjs.map +1 -1
  39. package/dist/_chunks/skills.mjs +1 -1
  40. package/dist/_chunks/sources.mjs +8 -8
  41. package/dist/_chunks/sources.mjs.map +1 -1
  42. package/dist/_chunks/sync.mjs +390 -108
  43. package/dist/_chunks/sync.mjs.map +1 -1
  44. package/dist/_chunks/uninstall.mjs +16 -2
  45. package/dist/_chunks/uninstall.mjs.map +1 -1
  46. package/dist/agent/index.d.mts +22 -4
  47. package/dist/agent/index.d.mts.map +1 -1
  48. package/dist/agent/index.mjs +3 -3
  49. package/dist/cli.mjs +619 -328
  50. package/dist/cli.mjs.map +1 -1
  51. package/dist/retriv/index.d.mts +18 -3
  52. package/dist/retriv/index.d.mts.map +1 -1
  53. package/dist/retriv/index.mjs +30 -1
  54. package/dist/retriv/index.mjs.map +1 -1
  55. package/dist/retriv/worker.d.mts +2 -0
  56. package/dist/retriv/worker.d.mts.map +1 -1
  57. package/dist/retriv/worker.mjs +1 -0
  58. package/dist/retriv/worker.mjs.map +1 -1
  59. package/dist/sources/index.mjs +1 -1
  60. package/package.json +9 -8
  61. package/dist/_chunks/chunk.mjs +0 -15
@@ -1,16 +1,17 @@
1
+ import { a as getModelLabel, i as getAvailableModels, o as getModelName, r as createToolProgress, s as optimizeDocs, t as detectImportedPackages } from "./agent.mjs";
1
2
  import { a as getRepoCacheDir, c as getVersionKey, i as getPackageDbPath, o as getCacheDir, t as CACHE_DIR } from "./config.mjs";
2
3
  import { n as sanitizeMarkdown } from "./sanitize.mjs";
3
- import { _ as resolvePkgDir, a as getShippedSkills, b as writeToRepoCache, c as linkCachedDir, d as linkRepoCachedDir, f as linkShippedSkill, 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 linkPkgNamed, y as writeToCache } from "./cache.mjs";
4
+ import { _ as resolvePkgDir, a as getShippedSkills, b as writeToRepoCache, c as linkCachedDir, d as linkRepoCachedDir, f as linkShippedSkill, g as readCachedSection, 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 linkPkgNamed, y as writeToCache } from "./cache.mjs";
4
5
  import "./yaml.mjs";
5
6
  import { i as parseFrontmatter } from "./markdown.mjs";
6
- import { SearchDepsUnavailableError, createIndex } from "../retriv/index.mjs";
7
- import { f as getPrereleaseChangelogRef, n as getSharedSkillsDir, s as getBlogPreset, t as SHARED_SKILLS_DIR } from "./shared.mjs";
7
+ import { SearchDepsUnavailableError, createIndex, listIndexIds } from "../retriv/index.mjs";
8
+ import { a as semverDiff, c as getBlogPreset, n as getSharedSkillsDir, p as getPrereleaseChangelogRef, t as SHARED_SKILLS_DIR } from "./shared.mjs";
8
9
  import { A as parseGitSkillInput, B as isGhAvailable, C as downloadLlmsDocs, D as normalizeLlmsLinks, F as formatDiscussionAsMarkdown, G as fetchReleaseNotes, H as toCrawlPattern, I as generateDiscussionIndex, K as generateReleaseIndex, L as fetchGitHubIssues, M as resolveEntryFiles, N as generateDocsIndex, P as fetchGitHubDiscussions, R as formatIssueAsMarkdown, T as fetchLlmsTxt, U as fetchBlogReleases, V as fetchCrawledDocs, Z as fetchGitHubRaw, b as isShallowGitDocs, f as resolvePackageDocsWithAttempts, h as fetchGitDocs, i as fetchPkgDist, k as fetchGitSkills, n as fetchNpmPackage, nt as parsePackageSpec, p as searchNpmPackages, q as isPrerelease, s as readLocalDependencies, tt as parseGitHubUrl, u as resolveLocalPackageDocs, v as fetchReadmeContent, x as resolveGitHubRepo, y as filterFrameworkDocs, z as generateIssueIndex } from "./sources.mjs";
9
10
  import { a as targets } from "./detect.mjs";
10
- import { a as sanitizeName, c as SECTION_OUTPUT_FILES, g as maxLines, h as maxItems, i as linkSkillToAgents, l as buildAllSectionPrompts, n as computeSkillDirName, p as portabilizePrompt, t as generateSkillMd } from "./prompts.mjs";
11
- import { a as getModelLabel, i as getAvailableModels, o as getModelName, r as createToolProgress, s as optimizeDocs, t as detectImportedPackages } from "./agent.mjs";
12
- import { C as hasCompletedWizard, E as registerProject, O as updateConfig, T as readConfig, d as getInstalledGenerators, h as promptForAgent, l as timedSpinner, m as isInteractive, n as formatDuration, p as introLine, v as resolveAgent, x as defaultFeatures, y as sharedArgs } from "./formatting.mjs";
11
+ import { a as sanitizeName, c as SECTION_OUTPUT_FILES, g as maxLines, h as maxItems, i as linkSkillToAgents, l as buildAllSectionPrompts, m as wrapSection, n as computeSkillDirName, p as portabilizePrompt, s as SECTION_MERGE_ORDER, t as generateSkillMd } from "./prompts.mjs";
12
+ import { C as registerProject, S as readConfig, T as updateConfig, b as hasCompletedWizard, c as isInteractive, d as pickModel, f as promptForAgent, g as sharedArgs, h as resolveAgent, i as getInstalledGenerators, s as introLine, t as NO_MODELS_MESSAGE, v as defaultFeatures } from "./cli-helpers.mjs";
13
13
  import { o as parsePackages, s as readLock, t as getProjectState, u as writeLock } from "./skills.mjs";
14
+ import { l as timedSpinner, n as formatDuration } from "./formatting.mjs";
14
15
  import { t as runWizard } from "../cli.mjs";
15
16
  import { n as shutdownWorker } from "./pool.mjs";
16
17
  import { dirname, join, relative, resolve } from "pathe";
@@ -121,9 +122,7 @@ function detectDocsType(packageName, version, repoUrl, llmsUrl) {
121
122
  function handleShippedSkills(packageName, version, cwd, agent, global) {
122
123
  const shippedSkills = getShippedSkills(packageName, cwd, version);
123
124
  if (shippedSkills.length === 0) return null;
124
- const shared = getSharedSkillsDir(cwd);
125
- const agentConfig = targets[agent];
126
- const baseDir = global ? join(CACHE_DIR, "skills") : shared || join(cwd, agentConfig.skillsDir);
125
+ const baseDir = resolveBaseDir(cwd, agent, global);
127
126
  mkdirSync(baseDir, { recursive: true });
128
127
  for (const shipped of shippedSkills) {
129
128
  linkShippedSkill(baseDir, shipped.skillName, shipped.skillDir);
@@ -143,7 +142,7 @@ function handleShippedSkills(packageName, version, cwd, agent, global) {
143
142
  }
144
143
  /** Resolve the base skills directory for an agent */
145
144
  function resolveBaseDir(cwd, agent, global) {
146
- if (global) return join(CACHE_DIR, "skills");
145
+ if (global) return targets[agent].globalSkillsDir;
147
146
  const shared = getSharedSkillsDir(cwd);
148
147
  if (shared) return shared;
149
148
  const agentConfig = targets[agent];
@@ -321,6 +320,7 @@ async function fetchAndCacheResources(opts) {
321
320
  }
322
321
  });
323
322
  }
323
+ if (docs.length > 0) docsType = "docs";
324
324
  }
325
325
  writeToCache(packageName, version, cachedDocs);
326
326
  }
@@ -541,13 +541,41 @@ async function fetchAndCacheResources(opts) {
541
541
  usedCache: useCache
542
542
  };
543
543
  }
544
- /** Index all resources into the search database (single batch) */
544
+ /**
545
+ * Extract the parent document ID from a chunk ID.
546
+ * Chunk IDs have the form "docId#chunk-N"; non-chunk IDs return as-is.
547
+ */
548
+ function parentDocId(id) {
549
+ const idx = id.indexOf("#chunk-");
550
+ return idx === -1 ? id : id.slice(0, idx);
551
+ }
552
+ /** Cap and sort docs by type priority, mutates and truncates allDocs in place */
553
+ function capDocs(allDocs, max, onProgress) {
554
+ if (allDocs.length <= max) return;
555
+ const TYPE_PRIORITY = {
556
+ doc: 0,
557
+ issue: 1,
558
+ discussion: 2,
559
+ release: 3,
560
+ source: 4,
561
+ types: 5
562
+ };
563
+ allDocs.sort((a, b) => {
564
+ const ta = TYPE_PRIORITY[a.metadata?.type || "doc"] ?? 3;
565
+ const tb = TYPE_PRIORITY[b.metadata?.type || "doc"] ?? 3;
566
+ if (ta !== tb) return ta - tb;
567
+ return a.id.localeCompare(b.id);
568
+ });
569
+ onProgress(`Indexing capped at ${max}/${allDocs.length} docs (prioritized by type)`);
570
+ allDocs.length = max;
571
+ }
572
+ /** Index all resources into the search database, with incremental support */
545
573
  async function indexResources(opts) {
546
574
  const { packageName, version, cwd, onProgress } = opts;
547
575
  const features = opts.features ?? readConfig().features ?? defaultFeatures;
548
576
  if (!features.search) return;
549
577
  const dbPath = getPackageDbPath(packageName, version);
550
- if (existsSync(dbPath)) return;
578
+ const dbExists = existsSync(dbPath);
551
579
  const allDocs = [...opts.docsToIndex];
552
580
  const pkgDir = resolvePkgDir(packageName, cwd, version);
553
581
  if (features.search && pkgDir) {
@@ -564,31 +592,54 @@ async function indexResources(opts) {
564
592
  });
565
593
  }
566
594
  if (allDocs.length === 0) return;
567
- if (allDocs.length > MAX_INDEX_DOCS) {
568
- const TYPE_PRIORITY = {
569
- doc: 0,
570
- issue: 1,
571
- discussion: 2,
572
- release: 3,
573
- source: 4,
574
- types: 5
575
- };
576
- allDocs.sort((a, b) => {
577
- const ta = TYPE_PRIORITY[a.metadata?.type || "doc"] ?? 3;
578
- const tb = TYPE_PRIORITY[b.metadata?.type || "doc"] ?? 3;
579
- if (ta !== tb) return ta - tb;
580
- return a.id.localeCompare(b.id);
581
- });
582
- onProgress(`Indexing capped at ${MAX_INDEX_DOCS}/${allDocs.length} docs (prioritized by type)`);
583
- allDocs.length = MAX_INDEX_DOCS;
595
+ capDocs(allDocs, MAX_INDEX_DOCS, onProgress);
596
+ if (!dbExists) {
597
+ onProgress(`Building search index (${allDocs.length} docs)`);
598
+ try {
599
+ await createIndex(allDocs, {
600
+ dbPath,
601
+ onProgress: ({ phase, current, total }) => {
602
+ if (phase === "storing") {
603
+ const d = allDocs[current - 1];
604
+ onProgress(`Storing ${d?.metadata?.type === "source" || d?.metadata?.type === "types" ? "code" : d?.metadata?.type || "doc"} (${current}/${total})`);
605
+ } else if (phase === "embedding") onProgress(`Creating embeddings (${current}/${total})`);
606
+ }
607
+ });
608
+ } catch (err) {
609
+ if (err instanceof SearchDepsUnavailableError) onProgress("Search indexing skipped (native deps unavailable)");
610
+ else throw err;
611
+ }
612
+ return;
613
+ }
614
+ let existingIds;
615
+ try {
616
+ existingIds = await listIndexIds({ dbPath });
617
+ } catch (err) {
618
+ if (err instanceof SearchDepsUnavailableError) {
619
+ onProgress("Search indexing skipped (native deps unavailable)");
620
+ return;
621
+ }
622
+ throw err;
623
+ }
624
+ const existingParentIds = new Set(existingIds.map(parentDocId));
625
+ const incomingIds = new Set(allDocs.map((d) => d.id));
626
+ const newDocs = allDocs.filter((d) => !existingParentIds.has(d.id));
627
+ const removeIds = existingIds.filter((id) => !incomingIds.has(parentDocId(id)));
628
+ if (newDocs.length === 0 && removeIds.length === 0) {
629
+ onProgress("Search index up to date");
630
+ return;
584
631
  }
585
- onProgress(`Building search index (${allDocs.length} docs)`);
632
+ const parts = [];
633
+ if (newDocs.length > 0) parts.push(`+${newDocs.length} new`);
634
+ if (removeIds.length > 0) parts.push(`-${removeIds.length} stale`);
635
+ onProgress(`Updating search index (${parts.join(", ")})`);
586
636
  try {
587
- await createIndex(allDocs, {
637
+ await createIndex(newDocs, {
588
638
  dbPath,
639
+ removeIds,
589
640
  onProgress: ({ phase, current, total }) => {
590
641
  if (phase === "storing") {
591
- const d = allDocs[current - 1];
642
+ const d = newDocs[current - 1];
592
643
  onProgress(`Storing ${d?.metadata?.type === "source" || d?.metadata?.type === "types" ? "code" : d?.metadata?.type || "doc"} (${current}/${total})`);
593
644
  } else if (phase === "embedding") onProgress(`Creating embeddings (${current}/${total})`);
594
645
  }
@@ -662,10 +713,13 @@ async function ensureGitignore(skillsDir, cwd, isGlobal) {
662
713
  }
663
714
  const SKILLD_MARKER_START = "<!-- skilld -->";
664
715
  const SKILLD_MARKER_END = "<!-- /skilld -->";
665
- const SKILLD_INSTRUCTIONS = `${SKILLD_MARKER_START}
666
- Before modifying code, evaluate each installed skill against the current task.
667
- For each skill, determine YES/NO relevance and invoke all YES skills before proceeding.
668
- ${SKILLD_MARKER_END}`;
716
+ const DEFAULT_SKILL_HINT = "Before modifying code, evaluate each installed skill against the current task.\nFor each skill, determine YES/NO relevance and invoke all YES skills before proceeding.";
717
+ function getSkillInstructions(agent) {
718
+ return `${SKILLD_MARKER_START}\n${targets[agent].skillActivationHint || DEFAULT_SKILL_HINT}\n${SKILLD_MARKER_END}`;
719
+ }
720
+ function getMdcSkillInstructions(agent) {
721
+ return `---\ndescription: "Activates installed skilld skills before code changes"\nalwaysApply: true\n---\n\n${targets[agent].skillActivationHint || DEFAULT_SKILL_HINT}`;
722
+ }
669
723
  /**
670
724
  * Check if agent instruction file has skilld skill-activation snippet.
671
725
  * If missing, prompt to add it. Skipped for global installs or agents without an instructionFile.
@@ -675,55 +729,55 @@ async function ensureAgentInstructions(agent, cwd, isGlobal) {
675
729
  const agentConfig = targets[agent];
676
730
  if (!agentConfig.instructionFile) return;
677
731
  const filePath = join(cwd, agentConfig.instructionFile);
732
+ if (agentConfig.instructionFile.endsWith(".mdc")) {
733
+ if (existsSync(filePath)) return;
734
+ const content = `${getMdcSkillInstructions(agent)}\n`;
735
+ if (!isInteractive()) {
736
+ mkdirSync(join(filePath, ".."), { recursive: true });
737
+ writeFileSync(filePath, content);
738
+ return;
739
+ }
740
+ p.note(`This tells your agent to check installed skills before making
741
+ code changes. Without it, skills are available but may not
742
+ activate automatically.
743
+
744
+ \x1B[90m${getMdcSkillInstructions(agent)}\x1B[0m`, `Create ${agentConfig.instructionFile}`);
745
+ const add = await p.confirm({
746
+ message: `Create ${agentConfig.instructionFile} with skill activation instructions?`,
747
+ initialValue: true
748
+ });
749
+ if (p.isCancel(add) || !add) return;
750
+ mkdirSync(join(filePath, ".."), { recursive: true });
751
+ writeFileSync(filePath, content);
752
+ p.log.success(`Created ${agentConfig.instructionFile}`);
753
+ return;
754
+ }
678
755
  if (existsSync(filePath)) {
679
756
  if (readFileSync(filePath, "utf-8").includes("<!-- skilld -->")) return;
680
757
  }
681
758
  if (!isInteractive()) {
682
- if (existsSync(filePath)) appendFileSync(filePath, `${readFileSync(filePath, "utf-8").endsWith("\n") ? "" : "\n"}\n${SKILLD_INSTRUCTIONS}\n`);
683
- else writeFileSync(filePath, `${SKILLD_INSTRUCTIONS}\n`);
759
+ if (existsSync(filePath)) appendFileSync(filePath, `${readFileSync(filePath, "utf-8").endsWith("\n") ? "" : "\n"}\n${getSkillInstructions(agent)}\n`);
760
+ else writeFileSync(filePath, `${getSkillInstructions(agent)}\n`);
684
761
  return;
685
762
  }
686
- p.note(SKILLD_INSTRUCTIONS, `Will be added to ${agentConfig.instructionFile}`);
763
+ const action = existsSync(filePath) ? "Append to" : "Create";
764
+ p.note(`This tells your agent to check installed skills before making
765
+ code changes. Without it, skills are available but may not
766
+ activate automatically.
767
+
768
+ \x1B[90m${getSkillInstructions(agent).replace(/\n/g, "\n")}\x1B[0m`, `${action} ${agentConfig.instructionFile}`);
687
769
  const add = await p.confirm({
688
- message: `Add skill activation instructions to ${agentConfig.instructionFile}?`,
770
+ message: `${action} ${agentConfig.instructionFile} with skill activation instructions?`,
689
771
  initialValue: true
690
772
  });
691
773
  if (p.isCancel(add) || !add) return;
692
- if (existsSync(filePath)) appendFileSync(filePath, `${readFileSync(filePath, "utf-8").endsWith("\n") ? "" : "\n"}\n${SKILLD_INSTRUCTIONS}\n`);
693
- else writeFileSync(filePath, `${SKILLD_INSTRUCTIONS}\n`);
774
+ if (existsSync(filePath)) appendFileSync(filePath, `${readFileSync(filePath, "utf-8").endsWith("\n") ? "" : "\n"}\n${getSkillInstructions(agent)}\n`);
775
+ else writeFileSync(filePath, `${getSkillInstructions(agent)}\n`);
694
776
  p.log.success(`Updated ${agentConfig.instructionFile}`);
695
777
  }
696
- /** Select LLM model for SKILL.md generation (independent of target agent) */
697
- async function selectModel(skipPrompt) {
698
- const config = readConfig();
699
- const available = await getAvailableModels();
700
- if (available.length === 0) {
701
- p.log.warn("No LLM CLIs found (claude, gemini, codex)");
702
- return null;
703
- }
704
- if (skipPrompt) {
705
- if (config.model && available.some((m) => m.id === config.model)) return config.model;
706
- return available.find((m) => m.recommended)?.id ?? available[0].id;
707
- }
708
- const modelChoice = await p.select({
709
- message: "Model for SKILL.md generation",
710
- options: available.map((m) => ({
711
- label: m.recommended ? `${m.name} (Recommended)` : m.name,
712
- value: m.id,
713
- hint: `${m.agentName} · ${m.hint}`
714
- })),
715
- initialValue: available.find((m) => m.recommended)?.id ?? available[0].id
716
- });
717
- if (p.isCancel(modelChoice)) {
718
- p.cancel("Cancelled");
719
- return null;
720
- }
721
- updateConfig({ model: modelChoice });
722
- return modelChoice;
723
- }
724
778
  /** Default sections when model is pre-set (non-interactive) */
725
779
  const DEFAULT_SECTIONS = ["best-practices", "api-changes"];
726
- async function selectSkillSections(message = "Generate SKILL.md with LLM") {
780
+ async function selectSkillSections(message = "Enhance SKILL.md") {
727
781
  p.log.info("Budgets adapt to package release density.");
728
782
  const selected = await p.multiselect({
729
783
  message,
@@ -806,27 +860,62 @@ async function selectSkillSections(message = "Generate SKILL.md with LLM") {
806
860
  * If presetModel is provided, uses DEFAULT_SECTIONS without prompting.
807
861
  * Returns null if cancelled or no sections/model selected.
808
862
  */
809
- async function selectLlmConfig(presetModel, message) {
810
- if (presetModel) return {
811
- model: presetModel,
812
- sections: DEFAULT_SECTIONS
813
- };
863
+ async function selectLlmConfig(presetModel, message, updateCtx) {
864
+ if (presetModel) {
865
+ if ((await getAvailableModels()).some((m) => m.id === presetModel)) return {
866
+ model: presetModel,
867
+ sections: DEFAULT_SECTIONS
868
+ };
869
+ if (!isInteractive()) return null;
870
+ }
814
871
  if (!isInteractive()) return null;
815
- const defaultModel = await selectModel(true);
816
- if (!defaultModel) return null;
872
+ const config = readConfig();
873
+ const available = await getAvailableModels();
874
+ if (available.length === 0) {
875
+ p.log.warn(NO_MODELS_MESSAGE);
876
+ return null;
877
+ }
878
+ let defaultModel;
879
+ if (config.model && available.some((m) => m.id === config.model)) defaultModel = config.model;
880
+ else {
881
+ if (config.model) p.log.warn(`Configured model \x1B[36m${config.model}\x1B[0m is unavailable — using auto-selected fallback`);
882
+ defaultModel = available.find((m) => m.recommended)?.id ?? available[0].id;
883
+ }
817
884
  const defaultModelName = getModelName(defaultModel);
885
+ const providerHint = available.find((m) => m.id === defaultModel)?.providerName ?? "";
886
+ const sourceHint = config.model === defaultModel ? "configured" : "recommended";
887
+ const defaultHint = providerHint ? `${providerHint} · ${sourceHint}` : sourceHint;
888
+ let enhanceMessage = "Enhance SKILL.md?";
889
+ let defaultToSkip = false;
890
+ if (updateCtx) {
891
+ const diff = updateCtx.bumpType ?? (updateCtx.oldVersion && updateCtx.newVersion ? semverDiff(updateCtx.oldVersion, updateCtx.newVersion) : null);
892
+ const isSmallBump = diff === "patch" || diff === "prerelease" || diff === "prepatch" || diff === "preminor" || diff === "premajor";
893
+ const ageParts = [];
894
+ if (diff) ageParts.push(diff);
895
+ if (updateCtx.syncedAt) {
896
+ const syncedAtMs = new Date(updateCtx.syncedAt).getTime();
897
+ if (Number.isFinite(syncedAtMs)) {
898
+ const days = Math.floor((Date.now() - syncedAtMs) / 864e5);
899
+ ageParts.push(days === 0 ? "today" : days === 1 ? "1d ago" : `${days}d ago`);
900
+ }
901
+ }
902
+ if (updateCtx.wasEnhanced) ageParts.push("LLM-enhanced");
903
+ const hint = [updateCtx.oldVersion && updateCtx.newVersion ? `${updateCtx.oldVersion} → ${updateCtx.newVersion}` : null, ...ageParts].filter(Boolean).join(" · ");
904
+ if (hint) enhanceMessage = `Enhance SKILL.md? \x1B[90m(${hint})\x1B[0m`;
905
+ if (updateCtx.wasEnhanced && isSmallBump) defaultToSkip = true;
906
+ }
818
907
  const choice = await p.select({
819
- message: "Generate enhanced SKILL.md?",
908
+ message: enhanceMessage,
820
909
  options: [
821
910
  {
822
911
  label: defaultModelName,
823
912
  value: "default",
824
- hint: "configured default"
913
+ hint: defaultHint
825
914
  },
826
915
  {
827
916
  label: "Different model",
828
917
  value: "pick",
829
- hint: "choose another model"
918
+ hint: "choose another enhancement model"
830
919
  },
831
920
  {
832
921
  label: "Prompt only",
@@ -836,9 +925,10 @@ async function selectLlmConfig(presetModel, message) {
836
925
  {
837
926
  label: "Skip",
838
927
  value: "skip",
839
- hint: "base skill only"
928
+ hint: "base skill with docs, issues, and types"
840
929
  }
841
- ]
930
+ ],
931
+ ...defaultToSkip ? { initialValue: "skip" } : {}
842
932
  });
843
933
  if (p.isCancel(choice)) return null;
844
934
  if (choice === "skip") return null;
@@ -852,10 +942,16 @@ async function selectLlmConfig(presetModel, message) {
852
942
  promptOnly: true
853
943
  };
854
944
  }
855
- const model = choice === "pick" ? await selectModel(false) : defaultModel;
945
+ let model;
946
+ if (choice === "pick") {
947
+ const picked = await pickModel(available);
948
+ if (!picked) return null;
949
+ updateConfig({ model: picked });
950
+ model = picked;
951
+ } else model = defaultModel;
856
952
  if (!model) return null;
857
953
  const modelName = getModelName(model);
858
- const { sections, customPrompt, cancelled } = await selectSkillSections(message ? `${message} (${modelName})` : `Generate SKILL.md with ${modelName}`);
954
+ const { sections, customPrompt, cancelled } = await selectSkillSections(message ? `${message} (${modelName})` : `Enhance SKILL.md with ${modelName}`);
859
955
  if (cancelled || sections.length === 0) return null;
860
956
  return {
861
957
  model,
@@ -922,7 +1018,7 @@ async function enhanceSkillWithLLM(opts) {
922
1018
  eject
923
1019
  });
924
1020
  writeFileSync(join(skillDir, "SKILL.md"), skillMd);
925
- } else llmLog.error(`LLM optimization failed${error ? `: ${error}` : ""}`);
1021
+ } else llmLog.error(`Enhancement failed${error ? `: ${error}` : ""}`);
926
1022
  }
927
1023
  /**
928
1024
  * Build and write PROMPT_*.md files for manual LLM use.
@@ -1302,10 +1398,93 @@ async function syncPackagesParallel(config) {
1302
1398
  p.log.success(skillMsg);
1303
1399
  for (const [, data] of skillData) for (const w of data.warnings) p.log.warn(`\x1B[33m${w}\x1B[0m`);
1304
1400
  if (errors.length > 0) for (const { pkg, reason } of errors) p.log.error(` ${pkg}: ${reason}`);
1401
+ const cachedPkgs = [];
1402
+ if (!config.force) for (const pkg of successfulPkgs) {
1403
+ const data = skillData.get(pkg);
1404
+ const resolvedName = data.resolved.name;
1405
+ if (DEFAULT_SECTIONS.every((s) => {
1406
+ const outputFile = SECTION_OUTPUT_FILES[s];
1407
+ return readCachedSection(resolvedName, data.version, outputFile) !== null;
1408
+ })) {
1409
+ const skillDir = join(resolveBaseDir(cwd, config.agent, config.global), data.skillDirName);
1410
+ const cachedParts = [];
1411
+ for (const s of SECTION_MERGE_ORDER) {
1412
+ if (!DEFAULT_SECTIONS.includes(s)) continue;
1413
+ const outputFile = SECTION_OUTPUT_FILES[s];
1414
+ const content = readCachedSection(resolvedName, data.version, outputFile);
1415
+ if (content) cachedParts.push(wrapSection(s, content));
1416
+ }
1417
+ const cachedBody = cachedParts.join("\n\n");
1418
+ const skillMd = generateSkillMd({
1419
+ name: resolvedName,
1420
+ version: data.version,
1421
+ releasedAt: data.resolved.releasedAt,
1422
+ dependencies: data.resolved.dependencies,
1423
+ distTags: data.resolved.distTags,
1424
+ body: cachedBody,
1425
+ relatedSkills: data.relatedSkills,
1426
+ hasIssues: data.hasIssues,
1427
+ hasDiscussions: data.hasDiscussions,
1428
+ hasReleases: data.hasReleases,
1429
+ hasChangelog: data.hasChangelog,
1430
+ docsType: data.docsType,
1431
+ hasShippedDocs: data.shippedDocs,
1432
+ pkgFiles: data.pkgFiles,
1433
+ generatedBy: "cached",
1434
+ dirName: data.skillDirName,
1435
+ packages: data.packages,
1436
+ repoUrl: data.resolved.repoUrl,
1437
+ features: data.features
1438
+ });
1439
+ writeFileSync(join(skillDir, "SKILL.md"), skillMd);
1440
+ cachedPkgs.push(pkg);
1441
+ }
1442
+ }
1443
+ const uncachedPkgs = successfulPkgs.filter((pkg) => !cachedPkgs.includes(pkg));
1444
+ if (cachedPkgs.length > 0) p.log.success(`Applied cached SKILL.md sections for ${cachedPkgs.join(", ")}`);
1305
1445
  const globalConfig = readConfig();
1306
- if (successfulPkgs.length > 0 && !globalConfig.skipLlm && !(config.yes && !config.model)) {
1307
- const llmConfig = await selectLlmConfig(config.model);
1308
- if (llmConfig?.promptOnly) for (const pkg of successfulPkgs) {
1446
+ let resolvedModel = config.model || (config.yes && !globalConfig.skipLlm ? globalConfig.model : void 0);
1447
+ if (!resolvedModel && config.yes && !globalConfig.skipLlm) {
1448
+ const { getAvailableModels } = await import("../agent/index.mjs");
1449
+ const available = await getAvailableModels();
1450
+ const auto = available.find((m) => m.recommended)?.id ?? available[0]?.id;
1451
+ if (auto) resolvedModel = auto;
1452
+ }
1453
+ if (uncachedPkgs.length > 0 && !globalConfig.skipLlm && !(config.yes && !resolvedModel)) {
1454
+ const DIFF_RANK = {
1455
+ major: 5,
1456
+ premajor: 4,
1457
+ minor: 3,
1458
+ preminor: 2,
1459
+ patch: 1,
1460
+ prepatch: 1,
1461
+ prerelease: 0
1462
+ };
1463
+ let parallelUpdateCtx;
1464
+ if (config.mode === "update") {
1465
+ let maxDiff = "";
1466
+ let allEnhanced = true;
1467
+ let anySyncedAt;
1468
+ for (const pkg of successfulPkgs) {
1469
+ const data = skillData.get(pkg);
1470
+ if (!data.wasEnhanced) allEnhanced = false;
1471
+ if (data.oldSyncedAt && (!anySyncedAt || data.oldSyncedAt < anySyncedAt)) anySyncedAt = data.oldSyncedAt;
1472
+ if (data.oldVersion) {
1473
+ const diff = semverDiff(data.oldVersion, data.version);
1474
+ if (diff && (DIFF_RANK[diff] ?? 0) > (DIFF_RANK[maxDiff] ?? -1)) maxDiff = diff;
1475
+ }
1476
+ }
1477
+ const first = skillData.get(successfulPkgs[0]);
1478
+ parallelUpdateCtx = {
1479
+ oldVersion: successfulPkgs.length === 1 ? first.oldVersion : void 0,
1480
+ newVersion: successfulPkgs.length === 1 ? first.version : void 0,
1481
+ syncedAt: anySyncedAt,
1482
+ wasEnhanced: allEnhanced,
1483
+ bumpType: maxDiff || void 0
1484
+ };
1485
+ }
1486
+ const llmConfig = await selectLlmConfig(resolvedModel, void 0, parallelUpdateCtx);
1487
+ if (llmConfig?.promptOnly) for (const pkg of uncachedPkgs) {
1309
1488
  const data = skillData.get(pkg);
1310
1489
  writePromptFiles({
1311
1490
  packageName: pkg,
@@ -1325,19 +1504,19 @@ async function syncPackagesParallel(config) {
1325
1504
  }
1326
1505
  else if (llmConfig) {
1327
1506
  p.log.step(getModelLabel(llmConfig.model));
1328
- for (const pkg of successfulPkgs) states.set(pkg, {
1507
+ for (const pkg of uncachedPkgs) states.set(pkg, {
1329
1508
  name: pkg,
1330
1509
  status: "pending",
1331
1510
  message: "Waiting..."
1332
1511
  });
1333
1512
  render();
1334
- const llmResults = await Promise.allSettled(successfulPkgs.map((pkg) => limit(() => enhanceWithLLM(pkg, skillData.get(pkg), {
1513
+ const llmResults = await Promise.allSettled(uncachedPkgs.map((pkg) => limit(() => enhanceWithLLM(pkg, skillData.get(pkg), {
1335
1514
  ...config,
1336
1515
  model: llmConfig.model
1337
1516
  }, cwd, update, llmConfig.sections, llmConfig.customPrompt))));
1338
1517
  logUpdate.done();
1339
1518
  const llmSucceeded = llmResults.filter((r) => r.status === "fulfilled").length;
1340
- p.log.success(`Enhanced ${llmSucceeded}/${successfulPkgs.length} skills with LLM`);
1519
+ p.log.success(`Enhanced ${llmSucceeded}/${uncachedPkgs.length} skills with LLM`);
1341
1520
  }
1342
1521
  }
1343
1522
  await ensureGitignore(getSharedSkillsDir(cwd) ? SHARED_SKILLS_DIR : agent.skillsDir, cwd, config.global);
@@ -1388,9 +1567,25 @@ async function syncBaseSkill(packageSpec, config, cwd, update) {
1388
1567
  if (useCache) update(packageName, "downloading", "Using cache", versionKey);
1389
1568
  else update(packageName, "downloading", config.force ? "Re-fetching docs..." : "Fetching docs...", versionKey);
1390
1569
  const baseDir = resolveBaseDir(cwd, config.agent, config.global);
1391
- const skillDirName = computeSkillDirName(packageName);
1570
+ let skillDirName = computeSkillDirName(packageName);
1571
+ if (config.mode === "update") {
1572
+ const lock = readLock(baseDir);
1573
+ if (lock) {
1574
+ for (const [name, info] of Object.entries(lock.skills)) if (info.packageName === packageName || parsePackages(info.packages).some((p) => p.name === packageName)) {
1575
+ skillDirName = name;
1576
+ break;
1577
+ }
1578
+ }
1579
+ }
1392
1580
  const skillDir = join(baseDir, skillDirName);
1393
1581
  mkdirSync(skillDir, { recursive: true });
1582
+ const preLock = config.mode === "update" ? readLock(baseDir)?.skills[skillDirName] : void 0;
1583
+ const preEnhanced = (() => {
1584
+ if (!preLock) return false;
1585
+ const skillMdPath = join(skillDir, "SKILL.md");
1586
+ if (!existsSync(skillMdPath)) return false;
1587
+ return !!parseFrontmatter(readFileSync(skillMdPath, "utf-8")).generated_by;
1588
+ })();
1394
1589
  const features = readConfig().features ?? defaultFeatures;
1395
1590
  const resources = await fetchAndCacheResources({
1396
1591
  packageName,
@@ -1469,7 +1664,10 @@ async function syncBaseSkill(packageSpec, config, cwd, update) {
1469
1664
  packages: allPackages.length > 1 ? allPackages : void 0,
1470
1665
  warnings: resources.warnings,
1471
1666
  features,
1472
- usedCache: resources.usedCache
1667
+ usedCache: resources.usedCache,
1668
+ oldVersion: preLock?.version,
1669
+ oldSyncedAt: preLock?.syncedAt,
1670
+ wasEnhanced: preEnhanced
1473
1671
  };
1474
1672
  }
1475
1673
  /** Phase 2: Enhance skill with LLM */
@@ -1533,13 +1731,23 @@ async function enhanceWithLLM(packageName, data, config, cwd, update, sections,
1533
1731
  }
1534
1732
  //#endregion
1535
1733
  //#region src/commands/sync.ts
1734
+ const RESOLVE_SOURCE_LABELS = {
1735
+ "npm": "npm registry",
1736
+ "github-docs": "GitHub versioned docs",
1737
+ "github-meta": "GitHub metadata",
1738
+ "github-search": "GitHub search",
1739
+ "readme": "README fallback",
1740
+ "llms.txt": "llms.txt convention",
1741
+ "crawl": "website crawl",
1742
+ "local": "local node_modules"
1743
+ };
1536
1744
  function showResolveAttempts(attempts) {
1537
1745
  if (attempts.length === 0) return;
1538
- p.log.message("\x1B[90mResolution attempts:\x1B[0m");
1746
+ p.log.message("\x1B[90mDoc resolution:\x1B[0m");
1539
1747
  for (const attempt of attempts) {
1540
1748
  const icon = attempt.status === "success" ? "\x1B[32m✓\x1B[0m" : "\x1B[90m✗\x1B[0m";
1541
- const source = `\x1B[90m${attempt.source}\x1B[0m`;
1542
- const msg = attempt.message ? ` - ${attempt.message}` : "";
1749
+ const source = `\x1B[90m${RESOLVE_SOURCE_LABELS[attempt.source] ?? attempt.source}\x1B[0m`;
1750
+ const msg = attempt.message ? ` \x1B[90m— ${attempt.message}\x1B[0m` : "";
1543
1751
  p.log.message(` ${icon} ${source}${msg}`);
1544
1752
  }
1545
1753
  }
@@ -1696,11 +1904,31 @@ async function syncSinglePackage(packageSpec, config) {
1696
1904
  }
1697
1905
  ensureCacheDir();
1698
1906
  const baseDir = resolveBaseDir(cwd, config.agent, config.global);
1699
- const skillDirName = config.name ? sanitizeName(config.name) : computeSkillDirName(packageName);
1907
+ let skillDirName = config.name ? sanitizeName(config.name) : computeSkillDirName(packageName);
1908
+ if (config.mode === "update" && !config.name) {
1909
+ const lock = readLock(baseDir);
1910
+ if (lock) {
1911
+ for (const [name, info] of Object.entries(lock.skills)) if (info.packageName === packageName || parsePackages(info.packages).some((p) => p.name === packageName)) {
1912
+ skillDirName = name;
1913
+ break;
1914
+ }
1915
+ }
1916
+ }
1700
1917
  const skillDir = config.eject ? typeof config.eject === "string" ? join(resolve(cwd, config.eject), skillDirName) : join(cwd, "skills", skillDirName) : join(baseDir, skillDirName);
1701
1918
  mkdirSync(skillDir, { recursive: true });
1702
1919
  const existingLock = config.eject ? void 0 : readLock(baseDir)?.skills[skillDirName];
1703
- if (existingLock && existingLock.packageName && existingLock.packageName !== packageName) {
1920
+ const isMerge = existingLock && existingLock.packageName && existingLock.packageName !== packageName;
1921
+ const updateCtx = config.mode === "update" && existingLock ? {
1922
+ oldVersion: existingLock.version,
1923
+ newVersion: version,
1924
+ syncedAt: existingLock.syncedAt,
1925
+ wasEnhanced: (() => {
1926
+ const skillMdPath = join(skillDir, "SKILL.md");
1927
+ if (!existsSync(skillMdPath)) return false;
1928
+ return !!parseFrontmatter(readFileSync(skillMdPath, "utf-8")).generated_by;
1929
+ })()
1930
+ } : void 0;
1931
+ if (isMerge) {
1704
1932
  spin.stop(`Merging ${packageName} into ${skillDirName}`);
1705
1933
  linkPkgNamed(skillDir, packageName, cwd, version);
1706
1934
  const repoSlug = resolved.repoUrl?.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?(?:[/#]|$)/)?.[1];
@@ -1815,8 +2043,54 @@ async function syncSinglePackage(packageSpec, config) {
1815
2043
  });
1816
2044
  writeFileSync(join(skillDir, "SKILL.md"), baseSkillMd);
1817
2045
  p.log.success(config.mode === "update" ? `Updated skill: ${relative(cwd, skillDir)}` : `Created base skill: ${relative(cwd, skillDir)}`);
1818
- if (!readConfig().skipLlm && (!config.yes || config.model)) {
1819
- const llmConfig = await selectLlmConfig(config.model);
2046
+ const allSectionsCached = !config.force && DEFAULT_SECTIONS.every((s) => {
2047
+ const outputFile = SECTION_OUTPUT_FILES[s];
2048
+ return readCachedSection(packageName, version, outputFile) !== null;
2049
+ });
2050
+ if (allSectionsCached) {
2051
+ const cachedParts = [];
2052
+ for (const s of SECTION_MERGE_ORDER) {
2053
+ if (!DEFAULT_SECTIONS.includes(s)) continue;
2054
+ const outputFile = SECTION_OUTPUT_FILES[s];
2055
+ const content = readCachedSection(packageName, version, outputFile);
2056
+ if (content) cachedParts.push(wrapSection(s, content));
2057
+ }
2058
+ const cachedBody = cachedParts.join("\n\n");
2059
+ const skillMd = generateSkillMd({
2060
+ name: packageName,
2061
+ version,
2062
+ releasedAt: resolved.releasedAt,
2063
+ description: resolved.description,
2064
+ dependencies: resolved.dependencies,
2065
+ distTags: resolved.distTags,
2066
+ body: cachedBody,
2067
+ relatedSkills,
2068
+ hasIssues: resources.hasIssues,
2069
+ hasDiscussions: resources.hasDiscussions,
2070
+ hasReleases: resources.hasReleases,
2071
+ hasChangelog,
2072
+ docsType: resources.docsType,
2073
+ hasShippedDocs: shippedDocs,
2074
+ pkgFiles,
2075
+ generatedBy: "cached",
2076
+ dirName: skillDirName,
2077
+ packages: allPackages.length > 1 ? allPackages : void 0,
2078
+ repoUrl: resolved.repoUrl,
2079
+ features,
2080
+ eject: isEject
2081
+ });
2082
+ writeFileSync(join(skillDir, "SKILL.md"), skillMd);
2083
+ p.log.success("Applied cached SKILL.md sections");
2084
+ }
2085
+ const globalConfig = readConfig();
2086
+ let resolvedModel = config.model || (config.yes && !globalConfig.skipLlm ? globalConfig.model : void 0);
2087
+ if (!resolvedModel && config.yes && !globalConfig.skipLlm) {
2088
+ const available = await getAvailableModels();
2089
+ const auto = available.find((m) => m.recommended)?.id ?? available[0]?.id;
2090
+ if (auto) resolvedModel = auto;
2091
+ }
2092
+ if (!allSectionsCached && !globalConfig.skipLlm && !(config.yes && !resolvedModel)) {
2093
+ const llmConfig = await selectLlmConfig(resolvedModel, void 0, updateCtx);
1820
2094
  if (llmConfig?.promptOnly) writePromptFiles({
1821
2095
  packageName,
1822
2096
  skillDir,
@@ -1876,7 +2150,8 @@ async function syncSinglePackage(packageSpec, config) {
1876
2150
  }
1877
2151
  await shutdownWorker();
1878
2152
  const ejectMsg = isEject ? " (ejected)" : "";
1879
- p.outro(config.mode === "update" ? `Updated ${packageName}${ejectMsg}` : `Synced ${packageName} to ${relative(cwd, skillDir)}${ejectMsg}`);
2153
+ const relDir = relative(cwd, skillDir);
2154
+ p.outro(config.mode === "update" ? `Updated ${packageName}${ejectMsg}` : `Synced ${packageName} → ${relDir}${ejectMsg}`);
1880
2155
  }
1881
2156
  const addCommandDef = defineCommand({
1882
2157
  meta: {
@@ -1913,7 +2188,7 @@ const addCommandDef = defineCommand({
1913
2188
  });
1914
2189
  return;
1915
2190
  }
1916
- if (!hasCompletedWizard()) await runWizard();
2191
+ if (!hasCompletedWizard()) await runWizard({ agent });
1917
2192
  const gitSources = [];
1918
2193
  const npmTokens = [];
1919
2194
  for (const input of rawInputs) {
@@ -1937,7 +2212,10 @@ const addCommandDef = defineCommand({
1937
2212
  if (npmTokens.length > 0) {
1938
2213
  const packages = [...new Set(npmTokens.flatMap((s) => s.split(/[,\s]+/)).map((s) => s.trim()).filter(Boolean))];
1939
2214
  const state = await getProjectState(cwd);
1940
- p.intro(introLine({ state }));
2215
+ p.intro(introLine({
2216
+ state,
2217
+ agentId: agent || void 0
2218
+ }));
1941
2219
  return syncCommand(state, {
1942
2220
  packages,
1943
2221
  global: args.global,
@@ -1986,9 +2264,12 @@ const ejectCommandDef = defineCommand({
1986
2264
  const cwd = process.cwd();
1987
2265
  const resolved = resolveAgent(args.agent);
1988
2266
  const agent = resolved && resolved !== "none" ? resolved : "claude-code";
1989
- if (!hasCompletedWizard()) await runWizard();
2267
+ if (!hasCompletedWizard()) await runWizard({ agent });
1990
2268
  const state = await getProjectState(cwd);
1991
- p.intro(introLine({ state }));
2269
+ p.intro(introLine({
2270
+ state,
2271
+ agentId: agent || void 0
2272
+ }));
1992
2273
  return syncCommand(state, {
1993
2274
  packages: [args.package],
1994
2275
  global: args.global,
@@ -2066,7 +2347,8 @@ const updateCommandDef = defineCommand({
2066
2347
  p.intro(introLine({
2067
2348
  state,
2068
2349
  generators,
2069
- modelId: config.model
2350
+ modelId: config.model,
2351
+ agentId: config.agent || agent || void 0
2070
2352
  }));
2071
2353
  }
2072
2354
  if (args.package) return syncCommand(state, {
@@ -2126,7 +2408,7 @@ async function exportPortablePrompts(packageSpec, opts) {
2126
2408
  ensureCacheDir();
2127
2409
  const skillDirName = computeSkillDirName(packageName);
2128
2410
  const features = readConfig().features ?? defaultFeatures;
2129
- const agent = opts.agent === "none" ? null : opts.agent ?? await import("./detect.mjs").then((n) => n.r).then((m) => m.detectTargetAgent());
2411
+ const agent = opts.agent === "none" ? null : opts.agent ?? await import("./detect2.mjs").then((m) => m.detectTargetAgent());
2130
2412
  const baseDir = agent ? resolveBaseDir(cwd, agent, false) : join(cwd, ".claude", "skills");
2131
2413
  const skillDir = opts.out ? resolve(cwd, opts.out) : join(baseDir, skillDirName);
2132
2414
  if (existsSync(skillDir) && !opts.force) {