bip-skills 1.4.5 → 1.4.7

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 (2) hide show
  1. package/dist/cli.mjs +209 -38
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -11,7 +11,7 @@ import "./_chunks/libs/esprima.mjs";
11
11
  import { t as xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
12
12
  import { execSync, spawn, spawnSync } from "child_process";
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
- import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
14
+ import { basename, dirname, isAbsolute, join, normalize, posix, relative, resolve, sep } from "path";
15
15
  import { homedir, platform, tmpdir } from "os";
16
16
  import { createHash } from "crypto";
17
17
  import { fileURLToPath } from "url";
@@ -396,12 +396,9 @@ var GitCloneError = class extends Error {
396
396
  };
397
397
  async function cloneRepo(url, ref) {
398
398
  const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
399
- const git = esm_default({
400
- timeout: { block: CLONE_TIMEOUT_MS },
401
- env: {
402
- ...process.env,
403
- GIT_TERMINAL_PROMPT: "0"
404
- }
399
+ const git = esm_default({ timeout: { block: CLONE_TIMEOUT_MS } }).env({
400
+ ...process.env,
401
+ GIT_TERMINAL_PROMPT: "0"
405
402
  });
406
403
  const cloneOptions = ref ? [
407
404
  "--depth",
@@ -1488,6 +1485,54 @@ var WellKnownProvider = class {
1488
1485
  displayName = "Well-Known Skills";
1489
1486
  WELL_KNOWN_PATH = ".well-known/skills";
1490
1487
  INDEX_FILE = "index.json";
1488
+ buildUrl(baseUrl, relativePath) {
1489
+ const normalizedRelative = relativePath.replace(/^\/+/, "");
1490
+ return new URL(normalizedRelative, `${baseUrl.replace(/\/?$/, "/")}`).toString();
1491
+ }
1492
+ getSkillRoot(entry) {
1493
+ if (entry.entry) {
1494
+ const normalizedEntry = posix.normalize(entry.entry).replace(/^\/+/, "");
1495
+ const entryDir = posix.dirname(normalizedEntry);
1496
+ return entryDir === "." ? "" : entryDir;
1497
+ }
1498
+ return entry.name;
1499
+ }
1500
+ resolveRepositoryUrl(index, resolvedBaseUrl, entry) {
1501
+ if (!entry.repositoryId) return `${resolvedBaseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`;
1502
+ return (index.repositories?.find((repo) => repo.id === entry.repositoryId))?.repository || `${resolvedBaseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`;
1503
+ }
1504
+ resolveInlineBaseUrl(repositoryUrl) {
1505
+ if (!repositoryUrl.startsWith("http://") && !repositoryUrl.startsWith("https://")) return null;
1506
+ try {
1507
+ const parsed = new URL(repositoryUrl);
1508
+ if (parsed.pathname.endsWith(`/${this.INDEX_FILE}`)) parsed.pathname = parsed.pathname.replace(new RegExp(`/${this.INDEX_FILE}$`), "");
1509
+ else parsed.pathname = parsed.pathname.replace(/\/$/, "");
1510
+ parsed.search = "";
1511
+ parsed.hash = "";
1512
+ return parsed.toString().replace(/\/$/, "");
1513
+ } catch {
1514
+ return null;
1515
+ }
1516
+ }
1517
+ resolveInstallTarget(index, resolvedBaseUrl, entry) {
1518
+ const repositoryUrl = this.resolveRepositoryUrl(index, resolvedBaseUrl, entry);
1519
+ const skillRoot = this.getSkillRoot(entry);
1520
+ const inlineBaseUrl = this.resolveInlineBaseUrl(repositoryUrl);
1521
+ if (inlineBaseUrl) {
1522
+ const skillMdRelativePath = entry.entry || posix.join(skillRoot, "SKILL.md");
1523
+ return {
1524
+ type: "inline",
1525
+ repositoryUrl,
1526
+ skillRoot,
1527
+ skillMdUrl: this.buildUrl(inlineBaseUrl, skillMdRelativePath)
1528
+ };
1529
+ }
1530
+ return {
1531
+ type: "repository",
1532
+ repositoryUrl,
1533
+ subpath: skillRoot
1534
+ };
1535
+ }
1491
1536
  match(url) {
1492
1537
  if (!url.startsWith("http://") && !url.startsWith("https://")) return { matches: false };
1493
1538
  try {
@@ -1530,14 +1575,17 @@ var WellKnownProvider = class {
1530
1575
  debugLog(`[well-known] index response body: ${responseText}`);
1531
1576
  const index = JSON.parse(responseText);
1532
1577
  if (!index.skills || !Array.isArray(index.skills)) continue;
1533
- let allValid = true;
1534
- for (const entry of index.skills) if (!this.isValidSkillEntry(entry)) {
1535
- allValid = false;
1536
- break;
1537
- }
1538
- if (allValid) return {
1539
- index,
1540
- resolvedBaseUrl: resolvedBase
1578
+ const totalSkillCount = index.skills.length;
1579
+ const validSkills = index.skills.filter((entry) => this.isValidSkillEntry(entry));
1580
+ const invalidSkillCount = totalSkillCount - validSkills.length;
1581
+ return {
1582
+ index: {
1583
+ ...index,
1584
+ skills: validSkills
1585
+ },
1586
+ resolvedBaseUrl: resolvedBase,
1587
+ totalSkillCount,
1588
+ invalidSkillCount
1541
1589
  };
1542
1590
  } catch {
1543
1591
  continue;
@@ -1561,6 +1609,12 @@ var WellKnownProvider = class {
1561
1609
  if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
1562
1610
  }
1563
1611
  if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
1612
+ if (e.entry !== void 0) {
1613
+ if (typeof e.entry !== "string") return false;
1614
+ if (e.entry.startsWith("/") || e.entry.startsWith("\\") || e.entry.includes("..")) return false;
1615
+ if (!e.entry.toLowerCase().endsWith("/skill.md") && e.entry.toLowerCase() !== "skill.md") return false;
1616
+ }
1617
+ if (e.repositoryId !== void 0 && typeof e.repositoryId !== "string") return false;
1564
1618
  return true;
1565
1619
  }
1566
1620
  async fetchSkill(url, options = {}) {
@@ -1576,28 +1630,40 @@ var WellKnownProvider = class {
1576
1630
  if (!skillName) return null;
1577
1631
  const skillEntry = index.skills.find((s) => s.name === skillName);
1578
1632
  if (!skillEntry) return null;
1579
- return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry);
1633
+ return this.fetchSkillByEntry(index, resolvedBaseUrl, skillEntry);
1580
1634
  } catch {
1581
1635
  return null;
1582
1636
  }
1583
1637
  }
1584
- async fetchSkillByEntry(baseUrl, entry) {
1638
+ async fetchSkillByEntry(index, baseUrl, entry) {
1585
1639
  try {
1586
- const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${entry.name}`;
1587
- const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
1588
- const response = await fetch(skillMdUrl);
1640
+ const installTarget = this.resolveInstallTarget(index, baseUrl, entry);
1641
+ const files = /* @__PURE__ */ new Map();
1642
+ if (installTarget.type === "repository") return {
1643
+ name: entry.name,
1644
+ description: entry.description,
1645
+ content: "",
1646
+ installName: entry.name,
1647
+ sourceUrl: installTarget.repositoryUrl,
1648
+ files,
1649
+ declaredFiles: [...entry.files],
1650
+ indexEntry: entry,
1651
+ installTarget
1652
+ };
1653
+ const response = await fetch(installTarget.skillMdUrl);
1589
1654
  if (!response.ok) return null;
1590
1655
  const content = await response.text();
1591
1656
  const { data } = (0, import_gray_matter.default)(content);
1592
1657
  if (!data.name || !data.description) return null;
1593
- const files = /* @__PURE__ */ new Map();
1594
1658
  files.set("SKILL.md", content);
1595
1659
  const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
1596
1660
  try {
1597
- const fileUrl = `${skillBaseUrl}/${filePath}`;
1661
+ const normalizedFilePath = filePath.replace(/^\/+/, "");
1662
+ const fileRelativePath = installTarget.skillRoot ? posix.join(installTarget.skillRoot, normalizedFilePath) : normalizedFilePath;
1663
+ const fileUrl = this.buildUrl(this.resolveInlineBaseUrl(installTarget.repositoryUrl) || baseUrl, fileRelativePath);
1598
1664
  const fileResponse = await fetch(fileUrl);
1599
1665
  if (fileResponse.ok) return {
1600
- path: filePath,
1666
+ path: normalizedFilePath,
1601
1667
  content: await fileResponse.text()
1602
1668
  };
1603
1669
  } catch {}
@@ -1610,24 +1676,51 @@ var WellKnownProvider = class {
1610
1676
  description: data.description,
1611
1677
  content,
1612
1678
  installName: entry.name,
1613
- sourceUrl: skillMdUrl,
1679
+ sourceUrl: installTarget.skillMdUrl,
1614
1680
  metadata: data.metadata,
1615
1681
  files,
1616
- indexEntry: entry
1682
+ declaredFiles: [...entry.files],
1683
+ indexEntry: entry,
1684
+ installTarget
1617
1685
  };
1618
1686
  } catch {
1619
1687
  return null;
1620
1688
  }
1621
1689
  }
1622
1690
  async fetchAllSkills(url, options = {}) {
1691
+ return (await this.fetchAllSkillsWithStatus(url, options)).skills;
1692
+ }
1693
+ async fetchAllSkillsWithStatus(url, options = {}) {
1623
1694
  try {
1624
1695
  const result = await this.fetchIndex(url, options);
1625
- if (!result) return [];
1626
- const { index, resolvedBaseUrl } = result;
1627
- const skillPromises = index.skills.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry));
1628
- return (await Promise.all(skillPromises)).filter((s) => s !== null);
1696
+ if (!result) return {
1697
+ skills: [],
1698
+ status: "index-not-found",
1699
+ totalSkillCount: 0,
1700
+ invalidSkillCount: 0
1701
+ };
1702
+ const { index, resolvedBaseUrl, totalSkillCount, invalidSkillCount } = result;
1703
+ if (index.skills.length === 0) return {
1704
+ skills: [],
1705
+ status: "no-valid-entries",
1706
+ totalSkillCount,
1707
+ invalidSkillCount
1708
+ };
1709
+ const skillPromises = index.skills.map((entry) => this.fetchSkillByEntry(index, resolvedBaseUrl, entry));
1710
+ const skills = (await Promise.all(skillPromises)).filter((s) => s !== null);
1711
+ return {
1712
+ skills,
1713
+ status: skills.length > 0 ? "ok" : "no-installable-skills",
1714
+ totalSkillCount,
1715
+ invalidSkillCount
1716
+ };
1629
1717
  } catch {
1630
- return [];
1718
+ return {
1719
+ skills: [],
1720
+ status: "index-not-found",
1721
+ totalSkillCount: 0,
1722
+ invalidSkillCount: 0
1723
+ };
1631
1724
  }
1632
1725
  }
1633
1726
  toRawUrl(url) {
@@ -1833,7 +1926,7 @@ function createEmptyLocalLock() {
1833
1926
  skills: {}
1834
1927
  };
1835
1928
  }
1836
- var version$1 = "1.4.5";
1929
+ var version$1 = "1.4.7";
1837
1930
  const isCancelled$1 = (value) => typeof value === "symbol";
1838
1931
  async function isSourcePrivate(source) {
1839
1932
  const ownerRepo = parseOwnerRepo(source);
@@ -1843,6 +1936,44 @@ async function isSourcePrivate(source) {
1843
1936
  function initTelemetry(version) {
1844
1937
  setVersion(version);
1845
1938
  }
1939
+ function getWellKnownFileCount(skill) {
1940
+ return skill.declaredFiles.length > 0 ? skill.declaredFiles.length : skill.files.size;
1941
+ }
1942
+ function getWellKnownEmptyStateMessage(status, totalSkillCount, invalidSkillCount) {
1943
+ switch (status) {
1944
+ case "no-valid-entries": return totalSkillCount > 0 ? `Found a skills index, but all ${totalSkillCount} entr${totalSkillCount === 1 ? "y is" : "ies are"} invalid. Check that each entry has a top-level SKILL.md in files[].` : "Found a skills index, but it does not contain any valid skill entries.";
1945
+ case "no-installable-skills":
1946
+ if (invalidSkillCount > 0) return `Found a skills index, but no installable skills could be loaded. ${invalidSkillCount} invalid entr${invalidSkillCount === 1 ? "y was" : "ies were"} skipped.`;
1947
+ return "Found a skills index, but no installable skills could be loaded from it.";
1948
+ default: return "No skills found at this URL. Make sure the server has a /.well-known/skills/index.json file.";
1949
+ }
1950
+ }
1951
+ async function prepareRepositoryBackedWellKnownSkill(skill) {
1952
+ if (skill.installTarget.type !== "repository") throw new Error("Skill is not repository-backed");
1953
+ const tempDir = await cloneRepo(skill.installTarget.repositoryUrl);
1954
+ try {
1955
+ const exactMatches = skill.installTarget.subpath ? await discoverSkills(tempDir, skill.installTarget.subpath, {
1956
+ includeInternal: true,
1957
+ fullDepth: true
1958
+ }) : [];
1959
+ const repoWideMatches = exactMatches.length > 0 ? exactMatches : await discoverSkills(tempDir, void 0, {
1960
+ includeInternal: true,
1961
+ fullDepth: true
1962
+ });
1963
+ const matched = repoWideMatches.find((candidate) => candidate.name.toLowerCase() === skill.installName.toLowerCase()) || exactMatches[0] || repoWideMatches[0];
1964
+ if (!matched) throw new Error(`No skill found at ${skill.installTarget.repositoryUrl}${skill.installTarget.subpath ? ` (${skill.installTarget.subpath})` : ""}`);
1965
+ return {
1966
+ preparedSkill: {
1967
+ ...matched,
1968
+ name: skill.installName
1969
+ },
1970
+ tempDir
1971
+ };
1972
+ } catch (error) {
1973
+ await cleanupTempDir(tempDir).catch(() => {});
1974
+ throw error;
1975
+ }
1976
+ }
1846
1977
  function riskLabel(risk) {
1847
1978
  switch (risk) {
1848
1979
  case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
@@ -2039,14 +2170,15 @@ async function handleWellKnownSkills(source, url, options, spinner, allowFallbac
2039
2170
  const shouldReportToSun = isInternalSkillSourceUrl(url);
2040
2171
  try {
2041
2172
  spinner.start("Discovering skills from well-known endpoint...");
2042
- const skills = await wellKnownProvider.fetchAllSkills(url, { fallbackToRoot });
2173
+ const discovery = await wellKnownProvider.fetchAllSkillsWithStatus(url, { fallbackToRoot });
2174
+ const skills = discovery.skills;
2043
2175
  if (skills.length === 0) {
2044
2176
  if (allowFallback) {
2045
2177
  spinner.stop(import_picocolors.default.yellow("Default well-known endpoint unavailable"));
2046
2178
  return false;
2047
2179
  }
2048
- spinner.stop(import_picocolors.default.red("No skills found"));
2049
- Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/skills/index.json file."));
2180
+ spinner.stop(import_picocolors.default.red("No usable skills found"));
2181
+ Se(import_picocolors.default.red(getWellKnownEmptyStateMessage(discovery.status, discovery.totalSkillCount, discovery.invalidSkillCount)));
2050
2182
  process.exit(1);
2051
2183
  }
2052
2184
  spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
@@ -2054,6 +2186,7 @@ async function handleWellKnownSkills(source, url, options, spinner, allowFallbac
2054
2186
  M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
2055
2187
  M.message(import_picocolors.default.dim(skill.description));
2056
2188
  if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
2189
+ else if (skill.declaredFiles.length > 1) M.message(import_picocolors.default.dim(` Files: ${skill.declaredFiles.join(", ")}`));
2057
2190
  }
2058
2191
  if (options.list) {
2059
2192
  console.log();
@@ -2061,7 +2194,8 @@ async function handleWellKnownSkills(source, url, options, spinner, allowFallbac
2061
2194
  for (const skill of skills) {
2062
2195
  M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
2063
2196
  M.message(` ${import_picocolors.default.dim(skill.description)}`);
2064
- if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
2197
+ const fileCount = getWellKnownFileCount(skill);
2198
+ if (fileCount > 1) M.message(` ${import_picocolors.default.dim(`Files: ${fileCount}`)}`);
2065
2199
  }
2066
2200
  console.log();
2067
2201
  Se("Run without --list to install");
@@ -2213,7 +2347,8 @@ async function handleWellKnownSkills(source, url, options, spinner, allowFallbac
2213
2347
  const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
2214
2348
  summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
2215
2349
  summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
2216
- if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
2350
+ const fileCount = getWellKnownFileCount(skill);
2351
+ if (fileCount > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${fileCount}`);
2217
2352
  const skillOverwrites = overwriteStatus.get(skill.installName);
2218
2353
  const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
2219
2354
  if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
@@ -2227,19 +2362,55 @@ async function handleWellKnownSkills(source, url, options, spinner, allowFallbac
2227
2362
  process.exit(0);
2228
2363
  }
2229
2364
  }
2365
+ const preparedRepositorySkills = /* @__PURE__ */ new Map();
2366
+ const repositoryPrepFailures = /* @__PURE__ */ new Map();
2367
+ const repositoryTempDirs = [];
2368
+ const repositoryBackedSkills = selectedSkills.filter((skill) => skill.installTarget.type === "repository");
2369
+ if (repositoryBackedSkills.length > 0) {
2370
+ spinner.start("Preparing repository-backed skills...");
2371
+ for (const skill of repositoryBackedSkills) try {
2372
+ const prepared = await prepareRepositoryBackedWellKnownSkill(skill);
2373
+ preparedRepositorySkills.set(skill.installName, prepared.preparedSkill);
2374
+ repositoryTempDirs.push(prepared.tempDir);
2375
+ } catch (error) {
2376
+ repositoryPrepFailures.set(skill.installName, error instanceof Error ? error.message : "Unknown error");
2377
+ }
2378
+ spinner.stop("Repository-backed skills prepared");
2379
+ }
2230
2380
  spinner.start("Installing skills...");
2231
2381
  const results = [];
2232
2382
  for (const skill of selectedSkills) for (const agent of targetAgents) {
2233
- const result = await installWellKnownSkillForAgent(skill, agent, {
2383
+ let installOutcome;
2384
+ const prepError = repositoryPrepFailures.get(skill.installName);
2385
+ if (prepError) installOutcome = {
2386
+ success: false,
2387
+ path: "",
2388
+ mode: installMode,
2389
+ error: prepError
2390
+ };
2391
+ else if (skill.installTarget.type === "repository") {
2392
+ const preparedSkill = preparedRepositorySkills.get(skill.installName);
2393
+ if (!preparedSkill) installOutcome = {
2394
+ success: false,
2395
+ path: "",
2396
+ mode: installMode,
2397
+ error: "Repository-backed skill was not prepared"
2398
+ };
2399
+ else installOutcome = await installSkillForAgent(preparedSkill, agent, {
2400
+ global: installGlobally,
2401
+ mode: installMode
2402
+ });
2403
+ } else installOutcome = await installWellKnownSkillForAgent(skill, agent, {
2234
2404
  global: installGlobally,
2235
2405
  mode: installMode
2236
2406
  });
2237
2407
  results.push({
2238
2408
  skill: skill.installName,
2239
2409
  agent: agents[agent].displayName,
2240
- ...result
2410
+ ...installOutcome
2241
2411
  });
2242
2412
  }
2413
+ await Promise.all(repositoryTempDirs.map((dir) => cleanupTempDir(dir).catch(() => {})));
2243
2414
  spinner.stop("Installation complete");
2244
2415
  console.log();
2245
2416
  const successful = results.filter((r) => r.success);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bip-skills",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {