bip-skills 1.4.8 → 1.4.10

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 +368 -111
  2. package/package.json +16 -18
package/dist/cli.mjs CHANGED
@@ -9,15 +9,18 @@ import { t as require_gray_matter } from "./_chunks/libs/gray-matter.mjs";
9
9
  import "./_chunks/libs/extend-shallow.mjs";
10
10
  import "./_chunks/libs/esprima.mjs";
11
11
  import { t as xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
12
- import { execSync, spawn, spawnSync } from "child_process";
12
+ import { execSync, spawnSync } from "child_process";
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
14
  import { basename, dirname, isAbsolute, join, normalize, posix, relative, resolve, sep } from "path";
15
15
  import { homedir, platform, tmpdir } from "os";
16
- import { createHash } from "crypto";
17
16
  import { fileURLToPath } from "url";
18
17
  import * as readline from "readline";
19
18
  import { Writable } from "stream";
19
+ import { Buffer } from "node:buffer";
20
20
  import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
21
+ import http from "node:http";
22
+ import https from "node:https";
23
+ import { createHash } from "crypto";
21
24
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
22
25
  const AGENTS_DIR$2 = ".agents";
23
26
  const SKILLS_SUBDIR = "skills";
@@ -31,9 +34,13 @@ const DEFAULT_GIT_HOST = "git.yyrd.com";
31
34
  const DEFAULT_GIT_BASE_URL = `https://${DEFAULT_GIT_HOST}`;
32
35
  const DEFAULT_GIT_FALLBACK_SSH_HOST = "git.yyrd.com";
33
36
  const DEFAULT_INSTALL_REPORT_URL = "https://sun.yyuap.com/api/install-report";
37
+ const TLS_RELAXED_HOSTS = ["sun.yyuap.com"];
34
38
  function isYyuapHost(hostname) {
35
39
  return hostname === "yyuap.com" || hostname.endsWith(".yyuap.com");
36
40
  }
41
+ function shouldRelaxTlsForHost(hostname) {
42
+ return TLS_RELAXED_HOSTS.includes(hostname);
43
+ }
37
44
  function isYyrdHost(hostname) {
38
45
  return hostname === "yyrd.com" || hostname.endsWith(".yyrd.com");
39
46
  }
@@ -515,13 +522,41 @@ async function hasSkillMd(dir) {
515
522
  return false;
516
523
  }
517
524
  }
525
+ function getYamlHeuristicHint(content, frontmatterKeys) {
526
+ if (frontmatterKeys.length > 0 || !content.startsWith("---\n")) return null;
527
+ const closingMarkerIndex = content.indexOf("\n---", 4);
528
+ if (closingMarkerIndex === -1) return "frontmatter start marker was found, but the closing --- marker may be missing or malformed";
529
+ const suspiciousLine = content.slice(4, closingMarkerIndex).split(/\r?\n/).find((line) => /^[A-Za-z0-9_-]+:\s.+:\s/.test(line) && !/^[A-Za-z0-9_-]+:\s*["'|>]/.test(line));
530
+ if (suspiciousLine) return `frontmatter looks present but YAML may be invalid; this line contains an unquoted ": " inside a scalar value: ${JSON.stringify(suspiciousLine)}`;
531
+ return "frontmatter looks present but YAML parsing produced no keys; check for hidden characters, invalid YAML syntax, or unquoted \": \" inside values";
532
+ }
518
533
  async function parseSkillMd(skillMdPath, options) {
519
534
  try {
520
535
  const content = await readFile(skillMdPath, "utf-8");
521
536
  const { data } = (0, import_gray_matter.default)(content);
522
- if (!data.name || !data.description) return null;
523
- if (typeof data.name !== "string" || typeof data.description !== "string") return null;
524
- if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
537
+ const frontmatterKeys = Object.keys(data);
538
+ const frontmatterSummary = frontmatterKeys.length === 0 ? "(none)" : frontmatterKeys.map((key) => {
539
+ const value = data[key];
540
+ return `${key}:${Array.isArray(value) ? "array" : typeof value}`;
541
+ }).join(", ");
542
+ const leadingPreview = JSON.stringify(content.slice(0, 80));
543
+ const firstLine = JSON.stringify(content.split(/\r?\n/, 1)[0] ?? "");
544
+ const leadingCharCodes = Array.from(content.slice(0, 8)).map((char) => `U+${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`).join(", ");
545
+ const heuristicHint = getYamlHeuristicHint(content, frontmatterKeys);
546
+ if (!data.name || !data.description) {
547
+ debugLog(`[discover] skipped ${skillMdPath}: missing required frontmatter fields "name" and/or "description" (parsed keys: ${frontmatterSummary}, firstLine: ${firstLine}, leadingChars: ${leadingCharCodes}, leadingPreview: ${leadingPreview})`);
548
+ if (heuristicHint) debugLog(`[discover] hint ${skillMdPath}: ${heuristicHint}`);
549
+ return null;
550
+ }
551
+ if (typeof data.name !== "string" || typeof data.description !== "string") {
552
+ debugLog(`[discover] skipped ${skillMdPath}: frontmatter "name" and "description" must be strings (name=${typeof data.name}, description=${typeof data.description}, parsed keys: ${frontmatterSummary})`);
553
+ return null;
554
+ }
555
+ if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) {
556
+ debugLog(`[discover] skipped ${skillMdPath}: internal skill hidden (set INSTALL_INTERNAL_SKILLS=1 or request it explicitly)`);
557
+ return null;
558
+ }
559
+ debugLog(`[discover] accepted ${skillMdPath}: name="${data.name}" (parsed keys: ${frontmatterSummary})`);
525
560
  return {
526
561
  name: data.name,
527
562
  description: data.description,
@@ -529,7 +564,8 @@ async function parseSkillMd(skillMdPath, options) {
529
564
  rawContent: content,
530
565
  metadata: data.metadata
531
566
  };
532
- } catch {
567
+ } catch (error) {
568
+ debugLog(`[discover] failed to read ${skillMdPath}: ${error instanceof Error ? error.message : String(error)}`);
533
569
  return null;
534
570
  }
535
571
  }
@@ -548,6 +584,7 @@ async function discoverSkills(basePath, subpath, options) {
548
584
  const skills = [];
549
585
  const seenNames = /* @__PURE__ */ new Set();
550
586
  const searchPath = subpath ? join(basePath, subpath) : basePath;
587
+ debugLog(`[discover] start basePath=${basePath} searchPath=${searchPath} includeInternal=${options?.includeInternal === true} fullDepth=${options?.fullDepth === true}`);
551
588
  const pluginGroupings = await getPluginGroupings(searchPath);
552
589
  const enhanceSkill = (skill) => {
553
590
  const resolvedPath = resolve(skill.path);
@@ -555,6 +592,7 @@ async function discoverSkills(basePath, subpath, options) {
555
592
  return skill;
556
593
  };
557
594
  if (await hasSkillMd(searchPath)) {
595
+ debugLog(`[discover] found root SKILL.md at ${join(searchPath, "SKILL.md")}`);
558
596
  let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
559
597
  if (skill) {
560
598
  skill = enhanceSkill(skill);
@@ -595,31 +633,38 @@ async function discoverSkills(basePath, subpath, options) {
595
633
  join(searchPath, ".zencoder/skills")
596
634
  ];
597
635
  prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
636
+ debugLog(`[discover] scanning priority directories (${prioritySearchDirs.length}): ${prioritySearchDirs.join(", ")}`);
598
637
  for (const dir of prioritySearchDirs) try {
599
638
  const entries = await readdir(dir, { withFileTypes: true });
600
639
  for (const entry of entries) if (entry.isDirectory()) {
601
640
  const skillDir = join(dir, entry.name);
602
641
  if (await hasSkillMd(skillDir)) {
642
+ debugLog(`[discover] candidate skill directory ${skillDir}`);
603
643
  let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
604
644
  if (skill && !seenNames.has(skill.name)) {
605
645
  skill = enhanceSkill(skill);
606
646
  skills.push(skill);
607
647
  seenNames.add(skill.name);
608
- }
648
+ } else if (skill) debugLog(`[discover] skipped ${join(skillDir, "SKILL.md")}: duplicate skill name "${skill.name}"`);
609
649
  }
610
650
  }
611
- } catch {}
651
+ } catch (error) {
652
+ debugLog(`[discover] unable to scan ${dir}: ${error instanceof Error ? error.message : String(error)}`);
653
+ }
612
654
  if (skills.length === 0 || options?.fullDepth) {
655
+ debugLog(`[discover] ${skills.length === 0 ? "no skills from priority scan" : "fullDepth enabled"}; starting recursive search from ${searchPath}`);
613
656
  const allSkillDirs = await findSkillDirs(searchPath);
657
+ debugLog(`[discover] recursive candidates (${allSkillDirs.length}): ${allSkillDirs.join(", ") || "(none)"}`);
614
658
  for (const skillDir of allSkillDirs) {
615
659
  let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
616
660
  if (skill && !seenNames.has(skill.name)) {
617
661
  skill = enhanceSkill(skill);
618
662
  skills.push(skill);
619
663
  seenNames.add(skill.name);
620
- }
664
+ } else if (skill) debugLog(`[discover] skipped ${join(skillDir, "SKILL.md")}: duplicate skill name "${skill.name}"`);
621
665
  }
622
666
  }
667
+ debugLog(`[discover] completed searchPath=${searchPath}; found ${skills.length} skill${skills.length === 1 ? "" : "s"}`);
623
668
  return skills;
624
669
  }
625
670
  function getSkillDisplayName(skill) {
@@ -1411,6 +1456,106 @@ async function listInstalledSkills(options = {}) {
1411
1456
  } catch {}
1412
1457
  return Array.from(skillsMap.values());
1413
1458
  }
1459
+ function getRequestUrl(input) {
1460
+ if (input instanceof URL) return input;
1461
+ if (typeof input === "string") return new URL(input);
1462
+ return new URL(input.url);
1463
+ }
1464
+ function shouldUseRelaxedTls(input) {
1465
+ try {
1466
+ const url = getRequestUrl(input);
1467
+ return url.protocol === "https:" && shouldRelaxTlsForHost(url.hostname);
1468
+ } catch {
1469
+ return false;
1470
+ }
1471
+ }
1472
+ function normalizeRequestMethod(input, init) {
1473
+ if (init?.method) return init.method.toUpperCase();
1474
+ if (typeof Request !== "undefined" && input instanceof Request) return input.method.toUpperCase();
1475
+ return "GET";
1476
+ }
1477
+ function buildHeaders(input, init) {
1478
+ const headers = new Headers(typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
1479
+ if (init?.headers) new Headers(init.headers).forEach((value, key) => {
1480
+ headers.set(key, value);
1481
+ });
1482
+ return headers;
1483
+ }
1484
+ async function readRequestBody(init) {
1485
+ const body = init?.body;
1486
+ if (body == null) return;
1487
+ if (typeof body === "string") return Buffer.from(body);
1488
+ if (body instanceof URLSearchParams) return Buffer.from(body.toString());
1489
+ if (body instanceof ArrayBuffer) return Buffer.from(body);
1490
+ if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
1491
+ if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());
1492
+ throw new TypeError("Unsupported request body for relaxed TLS fetch");
1493
+ }
1494
+ async function relaxedTlsFetch(input, init, redirectCount = 0) {
1495
+ if (redirectCount > 5) throw new Error("Too many redirects");
1496
+ const url = getRequestUrl(input);
1497
+ const method = normalizeRequestMethod(input, init);
1498
+ const headers = buildHeaders(input, init);
1499
+ const body = await readRequestBody(init);
1500
+ const transport = url.protocol === "https:" ? https : http;
1501
+ return await new Promise((resolve, reject) => {
1502
+ const request = transport.request(url, {
1503
+ method,
1504
+ headers: Object.fromEntries(headers.entries()),
1505
+ rejectUnauthorized: url.protocol === "https:" ? false : void 0
1506
+ }, (response) => {
1507
+ const status = response.statusCode ?? 500;
1508
+ const location = response.headers.location;
1509
+ if (location && [
1510
+ 301,
1511
+ 302,
1512
+ 303,
1513
+ 307,
1514
+ 308
1515
+ ].includes(status) && init?.redirect !== "manual") {
1516
+ response.resume();
1517
+ const nextUrl = new URL(location, url);
1518
+ const nextInit = {
1519
+ ...init,
1520
+ method: status === 303 ? "GET" : method
1521
+ };
1522
+ delete nextInit.body;
1523
+ relaxedTlsFetch(nextUrl, nextInit, redirectCount + 1).then(resolve).catch(reject);
1524
+ return;
1525
+ }
1526
+ const chunks = [];
1527
+ response.on("data", (chunk) => {
1528
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1529
+ });
1530
+ response.on("end", () => {
1531
+ const responseHeaders = Object.entries(response.headers).flatMap(([key, value]) => value == null ? [] : Array.isArray(value) ? value.map((item) => [key, item]) : [[key, value]]);
1532
+ resolve(new Response(method === "HEAD" ? null : Buffer.concat(chunks), {
1533
+ status,
1534
+ statusText: response.statusMessage ?? "",
1535
+ headers: new Headers(responseHeaders)
1536
+ }));
1537
+ });
1538
+ });
1539
+ request.on("error", reject);
1540
+ if (init?.signal) {
1541
+ const abort = () => {
1542
+ request.destroy(new DOMException("The operation was aborted.", "AbortError"));
1543
+ };
1544
+ if (init.signal.aborted) {
1545
+ abort();
1546
+ return;
1547
+ }
1548
+ init.signal.addEventListener("abort", abort, { once: true });
1549
+ request.on("close", () => init.signal?.removeEventListener("abort", abort));
1550
+ }
1551
+ if (body) request.write(body);
1552
+ request.end();
1553
+ });
1554
+ }
1555
+ async function fetchWithSelectiveTls(input, init) {
1556
+ if (!shouldUseRelaxedTls(input)) return fetch(input, init);
1557
+ return relaxedTlsFetch(input, init);
1558
+ }
1414
1559
  function getAllowedReportingUrl(envVarName) {
1415
1560
  const value = process.env[envVarName];
1416
1561
  if (!value) return null;
@@ -1439,7 +1584,7 @@ async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
1439
1584
  });
1440
1585
  const controller = new AbortController();
1441
1586
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
1442
- const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
1587
+ const response = await fetchWithSelectiveTls(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
1443
1588
  clearTimeout(timeout);
1444
1589
  if (!response.ok) return null;
1445
1590
  return await response.json();
@@ -1454,7 +1599,7 @@ function sendTelemetry(url, data) {
1454
1599
  if (cliVersion) params.set("v", cliVersion);
1455
1600
  if (isCI()) params.set("ci", "1");
1456
1601
  for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
1457
- fetch(`${url}?${params.toString()}`).catch(() => {});
1602
+ fetchWithSelectiveTls(`${url}?${params.toString()}`).catch(() => {});
1458
1603
  } catch {}
1459
1604
  }
1460
1605
  function track(data) {
@@ -1566,7 +1711,7 @@ var WellKnownProvider = class {
1566
1711
  });
1567
1712
  for (const { indexUrl, baseUrl: resolvedBase } of urlsToTry) try {
1568
1713
  debugLog(`[well-known] requesting index: ${indexUrl}`);
1569
- const response = await fetch(indexUrl);
1714
+ const response = await fetchWithSelectiveTls(indexUrl);
1570
1715
  if (!response.ok) {
1571
1716
  debugLog(`[well-known] index response status: ${response.status} ${response.statusText}`);
1572
1717
  continue;
@@ -1650,7 +1795,7 @@ var WellKnownProvider = class {
1650
1795
  indexEntry: entry,
1651
1796
  installTarget
1652
1797
  };
1653
- const response = await fetch(installTarget.skillMdUrl);
1798
+ const response = await fetchWithSelectiveTls(installTarget.skillMdUrl);
1654
1799
  if (!response.ok) return null;
1655
1800
  const content = await response.text();
1656
1801
  const { data } = (0, import_gray_matter.default)(content);
@@ -1660,8 +1805,7 @@ var WellKnownProvider = class {
1660
1805
  try {
1661
1806
  const normalizedFilePath = filePath.replace(/^\/+/, "");
1662
1807
  const fileRelativePath = installTarget.skillRoot ? posix.join(installTarget.skillRoot, normalizedFilePath) : normalizedFilePath;
1663
- const fileUrl = this.buildUrl(this.resolveInlineBaseUrl(installTarget.repositoryUrl) || baseUrl, fileRelativePath);
1664
- const fileResponse = await fetch(fileUrl);
1808
+ const fileResponse = await fetchWithSelectiveTls(this.buildUrl(this.resolveInlineBaseUrl(installTarget.repositoryUrl) || baseUrl, fileRelativePath));
1665
1809
  if (fileResponse.ok) return {
1666
1810
  path: normalizedFilePath,
1667
1811
  content: await fileResponse.text()
@@ -1926,7 +2070,7 @@ function createEmptyLocalLock() {
1926
2070
  skills: {}
1927
2071
  };
1928
2072
  }
1929
- var version$1 = "1.4.8";
2073
+ var version$1 = "1.4.10";
1930
2074
  const isCancelled$1 = (value) => typeof value === "symbol";
1931
2075
  async function isSourcePrivate(source) {
1932
2076
  const ownerRepo = parseOwnerRepo(source);
@@ -1939,6 +2083,14 @@ function initTelemetry(version) {
1939
2083
  function getWellKnownFileCount(skill) {
1940
2084
  return skill.declaredFiles.length > 0 ? skill.declaredFiles.length : skill.files.size;
1941
2085
  }
2086
+ function getWellKnownSkillHash(skill) {
2087
+ return skill.indexEntry.hash || skill.indexEntry.contentHash || skill.indexEntry.skillFolderHash || "";
2088
+ }
2089
+ function getWellKnownSkillPath(skill) {
2090
+ if (skill.indexEntry.entry) return skill.indexEntry.entry;
2091
+ if (skill.installTarget.type === "inline") return skill.installTarget.skillRoot ? posix.join(skill.installTarget.skillRoot, "SKILL.md") : "SKILL.md";
2092
+ return skill.installTarget.subpath ? posix.join(skill.installTarget.subpath, "SKILL.md") : "SKILL.md";
2093
+ }
1942
2094
  function getWellKnownEmptyStateMessage(status, totalSkillCount, invalidSkillCount) {
1943
2095
  switch (status) {
1944
2096
  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.";
@@ -2446,7 +2598,8 @@ async function handleWellKnownSkills(source, url, options, spinner, allowFallbac
2446
2598
  source: sourceIdentifier,
2447
2599
  sourceType: "well-known",
2448
2600
  sourceUrl: skill.sourceUrl,
2449
- skillFolderHash: ""
2601
+ skillPath: getWellKnownSkillPath(skill),
2602
+ skillFolderHash: getWellKnownSkillHash(skill)
2450
2603
  });
2451
2604
  } catch {}
2452
2605
  }
@@ -2590,6 +2743,7 @@ async function runAdd(args, options = {}) {
2590
2743
  fullDepth: options.fullDepth
2591
2744
  });
2592
2745
  if (skills.length === 0) {
2746
+ debugLog(`[add] discoverSkills returned 0 skills for source=${parsed.type === "local" ? parsed.localPath : parsed.url} searchSubpath=${parsed.subpath ?? "(root)"} includeInternal=${includeInternal} fullDepth=${options.fullDepth === true}`);
2593
2747
  spinner.stop(import_picocolors.default.red("No skills found"));
2594
2748
  Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
2595
2749
  await cleanup(tempDir);
@@ -3145,7 +3299,7 @@ async function searchSkillsAPI(query) {
3145
3299
  try {
3146
3300
  const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`;
3147
3301
  debugLog(`[find] requesting search: ${url}`);
3148
- const res = await fetch(url);
3302
+ const res = await fetchWithSelectiveTls(url);
3149
3303
  debugLog(`[find] search response status: ${res.status} ${res.statusText}`);
3150
3304
  const responseText = await res.text();
3151
3305
  debugLog(`[find] search response body: ${responseText}`);
@@ -3961,6 +4115,146 @@ function parseRemoveOptions(args) {
3961
4115
  options
3962
4116
  };
3963
4117
  }
4118
+ function getUpdateCheckApiBase() {
4119
+ return process.env.SKILLS_API_URL || DEFAULT_SEARCH_API_BASE;
4120
+ }
4121
+ function canCheckWithServer(skill) {
4122
+ return skill.entry.sourceType !== "github";
4123
+ }
4124
+ function canCheckWithGitHub(skill) {
4125
+ return skill.entry.sourceType === "github" && Boolean(skill.entry.skillFolderHash) && Boolean(skill.entry.skillPath);
4126
+ }
4127
+ function buildUpdateCheckRequest(skills) {
4128
+ return {
4129
+ forceRefresh: true,
4130
+ skills: skills.map(({ name, entry }) => ({
4131
+ name,
4132
+ source: entry.source,
4133
+ sourceType: entry.sourceType,
4134
+ sourceUrl: entry.sourceUrl,
4135
+ skillPath: entry.skillPath,
4136
+ skillFolderHash: entry.skillFolderHash
4137
+ }))
4138
+ };
4139
+ }
4140
+ async function checkServerUpdates(skills, apiBaseUrl) {
4141
+ if (skills.length === 0) return {
4142
+ checkedCount: 0,
4143
+ skippedCount: 0,
4144
+ updates: [],
4145
+ errors: []
4146
+ };
4147
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
4148
+ const url = `${apiBaseUrl.replace(/\/$/, "")}/api/check-updates`;
4149
+ const request = buildUpdateCheckRequest(skills);
4150
+ debugLog(`[update] requesting update check: ${url}`);
4151
+ debugLog(`[update] update check request: ${JSON.stringify(request)}`);
4152
+ const response = await fetchWithSelectiveTls(url, {
4153
+ method: "POST",
4154
+ headers: {
4155
+ "Content-Type": "application/json",
4156
+ Accept: "application/json"
4157
+ },
4158
+ body: JSON.stringify(request)
4159
+ });
4160
+ debugLog(`[update] update check response status: ${response.status} ${response.statusText}`);
4161
+ if (!response.ok) throw new Error(`Update check failed with status ${response.status}`);
4162
+ const responseText = await response.text();
4163
+ debugLog(`[update] update check response body: ${responseText}`);
4164
+ const data = JSON.parse(responseText);
4165
+ return {
4166
+ checkedCount: data.checked ?? skills.length,
4167
+ skippedCount: data.skipped ?? 0,
4168
+ updates: (data.updates || []).flatMap((update) => {
4169
+ const skill = byName.get(update.name);
4170
+ if (!skill) return [];
4171
+ return [{
4172
+ name: update.name,
4173
+ source: update.source,
4174
+ entry: skill.entry,
4175
+ currentHash: update.currentHash,
4176
+ latestHash: update.latestHash
4177
+ }];
4178
+ }),
4179
+ errors: data.errors || []
4180
+ };
4181
+ }
4182
+ async function checkGitHubUpdates(skills, token) {
4183
+ const updates = [];
4184
+ const errors = [];
4185
+ for (const { name, entry } of skills) try {
4186
+ const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
4187
+ if (!latestHash) {
4188
+ errors.push({
4189
+ name,
4190
+ source: entry.source,
4191
+ error: "Could not fetch from GitHub"
4192
+ });
4193
+ continue;
4194
+ }
4195
+ if (latestHash !== entry.skillFolderHash) updates.push({
4196
+ name,
4197
+ source: entry.source,
4198
+ entry,
4199
+ currentHash: entry.skillFolderHash,
4200
+ latestHash
4201
+ });
4202
+ } catch (err) {
4203
+ errors.push({
4204
+ name,
4205
+ source: entry.source,
4206
+ error: err instanceof Error ? err.message : "Unknown error"
4207
+ });
4208
+ }
4209
+ return {
4210
+ checkedCount: skills.length,
4211
+ skippedCount: 0,
4212
+ updates,
4213
+ errors
4214
+ };
4215
+ }
4216
+ async function checkSkillUpdates(skills, options = {}) {
4217
+ const serverSkills = skills.filter(canCheckWithServer);
4218
+ const githubSkills = skills.filter(canCheckWithGitHub);
4219
+ let checkedCount = 0;
4220
+ let skippedCount = skills.length - serverSkills.length - githubSkills.length;
4221
+ const updates = [];
4222
+ const errors = [];
4223
+ debugLog(`[update] prepared ${serverSkills.length} server skill(s), ${githubSkills.length} github skill(s), ${skippedCount} skipped`);
4224
+ for (const skill of skills) {
4225
+ if (serverSkills.includes(skill)) {
4226
+ debugLog(`[update] server check ${skill.name}: sourceType=${skill.entry.sourceType} hash=${skill.entry.skillFolderHash || "(empty)"}`);
4227
+ continue;
4228
+ }
4229
+ if (githubSkills.includes(skill)) {
4230
+ debugLog(`[update] github check ${skill.name}: path=${skill.entry.skillPath || "(missing)"} hash=${skill.entry.skillFolderHash || "(empty)"}`);
4231
+ continue;
4232
+ }
4233
+ debugLog(`[update] skipped ${skill.name}: sourceType=${skill.entry.sourceType} path=${skill.entry.skillPath || "(missing)"} hash=${skill.entry.skillFolderHash || "(empty)"}`);
4234
+ }
4235
+ if (serverSkills.length > 0) try {
4236
+ const serverResult = await checkServerUpdates(serverSkills, options.apiBaseUrl || getUpdateCheckApiBase());
4237
+ checkedCount += serverResult.checkedCount;
4238
+ skippedCount += serverResult.skippedCount;
4239
+ updates.push(...serverResult.updates);
4240
+ errors.push(...serverResult.errors);
4241
+ } catch (err) {
4242
+ debugLog(`[update] server update check failed: ${err instanceof Error ? err.message : "Unknown error"}`);
4243
+ }
4244
+ if (githubSkills.length > 0) {
4245
+ const githubResult = await checkGitHubUpdates(githubSkills, options.githubToken);
4246
+ checkedCount += githubResult.checkedCount;
4247
+ skippedCount += githubResult.skippedCount;
4248
+ updates.push(...githubResult.updates);
4249
+ errors.push(...githubResult.errors);
4250
+ }
4251
+ return {
4252
+ checkedCount,
4253
+ skippedCount,
4254
+ updates,
4255
+ errors
4256
+ };
4257
+ }
3964
4258
  const __dirname = dirname(fileURLToPath(import.meta.url));
3965
4259
  function getVersion() {
3966
4260
  try {
@@ -3977,12 +4271,12 @@ const BOLD = "\x1B[1m";
3977
4271
  const DIM = "\x1B[38;5;102m";
3978
4272
  const TEXT = "\x1B[38;5;145m";
3979
4273
  const LOGO_LINES = [
3980
- "███████╗██╗ ██╗██╗██╗ ██╗ ███████╗",
3981
- "██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝",
3982
- "███████╗█████╔╝ ██║██║ ██║ ███████╗",
3983
- "╚════██║██╔═██╗ ██║██║ ██║ ╚════██║",
3984
- "███████║██║ ██╗██║███████╗███████╗███████║",
3985
- "╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝"
4274
+ "██████╗ ██╗ ██████╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗",
4275
+ "██╔══██╗ ██║ ██╔══██╗ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝",
4276
+ "██████╔╝ ██║ ██████╔╝█████╗███████╗█████╔╝ ██║██║ ██║ ███████╗",
4277
+ "██╔══██╗ ██║ ██╔═══╝ ╚════╝╚════██║██╔═██╗ ██║██║ ██║ ╚════██║",
4278
+ "██████╔╝ ██║ ██║ ███████║██║ ██╗██║███████╗███████╗███████║",
4279
+ "╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝"
3986
4280
  ];
3987
4281
  const GRAYS = [
3988
4282
  "\x1B[38;5;250m",
@@ -4001,6 +4295,8 @@ function showLogo() {
4001
4295
  function showBanner() {
4002
4296
  showLogo();
4003
4297
  console.log();
4298
+ console.log(`${BOLD}${TEXT}BIP-SKILLS${RESET}`);
4299
+ console.log();
4004
4300
  console.log(`${DIM}The open agent skills ecosystem${RESET}`);
4005
4301
  console.log();
4006
4302
  console.log(` ${DIM}$${RESET} ${TEXT}${CLI_NPX_COMMAND} add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
@@ -4198,82 +4494,69 @@ function readSkillLock() {
4198
4494
  };
4199
4495
  }
4200
4496
  }
4497
+ function getLockedSkillsForUpdate(lock) {
4498
+ return Object.entries(lock.skills).map(([name, entry]) => ({
4499
+ name,
4500
+ entry
4501
+ }));
4502
+ }
4503
+ function buildWellKnownUpdateUrl(name, entry) {
4504
+ try {
4505
+ if (entry.sourceUrl.startsWith("http://") || entry.sourceUrl.startsWith("https://")) {
4506
+ const parsed = new URL(entry.sourceUrl);
4507
+ const markerIndex = parsed.pathname.indexOf("/.well-known/skills");
4508
+ const basePath = markerIndex >= 0 ? parsed.pathname.slice(0, markerIndex) : "";
4509
+ return `${parsed.origin}${basePath}@${name}`;
4510
+ }
4511
+ } catch {}
4512
+ if (entry.source.startsWith("http://") || entry.source.startsWith("https://")) return `${entry.source.replace(/\/$/, "")}@${name}`;
4513
+ if (entry.source.includes(".")) return `https://${entry.source.replace(/\/$/, "")}@${name}`;
4514
+ return entry.sourceUrl;
4515
+ }
4516
+ function buildUpdateInstallUrl(name, entry) {
4517
+ if (entry.sourceType === "well-known") return buildWellKnownUpdateUrl(name, entry);
4518
+ if (entry.sourceType !== "github" || !entry.skillPath) return entry.sourceUrl;
4519
+ let skillFolder = entry.skillPath;
4520
+ if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
4521
+ else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
4522
+ if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
4523
+ return `${entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "")}/tree/main/${skillFolder}`;
4524
+ }
4201
4525
  async function runCheck(args = []) {
4202
4526
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
4203
4527
  console.log();
4204
4528
  const lock = readSkillLock();
4205
- const skillNames = Object.keys(lock.skills);
4206
- if (skillNames.length === 0) {
4529
+ if (Object.keys(lock.skills).length === 0) {
4207
4530
  console.log(`${DIM}No skills tracked in lock file.${RESET}`);
4208
4531
  console.log(`${DIM}Install skills with${RESET} ${TEXT}${CLI_NPX_COMMAND} add <package>${RESET}`);
4209
4532
  return;
4210
4533
  }
4211
- const token = getGitHubToken();
4212
- const skillsBySource = /* @__PURE__ */ new Map();
4213
- let skippedCount = 0;
4214
- for (const skillName of skillNames) {
4215
- const entry = lock.skills[skillName];
4216
- if (!entry) continue;
4217
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) {
4218
- skippedCount++;
4219
- continue;
4220
- }
4221
- const existing = skillsBySource.get(entry.source) || [];
4222
- existing.push({
4223
- name: skillName,
4224
- entry
4225
- });
4226
- skillsBySource.set(entry.source, existing);
4227
- }
4228
- const totalSkills = skillNames.length - skippedCount;
4229
- if (totalSkills === 0) {
4230
- console.log(`${DIM}No GitHub skills to check.${RESET}`);
4534
+ const result = await checkSkillUpdates(getLockedSkillsForUpdate(lock), { githubToken: getGitHubToken() });
4535
+ if (result.checkedCount === 0) {
4536
+ console.log(`${DIM}No skills to check.${RESET}`);
4231
4537
  return;
4232
4538
  }
4233
- console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
4234
- const updates = [];
4235
- const errors = [];
4236
- for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
4237
- const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
4238
- if (!latestHash) {
4239
- errors.push({
4240
- name,
4241
- source,
4242
- error: "Could not fetch from GitHub"
4243
- });
4244
- continue;
4245
- }
4246
- if (latestHash !== entry.skillFolderHash) updates.push({
4247
- name,
4248
- source
4249
- });
4250
- } catch (err) {
4251
- errors.push({
4252
- name,
4253
- source,
4254
- error: err instanceof Error ? err.message : "Unknown error"
4255
- });
4256
- }
4539
+ console.log(`${DIM}Checking ${result.checkedCount} skill(s) for updates...${RESET}`);
4257
4540
  console.log();
4258
- if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
4541
+ if (result.updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
4259
4542
  else {
4260
- console.log(`${TEXT}${updates.length} update(s) available:${RESET}`);
4543
+ console.log(`${TEXT}${result.updates.length} update(s) available:${RESET}`);
4261
4544
  console.log();
4262
- for (const update of updates) {
4545
+ for (const update of result.updates) {
4263
4546
  console.log(` ${TEXT}↑${RESET} ${update.name}`);
4264
4547
  console.log(` ${DIM}source: ${update.source}${RESET}`);
4265
4548
  }
4266
4549
  console.log();
4267
4550
  console.log(`${DIM}Run${RESET} ${TEXT}${CLI_NPX_COMMAND} update${RESET} ${DIM}to update all skills${RESET}`);
4268
4551
  }
4269
- if (errors.length > 0) {
4552
+ if (result.errors.length > 0) {
4270
4553
  console.log();
4271
- console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
4554
+ console.log(`${DIM}Could not check ${result.errors.length} skill(s) (may need reinstall)${RESET}`);
4272
4555
  }
4273
4556
  track({
4274
4557
  event: "check",
4275
- skillCount: String(totalSkills),
4276
- updatesAvailable: String(updates.length)
4558
+ skillCount: String(result.checkedCount),
4559
+ updatesAvailable: String(result.updates.length)
4277
4560
  });
4278
4561
  console.log();
4279
4562
  }
@@ -4281,58 +4564,32 @@ async function runUpdate() {
4281
4564
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
4282
4565
  console.log();
4283
4566
  const lock = readSkillLock();
4284
- const skillNames = Object.keys(lock.skills);
4285
- if (skillNames.length === 0) {
4567
+ if (Object.keys(lock.skills).length === 0) {
4286
4568
  console.log(`${DIM}No skills tracked in lock file.${RESET}`);
4287
4569
  console.log(`${DIM}Install skills with${RESET} ${TEXT}${CLI_NPX_COMMAND} add <package>${RESET}`);
4288
4570
  return;
4289
4571
  }
4290
- const token = getGitHubToken();
4291
- const updates = [];
4292
- let checkedCount = 0;
4293
- for (const skillName of skillNames) {
4294
- const entry = lock.skills[skillName];
4295
- if (!entry) continue;
4296
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) continue;
4297
- checkedCount++;
4298
- try {
4299
- const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
4300
- if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
4301
- name: skillName,
4302
- source: entry.source,
4303
- entry
4304
- });
4305
- } catch {}
4306
- }
4307
- if (checkedCount === 0) {
4572
+ const result = await checkSkillUpdates(getLockedSkillsForUpdate(lock), { githubToken: getGitHubToken() });
4573
+ if (result.checkedCount === 0) {
4308
4574
  console.log(`${DIM}No skills to check.${RESET}`);
4309
4575
  return;
4310
4576
  }
4311
- if (updates.length === 0) {
4577
+ if (result.updates.length === 0) {
4312
4578
  console.log(`${TEXT}✓ All skills are up to date${RESET}`);
4313
4579
  console.log();
4314
4580
  return;
4315
4581
  }
4316
- console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
4582
+ console.log(`${TEXT}Found ${result.updates.length} update(s)${RESET}`);
4317
4583
  console.log();
4318
4584
  let successCount = 0;
4319
4585
  let failCount = 0;
4320
- for (const update of updates) {
4586
+ for (const update of result.updates) {
4321
4587
  console.log(`${TEXT}Updating ${update.name}...${RESET}`);
4322
- let installUrl = update.entry.sourceUrl;
4323
- if (update.entry.skillPath) {
4324
- let skillFolder = update.entry.skillPath;
4325
- if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
4326
- else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
4327
- if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
4328
- installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
4329
- installUrl = `${installUrl}/tree/main/${skillFolder}`;
4330
- }
4331
4588
  if (spawnSync("npx", [
4332
4589
  "-y",
4333
4590
  CLI_PACKAGE_NAME,
4334
4591
  "add",
4335
- installUrl,
4592
+ buildUpdateInstallUrl(update.name, update.entry),
4336
4593
  "-g",
4337
4594
  "-y"
4338
4595
  ], { stdio: [
@@ -4352,7 +4609,7 @@ async function runUpdate() {
4352
4609
  if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
4353
4610
  track({
4354
4611
  event: "update",
4355
- skillCount: String(updates.length),
4612
+ skillCount: String(result.updates.length),
4356
4613
  successCount: String(successCount),
4357
4614
  failCount: String(failCount)
4358
4615
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bip-skills",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,20 +14,6 @@
14
14
  "README.md",
15
15
  "ThirdPartyNoticeText.txt"
16
16
  ],
17
- "scripts": {
18
- "build": "obuild && node scripts/generate-licenses.ts",
19
- "generate-licenses": "node scripts/generate-licenses.ts",
20
- "dev": "node src/cli.ts",
21
- "exec:test": "node scripts/execute-tests.ts",
22
- "pack:check": "npm pack --dry-run --ignore-scripts --cache .npm-cache",
23
- "prepublishOnly": "npm run build",
24
- "format": "prettier --write 'src/**/*.ts' 'scripts/**/*.ts'",
25
- "format:check": "prettier --check 'src/**/*.ts' 'scripts/**/*.ts'",
26
- "prepare": "husky",
27
- "test": "vitest",
28
- "type-check": "tsc --noEmit",
29
- "publish:snapshot": "npm version prerelease --preid=snapshot --no-git-tag-version && npm publish --tag snapshot"
30
- },
31
17
  "lint-staged": {
32
18
  "src/**/*.ts": "prettier --write",
33
19
  "scripts/**/*.ts": "prettier --write",
@@ -88,7 +74,8 @@
88
74
  "author": "",
89
75
  "license": "MIT",
90
76
  "publishConfig": {
91
- "access": "public"
77
+ "access": "public",
78
+ "registry": "https://registry.npmjs.org/"
92
79
  },
93
80
  "devDependencies": {
94
81
  "@clack/prompts": "^0.11.0",
@@ -108,5 +95,16 @@
108
95
  "engines": {
109
96
  "node": ">=18"
110
97
  },
111
- "packageManager": "pnpm@10.17.1"
112
- }
98
+ "scripts": {
99
+ "build": "obuild && node scripts/generate-licenses.ts",
100
+ "generate-licenses": "node scripts/generate-licenses.ts",
101
+ "dev": "node src/cli.ts",
102
+ "exec:test": "node scripts/execute-tests.ts",
103
+ "pack:check": "npm pack --dry-run --ignore-scripts --cache .npm-cache",
104
+ "format": "prettier --write 'src/**/*.ts' 'scripts/**/*.ts'",
105
+ "format:check": "prettier --check 'src/**/*.ts' 'scripts/**/*.ts'",
106
+ "test": "vitest",
107
+ "type-check": "tsc --noEmit",
108
+ "publish:snapshot": "npm version prerelease --preid=snapshot --no-git-tag-version && npm publish --tag snapshot"
109
+ }
110
+ }