skilld 0.9.4 → 0.9.6

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.
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk.mjs";
2
2
  import { _ as writeSections, b as sanitizeMarkdown, h as readCachedSection, y as repairMarkdown } from "./storage.mjs";
3
- import { o as mapInsert, t as yamlEscape, u as getFilePatterns } from "./yaml.mjs";
3
+ import { o as getFilePatterns, s as getPackageRules, t as yamlEscape } from "./yaml.mjs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join, relative } from "pathe";
6
6
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
@@ -8,6 +8,7 @@ import { exec, spawn, spawnSync } from "node:child_process";
8
8
  import { globby } from "globby";
9
9
  import { findDynamicImports, findStaticImports } from "mlly";
10
10
  import { createHash } from "node:crypto";
11
+ import { setTimeout } from "node:timers/promises";
11
12
  import { promisify } from "node:util";
12
13
  import { readFile } from "node:fs/promises";
13
14
  import { parseSync } from "oxc-parser";
@@ -558,7 +559,7 @@ function budgetScale(sectionCount) {
558
559
  if (sectionCount === 3) return .7;
559
560
  return .6;
560
561
  }
561
- function apiChangesSection({ packageName, version, hasReleases, hasChangelog, hasIssues, hasDiscussions, features, enabledSectionCount }) {
562
+ function apiChangesSection({ packageName, version, hasReleases, hasChangelog, hasDocs, hasIssues, hasDiscussions, features, enabledSectionCount }) {
562
563
  const [, major, minor] = version?.match(/^(\d+)\.(\d+)/) ?? [];
563
564
  const searchHints = [];
564
565
  if (features?.search !== false) {
@@ -587,11 +588,11 @@ function apiChangesSection({ packageName, version, hasReleases, hasChangelog, ha
587
588
  });
588
589
  if (hasChangelog) referenceWeights.push({
589
590
  name: "Changelog",
590
- path: `./.skilld/pkg/${hasChangelog}`,
591
+ path: `./.skilld/${hasChangelog}`,
591
592
  score: 9,
592
593
  useFor: "Features/Breaking Changes sections per version"
593
594
  });
594
- referenceWeights.push({
595
+ if (hasDocs) referenceWeights.push({
595
596
  name: "Docs",
596
597
  path: "./.skilld/docs/",
597
598
  score: 4,
@@ -646,15 +647,16 @@ Each item: ⚠️ (breaking/deprecated) or ✨ (new) + API name + what changed +
646
647
  ].filter(Boolean)
647
648
  };
648
649
  }
649
- function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasReleases, hasChangelog, features, enabledSectionCount }) {
650
+ function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasReleases, hasChangelog, hasDocs, features, enabledSectionCount }) {
650
651
  const searchHints = [];
651
652
  if (features?.search !== false) searchHints.push(`\`npx -y skilld search "recommended" -p ${packageName}\``, `\`npx -y skilld search "avoid" -p ${packageName}\``);
652
- const referenceWeights = [{
653
+ const referenceWeights = [];
654
+ if (hasDocs) referenceWeights.push({
653
655
  name: "Docs",
654
656
  path: "./.skilld/docs/",
655
657
  score: 9,
656
658
  useFor: "Primary source — recommended patterns, configuration, idiomatic usage"
657
- }];
659
+ });
658
660
  if (hasDiscussions) referenceWeights.push({
659
661
  name: "Discussions",
660
662
  path: "./.skilld/discussions/_INDEX.md",
@@ -675,7 +677,7 @@ function bestPracticesSection({ packageName, hasIssues, hasDiscussions, hasRelea
675
677
  });
676
678
  if (hasChangelog) referenceWeights.push({
677
679
  name: "Changelog",
678
- path: `./.skilld/pkg/${hasChangelog}`,
680
+ path: `./.skilld/${hasChangelog}`,
679
681
  score: 3,
680
682
  useFor: "Only for new patterns introduced in recent versions"
681
683
  });
@@ -723,13 +725,14 @@ Content addressing the user's instructions above, using concise examples and sou
723
725
  rules: [`- **Custom section "${heading}":** MAX ${maxLines(50, 80, enabledSectionCount)} lines, use \`## ${heading}\` heading`]
724
726
  };
725
727
  }
726
- function apiSection({ hasReleases, hasChangelog, hasIssues, hasDiscussions, enabledSectionCount }) {
727
- const referenceWeights = [{
728
+ function apiSection({ hasReleases, hasChangelog, hasDocs, hasIssues, hasDiscussions, enabledSectionCount }) {
729
+ const referenceWeights = [];
730
+ if (hasDocs) referenceWeights.push({
728
731
  name: "Docs",
729
732
  path: "./.skilld/docs/",
730
733
  score: 10,
731
734
  useFor: "Primary source — scan all doc pages for export names"
732
- }];
735
+ });
733
736
  if (hasReleases) referenceWeights.push({
734
737
  name: "Releases",
735
738
  path: "./.skilld/releases/_INDEX.md",
@@ -738,7 +741,7 @@ function apiSection({ hasReleases, hasChangelog, hasIssues, hasDiscussions, enab
738
741
  });
739
742
  if (hasChangelog) referenceWeights.push({
740
743
  name: "Changelog",
741
- path: `./.skilld/pkg/${hasChangelog}`,
744
+ path: `./.skilld/${hasChangelog}`,
742
745
  score: 5,
743
746
  useFor: "New APIs added in recent versions"
744
747
  });
@@ -815,7 +818,7 @@ function generateImportantBlock({ packageName, hasIssues, hasDiscussions, hasRel
815
818
  const rows = [["Docs", hasShippedDocs ? `\`${skillDir}/.skilld/pkg/docs/\` or \`${skillDir}/.skilld/pkg/README.md\`` : docsType === "llms.txt" ? `\`${skillDir}/.skilld/docs/llms.txt\`` : docsType === "readme" ? `\`${skillDir}/.skilld/pkg/README.md\`` : `\`${skillDir}/.skilld/docs/\``], ["Package", `\`${skillDir}/.skilld/pkg/\``]];
816
819
  if (hasIssues) rows.push(["Issues", `\`${skillDir}/.skilld/issues/\``]);
817
820
  if (hasDiscussions) rows.push(["Discussions", `\`${skillDir}/.skilld/discussions/\``]);
818
- if (hasChangelog) rows.push(["Changelog", `\`${skillDir}/.skilld/pkg/${hasChangelog}\``]);
821
+ if (hasChangelog) rows.push(["Changelog", `\`${skillDir}/.skilld/${hasChangelog}\``]);
819
822
  if (hasReleases) rows.push(["Releases", `\`${skillDir}/.skilld/releases/\``]);
820
823
  const table = [
821
824
  "| Resource | Path |",
@@ -890,15 +893,18 @@ function buildSectionPrompt(opts) {
890
893
  hasDiscussions,
891
894
  hasReleases,
892
895
  hasChangelog,
896
+ hasDocs: !!opts.docFiles?.some((f) => f.includes("/docs/")),
893
897
  features: opts.features,
894
898
  enabledSectionCount: opts.enabledSectionCount
895
899
  }, customPrompt);
896
900
  if (!sectionDef) return "";
897
901
  const outputFile = SECTION_OUTPUT_FILES[section];
902
+ const packageRules = getPackageRules(packageName);
898
903
  const rules = [
899
904
  ...sectionDef.rules ?? [],
900
905
  "- Link to exact source file where you found info",
901
- "- TypeScript only, Vue uses `<script setup lang=\"ts\">`",
906
+ "- TypeScript only",
907
+ ...packageRules.map((r) => `- ${r}`),
902
908
  "- Imperative voice (\"Use X\" not \"You should use X\")",
903
909
  `- **NEVER fetch external URLs.** All information is in the local \`./.skilld/\` directory. Use Read, Glob${opts.features?.search !== false ? ", and `skilld search`" : ""} only.`,
904
910
  "- **Do NOT use Task tool or spawn subagents.** Work directly.",
@@ -1342,7 +1348,7 @@ function buildArgs(model, skillDir, symlinkDirs) {
1342
1348
  "-m",
1343
1349
  model,
1344
1350
  "--allowed-tools",
1345
- "read_file,write_file,glob_tool",
1351
+ "read_file,write_file,glob_tool,list_directory,search_file_content",
1346
1352
  "--include-directories",
1347
1353
  skillDir,
1348
1354
  ...symlinkDirs.flatMap((d) => ["--include-directories", d])
@@ -1382,61 +1388,44 @@ const TOOL_VERBS = {
1382
1388
  Bash: "Running",
1383
1389
  read_file: "Reading",
1384
1390
  glob_tool: "Searching",
1385
- write_file: "Writing"
1391
+ write_file: "Writing",
1392
+ list_directory: "Listing",
1393
+ search_file_content: "Searching"
1386
1394
  };
1387
1395
  function createToolProgress(log) {
1388
- const pending = /* @__PURE__ */ new Map();
1389
- let timer = null;
1390
- let lastEmitted = "";
1391
- function flush() {
1392
- const parts = [];
1393
- for (const [section, { verb, path, count }] of pending) {
1394
- const suffix = count > 1 ? ` \x1B[90m(+${count - 1})\x1B[0m` : "";
1395
- parts.push(`\x1B[90m[${section}]\x1B[0m ${verb} ${path}${suffix}`);
1396
- }
1397
- const msg = parts.join(" ");
1398
- if (msg && msg !== lastEmitted) {
1396
+ let lastMsg = "";
1397
+ let repeatCount = 0;
1398
+ function emit(msg) {
1399
+ if (msg === lastMsg) {
1400
+ repeatCount++;
1401
+ log.message(`${msg} \x1B[90m(+${repeatCount})\x1B[0m`);
1402
+ } else {
1403
+ lastMsg = msg;
1404
+ repeatCount = 0;
1399
1405
  log.message(msg);
1400
- lastEmitted = msg;
1401
1406
  }
1402
- pending.clear();
1403
- timer = null;
1404
1407
  }
1405
1408
  return ({ type, chunk, section }) => {
1406
1409
  if (type === "text") {
1407
- log.message(`${section ? `\x1B[90m[${section}]\x1B[0m ` : ""}Writing...`);
1410
+ emit(`${section ? `\x1B[90m[${section}]\x1B[0m ` : ""}Writing...`);
1408
1411
  return;
1409
1412
  }
1410
1413
  if (type !== "reasoning" || !chunk.startsWith("[")) return;
1411
- const key = section ?? "";
1412
- const match = chunk.match(/^\[(\w+)(?:,\s\w+)*(?::\s(.+))?\]$/);
1414
+ const match = chunk.match(/^\[([^:[\]]+)(?::\s(.+))?\]$/);
1413
1415
  if (!match) return;
1414
- const rawName = match[1];
1415
- const hint = match[2] ?? "";
1416
- let verb = TOOL_VERBS[rawName] ?? rawName;
1417
- let path = hint || "...";
1418
- if (rawName === "Bash" && hint) {
1419
- const searchMatch = hint.match(/skilld search\s+"([^"]+)"/);
1420
- if (searchMatch) {
1421
- verb = "skilld search:";
1422
- path = searchMatch[1];
1423
- } else path = hint.length > 60 ? `${hint.slice(0, 57)}...` : hint;
1424
- } else path = shortenPath(path);
1425
- if (rawName === "Write") {
1426
- if (timer) flush();
1416
+ const names = match[1].split(",").map((n) => n.trim());
1417
+ const hints = match[2]?.split(",").map((h) => h.trim()) ?? [];
1418
+ for (let i = 0; i < names.length; i++) {
1419
+ const rawName = names[i];
1420
+ const hint = hints[i] ?? hints[0] ?? "";
1421
+ const verb = TOOL_VERBS[rawName] ?? rawName;
1427
1422
  const prefix = section ? `\x1B[90m[${section}]\x1B[0m ` : "";
1428
- log.message(`${prefix}Writing ${path}`);
1429
- return;
1423
+ if (rawName === "Bash" && hint) {
1424
+ const searchMatch = hint.match(/skilld search\s+"([^"]+)"/);
1425
+ if (searchMatch) emit(`${prefix}Searching \x1B[36m"${searchMatch[1]}"\x1B[0m`);
1426
+ else emit(`${prefix}Running ${hint.length > 50 ? `${hint.slice(0, 47)}...` : hint}`);
1427
+ } else emit(`${prefix}${verb} \x1B[90m${shortenPath(hint || "...")}\x1B[0m`);
1430
1428
  }
1431
- const entry = mapInsert(pending, key, () => ({
1432
- verb,
1433
- path,
1434
- count: 0
1435
- }));
1436
- entry.verb = verb;
1437
- entry.path = path;
1438
- entry.count++;
1439
- if (!timer) timer = setTimeout(flush, 400);
1440
1429
  };
1441
1430
  }
1442
1431
  const CLI_DEFS = [
@@ -1615,13 +1604,16 @@ function optimizeSection(opts) {
1615
1604
  } catch {}
1616
1605
  }
1617
1606
  const raw = (existsSync(outputPath) ? readFileSync(outputPath, "utf-8") : lastWriteContent || accumulatedText).trim();
1607
+ const logsDir = join(skilldDir, "logs");
1608
+ const logName = section.toUpperCase().replace(/-/g, "_");
1609
+ if (debug || stderr && (!raw || code !== 0)) {
1610
+ mkdirSync(logsDir, { recursive: true });
1611
+ if (stderr) writeFileSync(join(logsDir, `${logName}.stderr.log`), stderr);
1612
+ }
1618
1613
  if (debug) {
1619
- const logsDir = join(skilldDir, "logs");
1620
1614
  mkdirSync(logsDir, { recursive: true });
1621
- const logName = section.toUpperCase().replace(/-/g, "_");
1622
1615
  if (rawLines.length) writeFileSync(join(logsDir, `${logName}.jsonl`), rawLines.join("\n"));
1623
1616
  if (raw) writeFileSync(join(logsDir, `${logName}.md`), raw);
1624
- if (stderr) writeFileSync(join(logsDir, `${logName}.stderr.log`), stderr);
1625
1617
  }
1626
1618
  if (!raw && code !== 0) {
1627
1619
  resolve({
@@ -1732,10 +1724,22 @@ async function optimizeDocs(opts) {
1732
1724
  }
1733
1725
  const skilldDir = join(skillDir, ".skilld");
1734
1726
  mkdirSync(skilldDir, { recursive: true });
1727
+ for (const entry of readdirSync(skilldDir)) {
1728
+ const entryPath = join(skilldDir, entry);
1729
+ try {
1730
+ if (lstatSync(entryPath).isSymbolicLink() && !existsSync(entryPath)) onProgress?.({
1731
+ chunk: `[warn: broken symlink .skilld/${entry}]`,
1732
+ type: "reasoning",
1733
+ text: "",
1734
+ reasoning: ""
1735
+ });
1736
+ } catch {}
1737
+ }
1735
1738
  const preExistingFiles = new Set(readdirSync(skilldDir));
1736
- const spawnResults = uncachedSections.length > 0 ? await Promise.allSettled(uncachedSections.map(({ section, prompt }) => {
1739
+ const STAGGER_MS = 3e3;
1740
+ const spawnResults = uncachedSections.length > 0 ? await Promise.allSettled(uncachedSections.map(({ section, prompt }, i) => {
1737
1741
  const outputFile = SECTION_OUTPUT_FILES[section];
1738
- return optimizeSection({
1742
+ const run = () => optimizeSection({
1739
1743
  section,
1740
1744
  prompt,
1741
1745
  outputFile,
@@ -1747,32 +1751,71 @@ async function optimizeDocs(opts) {
1747
1751
  debug,
1748
1752
  preExistingFiles
1749
1753
  });
1754
+ if (i === 0) return run();
1755
+ return setTimeout(i * STAGGER_MS).then(run);
1750
1756
  })) : [];
1751
1757
  const allResults = [...cachedResults];
1752
1758
  let totalUsage;
1753
1759
  let totalCost = 0;
1760
+ const retryQueue = [];
1754
1761
  for (let i = 0; i < spawnResults.length; i++) {
1755
1762
  const r = spawnResults[i];
1756
1763
  const { section, prompt } = uncachedSections[i];
1757
- if (r.status === "fulfilled") {
1758
- const result = r.value;
1759
- allResults.push(result);
1760
- if (result.wasOptimized && !noCache) setCache(prompt, model, section, result.content);
1761
- if (result.usage) {
1764
+ if (r.status === "fulfilled" && r.value.wasOptimized) {
1765
+ allResults.push(r.value);
1766
+ if (r.value.usage) {
1762
1767
  totalUsage = totalUsage ?? {
1763
1768
  input: 0,
1764
1769
  output: 0
1765
1770
  };
1766
- totalUsage.input += result.usage.input;
1767
- totalUsage.output += result.usage.output;
1771
+ totalUsage.input += r.value.usage.input;
1772
+ totalUsage.output += r.value.usage.output;
1768
1773
  }
1769
- if (result.cost != null) totalCost += result.cost;
1770
- } else allResults.push({
1774
+ if (r.value.cost != null) totalCost += r.value.cost;
1775
+ if (!noCache) setCache(prompt, model, section, r.value.content);
1776
+ } else retryQueue.push({
1777
+ index: i,
1778
+ section,
1779
+ prompt
1780
+ });
1781
+ }
1782
+ for (const { section, prompt } of retryQueue) {
1783
+ onProgress?.({
1784
+ chunk: `[${section}: retrying...]`,
1785
+ type: "reasoning",
1786
+ text: "",
1787
+ reasoning: "",
1788
+ section
1789
+ });
1790
+ await setTimeout(STAGGER_MS);
1791
+ const result = await optimizeSection({
1792
+ section,
1793
+ prompt,
1794
+ outputFile: SECTION_OUTPUT_FILES[section],
1795
+ skillDir,
1796
+ model,
1797
+ packageName,
1798
+ onProgress,
1799
+ timeout,
1800
+ debug,
1801
+ preExistingFiles
1802
+ }).catch((err) => ({
1771
1803
  section,
1772
1804
  content: "",
1773
1805
  wasOptimized: false,
1774
- error: String(r.reason)
1775
- });
1806
+ error: err.message
1807
+ }));
1808
+ allResults.push(result);
1809
+ if (result.wasOptimized && !noCache) setCache(prompt, model, section, result.content);
1810
+ if (result.usage) {
1811
+ totalUsage = totalUsage ?? {
1812
+ input: 0,
1813
+ output: 0
1814
+ };
1815
+ totalUsage.input += result.usage.input;
1816
+ totalUsage.output += result.usage.output;
1817
+ }
1818
+ if (result.cost != null) totalCost += result.cost;
1776
1819
  }
1777
1820
  if (version) {
1778
1821
  const sectionFiles = allResults.filter((r) => r.wasOptimized && r.content).map((r) => ({