@neta-art/neta-search-cli 0.2.20 → 0.2.23

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/neta-search.mjs +79 -13
  2. package/package.json +1 -1
package/neta-search.mjs CHANGED
@@ -27084,15 +27084,18 @@ var TIMEOUT_MS = 6e4;
27084
27084
  var MAX_TRIES = 4;
27085
27085
  var ArchiveNotFound = class extends Error {
27086
27086
  };
27087
- async function fetchArchive(env, id) {
27088
- const url = `${env.apiBase}${ARCHIVE_PATH}?id=${encodeURIComponent(id)}`;
27087
+ var DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
27088
+ async function fetchArchive(env, id, pin) {
27089
+ let url = `${env.apiBase}${ARCHIVE_PATH}?id=${encodeURIComponent(id)}`;
27090
+ if (pin) url += `&digest=${encodeURIComponent(pin)}`;
27089
27091
  const res = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
27090
- if (res.status === 404) throw new ArchiveNotFound(`${id} does not exist or has no released version`);
27092
+ if (res.status === 404) throw new ArchiveNotFound(pin ? `version ${pin} of ${id} is not available` : `${id} does not exist or has no released version`);
27091
27093
  const location = res.headers.get("location");
27092
27094
  if (res.status < 300 || res.status >= 400 || !location) throw new Error(`failed to download ${id}: HTTP ${res.status}`);
27093
27095
  const digest = res.headers.get("x-skill-digest") ?? "";
27094
27096
  const name = res.headers.get("x-skill-name") ?? id.split("/").pop();
27095
27097
  if (!digest) throw new Error(`failed to download ${id}: the server did not return a version (x-skill-digest)`);
27098
+ if (pin && digest !== pin) throw new Error(`failed to download ${id}: asked for version ${pin}, the server offered ${digest}`);
27096
27099
  const body = await download(new URL(location, url).toString(), id);
27097
27100
  const files = untarGz(body);
27098
27101
  const got = digestOf(new Map(files.map((f) => [f.path, f.data])));
@@ -27116,12 +27119,19 @@ async function archiveHead(env, id) {
27116
27119
  async function download(url, id) {
27117
27120
  for (let attempt = 1; ; attempt++) {
27118
27121
  const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
27119
- if (res.status === 202 && attempt < MAX_TRIES) {
27120
- const body = await res.json().catch(() => ({}));
27121
- await new Promise((r) => setTimeout(r, Math.min(Math.max(body.retryAfterMs ?? 2e3, 250), 5e3)));
27122
+ if ((res.status === 202 || res.status === 503) && attempt < MAX_TRIES) {
27123
+ let wait = 2e3;
27124
+ if (res.status === 202) {
27125
+ const body = await res.json().catch(() => ({}));
27126
+ wait = body.retryAfterMs ?? wait;
27127
+ } else {
27128
+ const sec = Number(res.headers.get("retry-after"));
27129
+ if (Number.isFinite(sec) && sec > 0) wait = sec * 1e3;
27130
+ }
27131
+ await new Promise((r) => setTimeout(r, Math.min(Math.max(wait, 250), 5e3)));
27122
27132
  continue;
27123
27133
  }
27124
- if (!res.ok) throw new Error(`failed to download ${id}: registry returned HTTP ${res.status}`);
27134
+ if (!res.ok) throw new Error(`failed to download ${id}: HTTP ${res.status}`);
27125
27135
  return Buffer.from(await res.arrayBuffer());
27126
27136
  }
27127
27137
  }
@@ -27247,7 +27257,12 @@ function writeSkill(target, files, source) {
27247
27257
  const trash = join2(dirname(target), `.trash-${target.split("/").pop()}-${process.pid}`);
27248
27258
  const hadOld = existsSync(target);
27249
27259
  if (hadOld) renameSync(target, trash);
27250
- renameSync(staging, target);
27260
+ try {
27261
+ renameSync(staging, target);
27262
+ } catch (e) {
27263
+ rmSync(staging, { recursive: true, force: true });
27264
+ throw e;
27265
+ }
27251
27266
  if (hadOld) rmSync(trash, { recursive: true, force: true });
27252
27267
  }
27253
27268
  var POINTER_RE = /^\s*neta-skill-id:\s*["']?(cohub:[^"'\s]+)["']?\s*$/m;
@@ -27392,6 +27407,22 @@ async function cmdUse(idOrName, opts) {
27392
27407
  let notice = null;
27393
27408
  let found = null;
27394
27409
  let fetchId = parsed && !mine ? idOrName : null;
27410
+ const pin = opts.digest?.trim().toLowerCase();
27411
+ if (opts.digest !== void 0) {
27412
+ if (!pin || !DIGEST_RE.test(pin)) {
27413
+ console.error(`--digest must look like sha256:<64 hex characters>, got "${opts.digest}".`);
27414
+ return 1;
27415
+ }
27416
+ if (!parsed) {
27417
+ console.error(
27418
+ resolved.unreachable ? `Cannot reach the server (${env.apiBase}), so handle "${input}" could not be resolved. Try again later.` : `--digest needs a handle or an id (cohub:<space>/<name>); "${input}" is neither.`
27419
+ );
27420
+ return 1;
27421
+ }
27422
+ const r = await pinnedFromCacheOrDownload(env, idOrName, pin);
27423
+ if (!r) return 1;
27424
+ return emit(env, r.found, r.handle ?? handle, name, { via, entry, viaPointer: false, notice: null, json: opts.json, pinned: true });
27425
+ }
27395
27426
  const installed = join4(SKILLS_DIR, name);
27396
27427
  const draft = join4(DRAFTS_DIR, name);
27397
27428
  if (skillDirExists(installed)) {
@@ -27446,16 +27477,21 @@ async function cmdUse(idOrName, opts) {
27446
27477
  console.error(`Skill "${idOrName}" is not available locally. Run neta-search skills find "${name}" to get its id.`);
27447
27478
  return 1;
27448
27479
  }
27480
+ return emit(env, found, handle, name, { via, entry, viaPointer, notice, json: opts.json, pinned: false });
27481
+ }
27482
+ async function emit(env, found, handle, name, o) {
27483
+ const { via, entry, viaPointer, notice } = o;
27449
27484
  const useId = randomUUID();
27450
27485
  const extra = { use_id: useId, where: found.where };
27451
27486
  if (via) extra["via"] = via;
27452
27487
  if (entry) extra["entry"] = entry;
27488
+ if (o.pinned) extra["pinned"] = "true";
27453
27489
  const digest = found.where === "mod" ? "" : found.digest ?? skillDirDigest(found.path);
27454
27490
  const reported = found.reportId ? await reportSkillEvent(env, "use", found.reportId, digest, name, extra) : false;
27455
27491
  const skillMd = readFileSync4(join4(found.path, "SKILL.md"), "utf8");
27456
27492
  const dir = resolve(found.path);
27457
27493
  const shown = handle ?? found.reportId ?? name;
27458
- if (opts.json) {
27494
+ if (o.json) {
27459
27495
  console.log(
27460
27496
  JSON.stringify({
27461
27497
  skill: found.reportId ?? name,
@@ -27513,6 +27549,18 @@ function findInMounts(spaceId, name) {
27513
27549
  }
27514
27550
  return hits[0] ?? null;
27515
27551
  }
27552
+ function pinnedDir(spaceId, name, digest) {
27553
+ return join4(FETCH_ROOT, spaceId, `${name}@${digest.slice("sha256:".length, "sha256:".length + 12)}`);
27554
+ }
27555
+ async function pinnedFromCacheOrDownload(env, fetchId, pin) {
27556
+ const p = parseId(fetchId);
27557
+ const dir = pinnedDir(p.spaceId, p.name, pin);
27558
+ const have = skillDirExists(dir) ? readSource(dir) : null;
27559
+ if (have && have.id === fetchId && have.digest === pin) {
27560
+ return { found: { path: dir, where: "cache", reportId: have.id, digest: have.digest }, handle: have.handle };
27561
+ }
27562
+ return download2(env, fetchId, pin, dir);
27563
+ }
27516
27564
  async function fromCacheOrDownload(env, fetchId) {
27517
27565
  const p = parseId(fetchId);
27518
27566
  const cacheDir = join4(FETCH_ROOT, p.spaceId, p.name);
@@ -27536,16 +27584,34 @@ async function fromCacheOrDownload(env, fetchId) {
27536
27584
  if (head.handle && head.handle !== have.handle) writeSource(cacheDir, { ...have, handle: head.handle });
27537
27585
  return { found: { path: cacheDir, where: "cache", reportId: have.id, digest: have.digest }, handle: head.handle ?? have.handle };
27538
27586
  }
27587
+ return download2(env, fetchId);
27588
+ }
27589
+ function sameVersion(dir, want) {
27590
+ if (!skillDirExists(dir)) return false;
27591
+ const have = readSource(dir);
27592
+ return !!have && have.id === want.id && have.digest === want.digest;
27593
+ }
27594
+ async function download2(env, fetchId, pin, target) {
27539
27595
  try {
27540
- const a = await fetchArchive(env, fetchId);
27596
+ const a = await fetchArchive(env, fetchId, pin);
27541
27597
  const skillMd = a.files.find((f) => f.path === "SKILL.md");
27542
27598
  if (skillMd && pointerTargetOf(skillMd.data.toString("utf8")) !== null) {
27543
27599
  console.error(`${fetchId} downloaded as another pointer; stopping here (pointers are followed only one level, to avoid loops).`);
27544
27600
  return null;
27545
27601
  }
27546
27602
  const q = parseId(a.id);
27547
- const path = join4(FETCH_ROOT, q.spaceId, q.name);
27548
- writeSkill(path, a.files, { id: a.id, digest: a.digest, handle: a.handle });
27603
+ const path = target ?? join4(FETCH_ROOT, q.spaceId, q.name);
27604
+ const record = { id: a.id, digest: a.digest, handle: a.handle };
27605
+ if (target) {
27606
+ if (sameVersion(path, record)) return { found: { path, where: "fetched", reportId: a.id, digest: a.digest }, handle: a.handle };
27607
+ try {
27608
+ writeSkill(path, a.files, record);
27609
+ } catch (e) {
27610
+ if (!sameVersion(path, record)) throw e;
27611
+ }
27612
+ return { found: { path, where: "fetched", reportId: a.id, digest: a.digest }, handle: a.handle };
27613
+ }
27614
+ writeSkill(path, a.files, record);
27549
27615
  return { found: { path, where: "fetched", reportId: a.id, digest: a.digest }, handle: a.handle };
27550
27616
  } catch (e) {
27551
27617
  console.error(e instanceof ArchiveNotFound ? `${e.message}.` : String(e instanceof Error ? e.message : e));
@@ -27778,7 +27844,7 @@ skills.command("find").description("search the ecosystem for skills").argument("
27778
27844
  skills.command("add").description("install a skill: an id or handle from find downloads its current release; a name from your drafts moves it into .agents/skills/").argument("<id-or-name>", "skill id or handle from find (e.g. neta-skills/holo-pack-opening), or the name of one of your drafts").option("--dir <dir>", "install directory", ".agents/skills").option("--json", "compact JSON output (for agents)").action(async (idOrName, opts) => {
27779
27845
  await run(cmdAdd(idOrName, opts));
27780
27846
  });
27781
- skills.command("use").description("use a skill without installing it: prints the full SKILL.md and the skill directory (local copy, mounted mod, cache, or download)").argument("<id-or-name>", "skill id, handle (e.g. neta-skills/holo-pack-opening), or local skill name").option("--json", "compact JSON output (for agents)").option("--via <channel>", "where this use came from, recorded in usage stats (site / board-menu / picker / find)").option("--entry <id>", "entry id (picker / menu entry), recorded in usage stats").action(async (idOrName, opts) => {
27847
+ skills.command("use").description("use a skill without installing it: prints the full SKILL.md and the skill directory (local copy, mounted mod, cache, or download)").argument("<id-or-name>", "skill id, handle (e.g. neta-skills/holo-pack-opening), or local skill name").option("--json", "compact JSON output (for agents)").option("--via <channel>", "where this use came from, recorded in usage stats (site / board-menu / skill-picker / find)").option("--entry <id>", "entry id (picker / menu entry), recorded in usage stats").addOption(new Option("--digest <sha256>", "use exactly this version (sha256:<hex>); needs a handle or id").hideHelp()).action(async (idOrName, opts) => {
27782
27848
  await run(cmdUse(idOrName, opts));
27783
27849
  });
27784
27850
  skills.command("list").alias("ls").description("list local skills (installed / pointer / your own) and check installed ones for updates").option("--dir <dir>", "skills directory", ".agents/skills").option("--offline", "do not check for updates online").option("--json", "compact JSON output (for agents)").action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/neta-search-cli",
3
- "version": "0.2.20",
3
+ "version": "0.2.23",
4
4
  "description": "neta-search CLI — find, try and install skills from the Cohub / Neta skill registry",
5
5
  "type": "module",
6
6
  "bin": {