skilld 1.2.3 → 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 (57) 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/install.mjs +4 -3
  21. package/dist/_chunks/install.mjs.map +1 -1
  22. package/dist/_chunks/list.mjs +3 -2
  23. package/dist/_chunks/list.mjs.map +1 -1
  24. package/dist/_chunks/pool.mjs +3 -2
  25. package/dist/_chunks/pool.mjs.map +1 -1
  26. package/dist/_chunks/prompts.mjs +38 -4
  27. package/dist/_chunks/prompts.mjs.map +1 -1
  28. package/dist/_chunks/search-interactive.mjs +3 -2
  29. package/dist/_chunks/search-interactive.mjs.map +1 -1
  30. package/dist/_chunks/search.mjs +4 -3
  31. package/dist/_chunks/search.mjs.map +1 -1
  32. package/dist/_chunks/setup.mjs +27 -0
  33. package/dist/_chunks/setup.mjs.map +1 -0
  34. package/dist/_chunks/shared.mjs +6 -2
  35. package/dist/_chunks/shared.mjs.map +1 -1
  36. package/dist/_chunks/skills.mjs +1 -1
  37. package/dist/_chunks/sources.mjs +1 -1
  38. package/dist/_chunks/sync.mjs +389 -108
  39. package/dist/_chunks/sync.mjs.map +1 -1
  40. package/dist/_chunks/uninstall.mjs +16 -2
  41. package/dist/_chunks/uninstall.mjs.map +1 -1
  42. package/dist/agent/index.d.mts +22 -4
  43. package/dist/agent/index.d.mts.map +1 -1
  44. package/dist/agent/index.mjs +3 -3
  45. package/dist/cli.mjs +619 -328
  46. package/dist/cli.mjs.map +1 -1
  47. package/dist/retriv/index.d.mts +18 -3
  48. package/dist/retriv/index.d.mts.map +1 -1
  49. package/dist/retriv/index.mjs +30 -1
  50. package/dist/retriv/index.mjs.map +1 -1
  51. package/dist/retriv/worker.d.mts +2 -0
  52. package/dist/retriv/worker.d.mts.map +1 -1
  53. package/dist/retriv/worker.mjs +1 -0
  54. package/dist/retriv/worker.mjs.map +1 -1
  55. package/dist/sources/index.mjs +1 -1
  56. package/package.json +3 -2
  57. 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];
@@ -542,13 +541,41 @@ async function fetchAndCacheResources(opts) {
542
541
  usedCache: useCache
543
542
  };
544
543
  }
545
- /** 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 */
546
573
  async function indexResources(opts) {
547
574
  const { packageName, version, cwd, onProgress } = opts;
548
575
  const features = opts.features ?? readConfig().features ?? defaultFeatures;
549
576
  if (!features.search) return;
550
577
  const dbPath = getPackageDbPath(packageName, version);
551
- if (existsSync(dbPath)) return;
578
+ const dbExists = existsSync(dbPath);
552
579
  const allDocs = [...opts.docsToIndex];
553
580
  const pkgDir = resolvePkgDir(packageName, cwd, version);
554
581
  if (features.search && pkgDir) {
@@ -565,31 +592,54 @@ async function indexResources(opts) {
565
592
  });
566
593
  }
567
594
  if (allDocs.length === 0) return;
568
- if (allDocs.length > MAX_INDEX_DOCS) {
569
- const TYPE_PRIORITY = {
570
- doc: 0,
571
- issue: 1,
572
- discussion: 2,
573
- release: 3,
574
- source: 4,
575
- types: 5
576
- };
577
- allDocs.sort((a, b) => {
578
- const ta = TYPE_PRIORITY[a.metadata?.type || "doc"] ?? 3;
579
- const tb = TYPE_PRIORITY[b.metadata?.type || "doc"] ?? 3;
580
- if (ta !== tb) return ta - tb;
581
- return a.id.localeCompare(b.id);
582
- });
583
- onProgress(`Indexing capped at ${MAX_INDEX_DOCS}/${allDocs.length} docs (prioritized by type)`);
584
- 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;
585
631
  }
586
- 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(", ")})`);
587
636
  try {
588
- await createIndex(allDocs, {
637
+ await createIndex(newDocs, {
589
638
  dbPath,
639
+ removeIds,
590
640
  onProgress: ({ phase, current, total }) => {
591
641
  if (phase === "storing") {
592
- const d = allDocs[current - 1];
642
+ const d = newDocs[current - 1];
593
643
  onProgress(`Storing ${d?.metadata?.type === "source" || d?.metadata?.type === "types" ? "code" : d?.metadata?.type || "doc"} (${current}/${total})`);
594
644
  } else if (phase === "embedding") onProgress(`Creating embeddings (${current}/${total})`);
595
645
  }
@@ -663,10 +713,13 @@ async function ensureGitignore(skillsDir, cwd, isGlobal) {
663
713
  }
664
714
  const SKILLD_MARKER_START = "<!-- skilld -->";
665
715
  const SKILLD_MARKER_END = "<!-- /skilld -->";
666
- const SKILLD_INSTRUCTIONS = `${SKILLD_MARKER_START}
667
- Before modifying code, evaluate each installed skill against the current task.
668
- For each skill, determine YES/NO relevance and invoke all YES skills before proceeding.
669
- ${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
+ }
670
723
  /**
671
724
  * Check if agent instruction file has skilld skill-activation snippet.
672
725
  * If missing, prompt to add it. Skipped for global installs or agents without an instructionFile.
@@ -676,55 +729,55 @@ async function ensureAgentInstructions(agent, cwd, isGlobal) {
676
729
  const agentConfig = targets[agent];
677
730
  if (!agentConfig.instructionFile) return;
678
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
+ }
679
755
  if (existsSync(filePath)) {
680
756
  if (readFileSync(filePath, "utf-8").includes("<!-- skilld -->")) return;
681
757
  }
682
758
  if (!isInteractive()) {
683
- if (existsSync(filePath)) appendFileSync(filePath, `${readFileSync(filePath, "utf-8").endsWith("\n") ? "" : "\n"}\n${SKILLD_INSTRUCTIONS}\n`);
684
- 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`);
685
761
  return;
686
762
  }
687
- 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}`);
688
769
  const add = await p.confirm({
689
- message: `Add skill activation instructions to ${agentConfig.instructionFile}?`,
770
+ message: `${action} ${agentConfig.instructionFile} with skill activation instructions?`,
690
771
  initialValue: true
691
772
  });
692
773
  if (p.isCancel(add) || !add) return;
693
- if (existsSync(filePath)) appendFileSync(filePath, `${readFileSync(filePath, "utf-8").endsWith("\n") ? "" : "\n"}\n${SKILLD_INSTRUCTIONS}\n`);
694
- 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`);
695
776
  p.log.success(`Updated ${agentConfig.instructionFile}`);
696
777
  }
697
- /** Select LLM model for SKILL.md generation (independent of target agent) */
698
- async function selectModel(skipPrompt) {
699
- const config = readConfig();
700
- const available = await getAvailableModels();
701
- if (available.length === 0) {
702
- p.log.warn("No LLM CLIs found (claude, gemini, codex)");
703
- return null;
704
- }
705
- if (skipPrompt) {
706
- if (config.model && available.some((m) => m.id === config.model)) return config.model;
707
- return available.find((m) => m.recommended)?.id ?? available[0].id;
708
- }
709
- const modelChoice = await p.select({
710
- message: "Model for SKILL.md generation",
711
- options: available.map((m) => ({
712
- label: m.recommended ? `${m.name} (Recommended)` : m.name,
713
- value: m.id,
714
- hint: `${m.agentName} · ${m.hint}`
715
- })),
716
- initialValue: available.find((m) => m.recommended)?.id ?? available[0].id
717
- });
718
- if (p.isCancel(modelChoice)) {
719
- p.cancel("Cancelled");
720
- return null;
721
- }
722
- updateConfig({ model: modelChoice });
723
- return modelChoice;
724
- }
725
778
  /** Default sections when model is pre-set (non-interactive) */
726
779
  const DEFAULT_SECTIONS = ["best-practices", "api-changes"];
727
- async function selectSkillSections(message = "Generate SKILL.md with LLM") {
780
+ async function selectSkillSections(message = "Enhance SKILL.md") {
728
781
  p.log.info("Budgets adapt to package release density.");
729
782
  const selected = await p.multiselect({
730
783
  message,
@@ -807,27 +860,62 @@ async function selectSkillSections(message = "Generate SKILL.md with LLM") {
807
860
  * If presetModel is provided, uses DEFAULT_SECTIONS without prompting.
808
861
  * Returns null if cancelled or no sections/model selected.
809
862
  */
810
- async function selectLlmConfig(presetModel, message) {
811
- if (presetModel) return {
812
- model: presetModel,
813
- sections: DEFAULT_SECTIONS
814
- };
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
+ }
815
871
  if (!isInteractive()) return null;
816
- const defaultModel = await selectModel(true);
817
- 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
+ }
818
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
+ }
819
907
  const choice = await p.select({
820
- message: "Generate enhanced SKILL.md?",
908
+ message: enhanceMessage,
821
909
  options: [
822
910
  {
823
911
  label: defaultModelName,
824
912
  value: "default",
825
- hint: "configured default"
913
+ hint: defaultHint
826
914
  },
827
915
  {
828
916
  label: "Different model",
829
917
  value: "pick",
830
- hint: "choose another model"
918
+ hint: "choose another enhancement model"
831
919
  },
832
920
  {
833
921
  label: "Prompt only",
@@ -837,9 +925,10 @@ async function selectLlmConfig(presetModel, message) {
837
925
  {
838
926
  label: "Skip",
839
927
  value: "skip",
840
- hint: "base skill only"
928
+ hint: "base skill with docs, issues, and types"
841
929
  }
842
- ]
930
+ ],
931
+ ...defaultToSkip ? { initialValue: "skip" } : {}
843
932
  });
844
933
  if (p.isCancel(choice)) return null;
845
934
  if (choice === "skip") return null;
@@ -853,10 +942,16 @@ async function selectLlmConfig(presetModel, message) {
853
942
  promptOnly: true
854
943
  };
855
944
  }
856
- 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;
857
952
  if (!model) return null;
858
953
  const modelName = getModelName(model);
859
- 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}`);
860
955
  if (cancelled || sections.length === 0) return null;
861
956
  return {
862
957
  model,
@@ -923,7 +1018,7 @@ async function enhanceSkillWithLLM(opts) {
923
1018
  eject
924
1019
  });
925
1020
  writeFileSync(join(skillDir, "SKILL.md"), skillMd);
926
- } else llmLog.error(`LLM optimization failed${error ? `: ${error}` : ""}`);
1021
+ } else llmLog.error(`Enhancement failed${error ? `: ${error}` : ""}`);
927
1022
  }
928
1023
  /**
929
1024
  * Build and write PROMPT_*.md files for manual LLM use.
@@ -1303,10 +1398,93 @@ async function syncPackagesParallel(config) {
1303
1398
  p.log.success(skillMsg);
1304
1399
  for (const [, data] of skillData) for (const w of data.warnings) p.log.warn(`\x1B[33m${w}\x1B[0m`);
1305
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(", ")}`);
1306
1445
  const globalConfig = readConfig();
1307
- if (successfulPkgs.length > 0 && !globalConfig.skipLlm && !(config.yes && !config.model)) {
1308
- const llmConfig = await selectLlmConfig(config.model);
1309
- 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) {
1310
1488
  const data = skillData.get(pkg);
1311
1489
  writePromptFiles({
1312
1490
  packageName: pkg,
@@ -1326,19 +1504,19 @@ async function syncPackagesParallel(config) {
1326
1504
  }
1327
1505
  else if (llmConfig) {
1328
1506
  p.log.step(getModelLabel(llmConfig.model));
1329
- for (const pkg of successfulPkgs) states.set(pkg, {
1507
+ for (const pkg of uncachedPkgs) states.set(pkg, {
1330
1508
  name: pkg,
1331
1509
  status: "pending",
1332
1510
  message: "Waiting..."
1333
1511
  });
1334
1512
  render();
1335
- 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), {
1336
1514
  ...config,
1337
1515
  model: llmConfig.model
1338
1516
  }, cwd, update, llmConfig.sections, llmConfig.customPrompt))));
1339
1517
  logUpdate.done();
1340
1518
  const llmSucceeded = llmResults.filter((r) => r.status === "fulfilled").length;
1341
- p.log.success(`Enhanced ${llmSucceeded}/${successfulPkgs.length} skills with LLM`);
1519
+ p.log.success(`Enhanced ${llmSucceeded}/${uncachedPkgs.length} skills with LLM`);
1342
1520
  }
1343
1521
  }
1344
1522
  await ensureGitignore(getSharedSkillsDir(cwd) ? SHARED_SKILLS_DIR : agent.skillsDir, cwd, config.global);
@@ -1389,9 +1567,25 @@ async function syncBaseSkill(packageSpec, config, cwd, update) {
1389
1567
  if (useCache) update(packageName, "downloading", "Using cache", versionKey);
1390
1568
  else update(packageName, "downloading", config.force ? "Re-fetching docs..." : "Fetching docs...", versionKey);
1391
1569
  const baseDir = resolveBaseDir(cwd, config.agent, config.global);
1392
- 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
+ }
1393
1580
  const skillDir = join(baseDir, skillDirName);
1394
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
+ })();
1395
1589
  const features = readConfig().features ?? defaultFeatures;
1396
1590
  const resources = await fetchAndCacheResources({
1397
1591
  packageName,
@@ -1470,7 +1664,10 @@ async function syncBaseSkill(packageSpec, config, cwd, update) {
1470
1664
  packages: allPackages.length > 1 ? allPackages : void 0,
1471
1665
  warnings: resources.warnings,
1472
1666
  features,
1473
- usedCache: resources.usedCache
1667
+ usedCache: resources.usedCache,
1668
+ oldVersion: preLock?.version,
1669
+ oldSyncedAt: preLock?.syncedAt,
1670
+ wasEnhanced: preEnhanced
1474
1671
  };
1475
1672
  }
1476
1673
  /** Phase 2: Enhance skill with LLM */
@@ -1534,13 +1731,23 @@ async function enhanceWithLLM(packageName, data, config, cwd, update, sections,
1534
1731
  }
1535
1732
  //#endregion
1536
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
+ };
1537
1744
  function showResolveAttempts(attempts) {
1538
1745
  if (attempts.length === 0) return;
1539
- p.log.message("\x1B[90mResolution attempts:\x1B[0m");
1746
+ p.log.message("\x1B[90mDoc resolution:\x1B[0m");
1540
1747
  for (const attempt of attempts) {
1541
1748
  const icon = attempt.status === "success" ? "\x1B[32m✓\x1B[0m" : "\x1B[90m✗\x1B[0m";
1542
- const source = `\x1B[90m${attempt.source}\x1B[0m`;
1543
- 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` : "";
1544
1751
  p.log.message(` ${icon} ${source}${msg}`);
1545
1752
  }
1546
1753
  }
@@ -1697,11 +1904,31 @@ async function syncSinglePackage(packageSpec, config) {
1697
1904
  }
1698
1905
  ensureCacheDir();
1699
1906
  const baseDir = resolveBaseDir(cwd, config.agent, config.global);
1700
- 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
+ }
1701
1917
  const skillDir = config.eject ? typeof config.eject === "string" ? join(resolve(cwd, config.eject), skillDirName) : join(cwd, "skills", skillDirName) : join(baseDir, skillDirName);
1702
1918
  mkdirSync(skillDir, { recursive: true });
1703
1919
  const existingLock = config.eject ? void 0 : readLock(baseDir)?.skills[skillDirName];
1704
- 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) {
1705
1932
  spin.stop(`Merging ${packageName} into ${skillDirName}`);
1706
1933
  linkPkgNamed(skillDir, packageName, cwd, version);
1707
1934
  const repoSlug = resolved.repoUrl?.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?(?:[/#]|$)/)?.[1];
@@ -1816,8 +2043,54 @@ async function syncSinglePackage(packageSpec, config) {
1816
2043
  });
1817
2044
  writeFileSync(join(skillDir, "SKILL.md"), baseSkillMd);
1818
2045
  p.log.success(config.mode === "update" ? `Updated skill: ${relative(cwd, skillDir)}` : `Created base skill: ${relative(cwd, skillDir)}`);
1819
- if (!readConfig().skipLlm && (!config.yes || config.model)) {
1820
- 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);
1821
2094
  if (llmConfig?.promptOnly) writePromptFiles({
1822
2095
  packageName,
1823
2096
  skillDir,
@@ -1877,7 +2150,8 @@ async function syncSinglePackage(packageSpec, config) {
1877
2150
  }
1878
2151
  await shutdownWorker();
1879
2152
  const ejectMsg = isEject ? " (ejected)" : "";
1880
- 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}`);
1881
2155
  }
1882
2156
  const addCommandDef = defineCommand({
1883
2157
  meta: {
@@ -1914,7 +2188,7 @@ const addCommandDef = defineCommand({
1914
2188
  });
1915
2189
  return;
1916
2190
  }
1917
- if (!hasCompletedWizard()) await runWizard();
2191
+ if (!hasCompletedWizard()) await runWizard({ agent });
1918
2192
  const gitSources = [];
1919
2193
  const npmTokens = [];
1920
2194
  for (const input of rawInputs) {
@@ -1938,7 +2212,10 @@ const addCommandDef = defineCommand({
1938
2212
  if (npmTokens.length > 0) {
1939
2213
  const packages = [...new Set(npmTokens.flatMap((s) => s.split(/[,\s]+/)).map((s) => s.trim()).filter(Boolean))];
1940
2214
  const state = await getProjectState(cwd);
1941
- p.intro(introLine({ state }));
2215
+ p.intro(introLine({
2216
+ state,
2217
+ agentId: agent || void 0
2218
+ }));
1942
2219
  return syncCommand(state, {
1943
2220
  packages,
1944
2221
  global: args.global,
@@ -1987,9 +2264,12 @@ const ejectCommandDef = defineCommand({
1987
2264
  const cwd = process.cwd();
1988
2265
  const resolved = resolveAgent(args.agent);
1989
2266
  const agent = resolved && resolved !== "none" ? resolved : "claude-code";
1990
- if (!hasCompletedWizard()) await runWizard();
2267
+ if (!hasCompletedWizard()) await runWizard({ agent });
1991
2268
  const state = await getProjectState(cwd);
1992
- p.intro(introLine({ state }));
2269
+ p.intro(introLine({
2270
+ state,
2271
+ agentId: agent || void 0
2272
+ }));
1993
2273
  return syncCommand(state, {
1994
2274
  packages: [args.package],
1995
2275
  global: args.global,
@@ -2067,7 +2347,8 @@ const updateCommandDef = defineCommand({
2067
2347
  p.intro(introLine({
2068
2348
  state,
2069
2349
  generators,
2070
- modelId: config.model
2350
+ modelId: config.model,
2351
+ agentId: config.agent || agent || void 0
2071
2352
  }));
2072
2353
  }
2073
2354
  if (args.package) return syncCommand(state, {
@@ -2127,7 +2408,7 @@ async function exportPortablePrompts(packageSpec, opts) {
2127
2408
  ensureCacheDir();
2128
2409
  const skillDirName = computeSkillDirName(packageName);
2129
2410
  const features = readConfig().features ?? defaultFeatures;
2130
- 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());
2131
2412
  const baseDir = agent ? resolveBaseDir(cwd, agent, false) : join(cwd, ".claude", "skills");
2132
2413
  const skillDir = opts.out ? resolve(cwd, opts.out) : join(baseDir, skillDirName);
2133
2414
  if (existsSync(skillDir) && !opts.force) {