@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.
- package/neta-search.mjs +79 -13
- 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
|
-
|
|
27088
|
-
|
|
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
|
-
|
|
27121
|
-
|
|
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}:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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) => {
|