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

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/README.md CHANGED
@@ -10,6 +10,7 @@ npx -y @neta-art/neta-search-cli@latest skills add <id> # insta
10
10
  npx -y @neta-art/neta-search-cli@latest skills list # what's in .agents/skills/, and which installed ones have updates
11
11
  npx -y @neta-art/neta-search-cli@latest skills update # update installed skills to their current release (or name some: handle, id or directory)
12
12
  npx -y @neta-art/neta-search-cli@latest skills init <name> # scaffold your own skill
13
+ npx -y @neta-art/neta-search-cli@latest skills submit <name> # submit .agents/skills/<name>/ to the hub and wait for the result
13
14
  ```
14
15
 
15
16
  `<id>` is what `find` prints: a handle such as `neta-skills/holo-pack-opening` or `cohub/agent-browser`. Skills without a handle are listed as `cohub:<spaceId>/<name>`, which works everywhere a handle does. `update` and `remove` also accept the directory name under `.agents/skills/`.
@@ -22,6 +23,8 @@ Each `find --json` item carries a ready-to-run `use` command (`skills use <id> -
22
23
 
23
24
  Use `@latest` (or an exact version): with a range such as `@0.2`, npx keeps running whatever matching version it cached earlier and never checks for a newer one.
24
25
 
26
+ `submit` works inside a Cohub sandbox only: it checks the directory locally (frontmatter, size, symlinks), sends this version to the hub under your name, and waits for the result: collected (with its digest) or why not. Submitting is not publishing: a version is offered to others only after it passes review. To keep a skill to yourself, add `share: false` to its frontmatter.
27
+
25
28
  Inside a Cohub sandbox the CLI picks up the sandbox identity automatically; elsewhere it runs anonymously.
26
29
 
27
30
  Single file, no runtime dependencies. Node.js 20+.
package/neta-search.mjs CHANGED
@@ -4210,8 +4210,8 @@ var require_util = __commonJS({
4210
4210
  iterator = iterable[Symbol.asyncIterator]();
4211
4211
  },
4212
4212
  async pull(controller) {
4213
- const { done, value } = await iterator.next();
4214
- if (done) {
4213
+ const { done: done2, value } = await iterator.next();
4214
+ if (done2) {
4215
4215
  queueMicrotask(() => {
4216
4216
  controller.close();
4217
4217
  });
@@ -7158,8 +7158,8 @@ var require_util2 = __commonJS({
7158
7158
  const bytes = [];
7159
7159
  let byteLength = 0;
7160
7160
  while (true) {
7161
- const { done, value: chunk } = await reader.read();
7162
- if (done) {
7161
+ const { done: done2, value: chunk } = await reader.read();
7162
+ if (done2) {
7163
7163
  return Buffer.concat(bytes, byteLength);
7164
7164
  }
7165
7165
  if (!isUint8Array(chunk)) {
@@ -7402,8 +7402,8 @@ var require_webidl = __commonJS({
7402
7402
  });
7403
7403
  }
7404
7404
  while (true) {
7405
- const { done, value } = method.next();
7406
- if (done) {
7405
+ const { done: done2, value } = method.next();
7406
+ if (done2) {
7407
7407
  break;
7408
7408
  }
7409
7409
  seq.push(converter(value));
@@ -8395,8 +8395,8 @@ Content-Type: ${value.type || "application/octet-stream"}\r
8395
8395
  iterator = action(object)[Symbol.asyncIterator]();
8396
8396
  },
8397
8397
  async pull(controller) {
8398
- const { value, done } = await iterator.next();
8399
- if (done) {
8398
+ const { value, done: done2 } = await iterator.next();
8399
+ if (done2) {
8400
8400
  queueMicrotask(() => {
8401
8401
  controller.close();
8402
8402
  });
@@ -16793,11 +16793,11 @@ var require_fetch = __commonJS({
16793
16793
  let bytes;
16794
16794
  let isFailure;
16795
16795
  try {
16796
- const { done, value } = await fetchParams.controller.next();
16796
+ const { done: done2, value } = await fetchParams.controller.next();
16797
16797
  if (isAborted(fetchParams)) {
16798
16798
  break;
16799
16799
  }
16800
- bytes = done ? void 0 : value;
16800
+ bytes = done2 ? void 0 : value;
16801
16801
  } catch (err) {
16802
16802
  if (fetchParams.controller.ended && !timingInfo.encodedBodySize) {
16803
16803
  bytes = void 0;
@@ -17394,14 +17394,14 @@ var require_util4 = __commonJS({
17394
17394
  (async () => {
17395
17395
  while (!fr[kAborted]) {
17396
17396
  try {
17397
- const { done, value } = await chunkPromise;
17397
+ const { done: done2, value } = await chunkPromise;
17398
17398
  if (isFirstChunk && !fr[kAborted]) {
17399
17399
  queueMicrotask(() => {
17400
17400
  fireAProgressEvent("loadstart", fr);
17401
17401
  });
17402
17402
  }
17403
17403
  isFirstChunk = false;
17404
- if (!done && types.isUint8Array(value)) {
17404
+ if (!done2 && types.isUint8Array(value)) {
17405
17405
  bytes.push(value);
17406
17406
  if ((fr[kLastProgressEventFired] === void 0 || Date.now() - fr[kLastProgressEventFired] >= 50) && !fr[kAborted]) {
17407
17407
  fr[kLastProgressEventFired] = Date.now();
@@ -17410,7 +17410,7 @@ var require_util4 = __commonJS({
17410
17410
  });
17411
17411
  }
17412
17412
  chunkPromise = reader.read();
17413
- } else if (done) {
17413
+ } else if (done2) {
17414
17414
  queueMicrotask(() => {
17415
17415
  fr[kState] = "done";
17416
17416
  try {
@@ -25148,16 +25148,16 @@ function transformConnectPostToGetRequest(request3, message, useBase64) {
25148
25148
  // cli/node_modules/@connectrpc/connect/dist/esm/protocol/run-call.js
25149
25149
  function runUnaryCall(opt) {
25150
25150
  const next = applyInterceptors(opt.next, opt.interceptors);
25151
- const [signal, abort, done] = setupSignal(opt);
25151
+ const [signal, abort, done2] = setupSignal(opt);
25152
25152
  const req = Object.assign(Object.assign({}, opt.req), { message: normalize(opt.req.method.I, opt.req.message), signal });
25153
25153
  return next(req).then((res) => {
25154
- done();
25154
+ done2();
25155
25155
  return res;
25156
25156
  }, abort);
25157
25157
  }
25158
25158
  function runStreamingCall(opt) {
25159
25159
  const next = applyInterceptors(opt.next, opt.interceptors);
25160
- const [signal, abort, done] = setupSignal(opt);
25160
+ const [signal, abort, done2] = setupSignal(opt);
25161
25161
  const req = Object.assign(Object.assign({}, opt.req), { message: normalizeIterable(opt.req.method.I, opt.req.message), signal });
25162
25162
  let doneCalled = false;
25163
25163
  signal.addEventListener("abort", function() {
@@ -25179,7 +25179,7 @@ function runStreamingCall(opt) {
25179
25179
  return it.next().then((r) => {
25180
25180
  if (r.done == true) {
25181
25181
  doneCalled = true;
25182
- done();
25182
+ done2();
25183
25183
  }
25184
25184
  return r;
25185
25185
  }, abort);
@@ -25201,7 +25201,7 @@ function setupSignal(opt) {
25201
25201
  cleanup();
25202
25202
  return Promise.reject(e);
25203
25203
  },
25204
- function done() {
25204
+ function done2() {
25205
25205
  cleanup();
25206
25206
  controller.abort();
25207
25207
  }
@@ -26957,9 +26957,12 @@ function eventContext(env, extra = {}) {
26957
26957
  return ctx;
26958
26958
  }
26959
26959
  async function reportSkillEvent(env, event, skillId, digest, skillName, extra = {}) {
26960
- if (env.anonymous) return false;
26960
+ return await reportSkillEventDetail(env, event, skillId, digest, skillName, extra) !== "unreachable";
26961
+ }
26962
+ async function reportSkillEventDetail(env, event, skillId, digest, skillName, extra = {}) {
26963
+ if (env.anonymous) return "unreachable";
26961
26964
  try {
26962
- await feedbackClient(env).reportSkillEvents({
26965
+ const res = await feedbackClient(env).reportSkillEvents({
26963
26966
  events: [
26964
26967
  {
26965
26968
  event,
@@ -26973,10 +26976,10 @@ async function reportSkillEvent(env, event, skillId, digest, skillName, extra =
26973
26976
  }
26974
26977
  ]
26975
26978
  });
26976
- return true;
26979
+ return res.accepted > 0 ? "accepted" : "dropped";
26977
26980
  } catch (e) {
26978
26981
  console.error(`Note: failed to report the ${event} event (this does not affect the ${event}): ${e instanceof Error ? e.message : e}`);
26979
- return false;
26982
+ return "unreachable";
26980
26983
  }
26981
26984
  }
26982
26985
 
@@ -27084,15 +27087,18 @@ var TIMEOUT_MS = 6e4;
27084
27087
  var MAX_TRIES = 4;
27085
27088
  var ArchiveNotFound = class extends Error {
27086
27089
  };
27087
- async function fetchArchive(env, id) {
27088
- const url = `${env.apiBase}${ARCHIVE_PATH}?id=${encodeURIComponent(id)}`;
27090
+ var DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
27091
+ async function fetchArchive(env, id, pin) {
27092
+ let url = `${env.apiBase}${ARCHIVE_PATH}?id=${encodeURIComponent(id)}`;
27093
+ if (pin) url += `&digest=${encodeURIComponent(pin)}`;
27089
27094
  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`);
27095
+ 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
27096
  const location = res.headers.get("location");
27092
27097
  if (res.status < 300 || res.status >= 400 || !location) throw new Error(`failed to download ${id}: HTTP ${res.status}`);
27093
27098
  const digest = res.headers.get("x-skill-digest") ?? "";
27094
27099
  const name = res.headers.get("x-skill-name") ?? id.split("/").pop();
27095
27100
  if (!digest) throw new Error(`failed to download ${id}: the server did not return a version (x-skill-digest)`);
27101
+ if (pin && digest !== pin) throw new Error(`failed to download ${id}: asked for version ${pin}, the server offered ${digest}`);
27096
27102
  const body = await download(new URL(location, url).toString(), id);
27097
27103
  const files = untarGz(body);
27098
27104
  const got = digestOf(new Map(files.map((f) => [f.path, f.data])));
@@ -27116,12 +27122,19 @@ async function archiveHead(env, id) {
27116
27122
  async function download(url, id) {
27117
27123
  for (let attempt = 1; ; attempt++) {
27118
27124
  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)));
27125
+ if ((res.status === 202 || res.status === 503) && attempt < MAX_TRIES) {
27126
+ let wait = 2e3;
27127
+ if (res.status === 202) {
27128
+ const body = await res.json().catch(() => ({}));
27129
+ wait = body.retryAfterMs ?? wait;
27130
+ } else {
27131
+ const sec = Number(res.headers.get("retry-after"));
27132
+ if (Number.isFinite(sec) && sec > 0) wait = sec * 1e3;
27133
+ }
27134
+ await new Promise((r) => setTimeout(r, Math.min(Math.max(wait, 250), 5e3)));
27122
27135
  continue;
27123
27136
  }
27124
- if (!res.ok) throw new Error(`failed to download ${id}: registry returned HTTP ${res.status}`);
27137
+ if (!res.ok) throw new Error(`failed to download ${id}: HTTP ${res.status}`);
27125
27138
  return Buffer.from(await res.arrayBuffer());
27126
27139
  }
27127
27140
  }
@@ -27247,7 +27260,12 @@ function writeSkill(target, files, source) {
27247
27260
  const trash = join2(dirname(target), `.trash-${target.split("/").pop()}-${process.pid}`);
27248
27261
  const hadOld = existsSync(target);
27249
27262
  if (hadOld) renameSync(target, trash);
27250
- renameSync(staging, target);
27263
+ try {
27264
+ renameSync(staging, target);
27265
+ } catch (e) {
27266
+ rmSync(staging, { recursive: true, force: true });
27267
+ throw e;
27268
+ }
27251
27269
  if (hadOld) rmSync(trash, { recursive: true, force: true });
27252
27270
  }
27253
27271
  var POINTER_RE = /^\s*neta-skill-id:\s*["']?(cohub:[^"'\s]+)["']?\s*$/m;
@@ -27277,14 +27295,15 @@ function sharedOptOut(dir) {
27277
27295
  }
27278
27296
  }
27279
27297
  function shareNotice(id, dir, draft, reported) {
27298
+ const name = id.split("/").pop();
27280
27299
  if (draft) {
27281
- console.error(`Note: skills in the drafts directory are not shared. Move it to ${SKILLS_DIR}/ and others will be able to find and install it.`);
27300
+ console.error(`Note: drafts are not sent to the hub. When it is ready, run neta-search skills add ${name} to move it to ${SKILLS_DIR}/, then neta-search skills submit ${name}.`);
27282
27301
  } else if (sharedOptOut(dir)) {
27283
- console.error(`Note: ${id} sets share: false, so it is not shared with others.`);
27302
+ console.error(`Note: ${id} sets share: false, so it is not sent to the hub.`);
27284
27303
  } else if (!reported) {
27285
- console.error(`Note: could not reach the server this time, so ${id} is not shared yet; it will retry on the next use.`);
27304
+ console.error(`Note: could not reach the server this time, so this version of ${id} was not sent to the hub; it will retry on the next use.`);
27286
27305
  } else {
27287
- console.error(`Note: the version you just used will be shared with others under your name once the server verifies it: ${id}. To opt out, add share: false to the SKILL.md frontmatter.`);
27306
+ console.error(`Note: this version of ${id} was sent to the hub under your name. Others can use it only after it passes review; neta-search skills submit ${name} shows whether it was collected. To keep it to yourself, add share: false to the SKILL.md frontmatter.`);
27288
27307
  }
27289
27308
  }
27290
27309
 
@@ -27392,6 +27411,22 @@ async function cmdUse(idOrName, opts) {
27392
27411
  let notice = null;
27393
27412
  let found = null;
27394
27413
  let fetchId = parsed && !mine ? idOrName : null;
27414
+ const pin = opts.digest?.trim().toLowerCase();
27415
+ if (opts.digest !== void 0) {
27416
+ if (!pin || !DIGEST_RE.test(pin)) {
27417
+ console.error(`--digest must look like sha256:<64 hex characters>, got "${opts.digest}".`);
27418
+ return 1;
27419
+ }
27420
+ if (!parsed) {
27421
+ console.error(
27422
+ 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.`
27423
+ );
27424
+ return 1;
27425
+ }
27426
+ const r = await pinnedFromCacheOrDownload(env, idOrName, pin);
27427
+ if (!r) return 1;
27428
+ return emit(env, r.found, r.handle ?? handle, name, { via, entry, viaPointer: false, notice: null, json: opts.json, pinned: true });
27429
+ }
27395
27430
  const installed = join4(SKILLS_DIR, name);
27396
27431
  const draft = join4(DRAFTS_DIR, name);
27397
27432
  if (skillDirExists(installed)) {
@@ -27446,16 +27481,21 @@ async function cmdUse(idOrName, opts) {
27446
27481
  console.error(`Skill "${idOrName}" is not available locally. Run neta-search skills find "${name}" to get its id.`);
27447
27482
  return 1;
27448
27483
  }
27484
+ return emit(env, found, handle, name, { via, entry, viaPointer, notice, json: opts.json, pinned: false });
27485
+ }
27486
+ async function emit(env, found, handle, name, o) {
27487
+ const { via, entry, viaPointer, notice } = o;
27449
27488
  const useId = randomUUID();
27450
27489
  const extra = { use_id: useId, where: found.where };
27451
27490
  if (via) extra["via"] = via;
27452
27491
  if (entry) extra["entry"] = entry;
27492
+ if (o.pinned) extra["pinned"] = "true";
27453
27493
  const digest = found.where === "mod" ? "" : found.digest ?? skillDirDigest(found.path);
27454
27494
  const reported = found.reportId ? await reportSkillEvent(env, "use", found.reportId, digest, name, extra) : false;
27455
27495
  const skillMd = readFileSync4(join4(found.path, "SKILL.md"), "utf8");
27456
27496
  const dir = resolve(found.path);
27457
27497
  const shown = handle ?? found.reportId ?? name;
27458
- if (opts.json) {
27498
+ if (o.json) {
27459
27499
  console.log(
27460
27500
  JSON.stringify({
27461
27501
  skill: found.reportId ?? name,
@@ -27513,6 +27553,18 @@ function findInMounts(spaceId, name) {
27513
27553
  }
27514
27554
  return hits[0] ?? null;
27515
27555
  }
27556
+ function pinnedDir(spaceId, name, digest) {
27557
+ return join4(FETCH_ROOT, spaceId, `${name}@${digest.slice("sha256:".length, "sha256:".length + 12)}`);
27558
+ }
27559
+ async function pinnedFromCacheOrDownload(env, fetchId, pin) {
27560
+ const p = parseId(fetchId);
27561
+ const dir = pinnedDir(p.spaceId, p.name, pin);
27562
+ const have = skillDirExists(dir) ? readSource(dir) : null;
27563
+ if (have && have.id === fetchId && have.digest === pin) {
27564
+ return { found: { path: dir, where: "cache", reportId: have.id, digest: have.digest }, handle: have.handle };
27565
+ }
27566
+ return download2(env, fetchId, pin, dir);
27567
+ }
27516
27568
  async function fromCacheOrDownload(env, fetchId) {
27517
27569
  const p = parseId(fetchId);
27518
27570
  const cacheDir = join4(FETCH_ROOT, p.spaceId, p.name);
@@ -27536,16 +27588,34 @@ async function fromCacheOrDownload(env, fetchId) {
27536
27588
  if (head.handle && head.handle !== have.handle) writeSource(cacheDir, { ...have, handle: head.handle });
27537
27589
  return { found: { path: cacheDir, where: "cache", reportId: have.id, digest: have.digest }, handle: head.handle ?? have.handle };
27538
27590
  }
27591
+ return download2(env, fetchId);
27592
+ }
27593
+ function sameVersion(dir, want) {
27594
+ if (!skillDirExists(dir)) return false;
27595
+ const have = readSource(dir);
27596
+ return !!have && have.id === want.id && have.digest === want.digest;
27597
+ }
27598
+ async function download2(env, fetchId, pin, target) {
27539
27599
  try {
27540
- const a = await fetchArchive(env, fetchId);
27600
+ const a = await fetchArchive(env, fetchId, pin);
27541
27601
  const skillMd = a.files.find((f) => f.path === "SKILL.md");
27542
27602
  if (skillMd && pointerTargetOf(skillMd.data.toString("utf8")) !== null) {
27543
27603
  console.error(`${fetchId} downloaded as another pointer; stopping here (pointers are followed only one level, to avoid loops).`);
27544
27604
  return null;
27545
27605
  }
27546
27606
  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 });
27607
+ const path = target ?? join4(FETCH_ROOT, q.spaceId, q.name);
27608
+ const record = { id: a.id, digest: a.digest, handle: a.handle };
27609
+ if (target) {
27610
+ if (sameVersion(path, record)) return { found: { path, where: "fetched", reportId: a.id, digest: a.digest }, handle: a.handle };
27611
+ try {
27612
+ writeSkill(path, a.files, record);
27613
+ } catch (e) {
27614
+ if (!sameVersion(path, record)) throw e;
27615
+ }
27616
+ return { found: { path, where: "fetched", reportId: a.id, digest: a.digest }, handle: a.handle };
27617
+ }
27618
+ writeSkill(path, a.files, record);
27549
27619
  return { found: { path, where: "fetched", reportId: a.id, digest: a.digest }, handle: a.handle };
27550
27620
  } catch (e) {
27551
27621
  console.error(e instanceof ArchiveNotFound ? `${e.message}.` : String(e instanceof Error ? e.message : e));
@@ -27578,9 +27648,10 @@ async function cmdInit(name, opts) {
27578
27648
  console.log(`
27579
27649
  Next steps:`);
27580
27650
  console.log(` 1. Edit SKILL.md. Make the description say when to use this skill; agents decide by it, and search matches on it.`);
27581
- console.log(` 2. Try it: neta-search skills use ${slug} (drafts work directly and are not shared)`);
27582
- console.log(` 3. When it is ready: neta-search skills add ${slug} moves it to .agents/skills/; versions you use from there are shared with others`);
27583
- console.log(` (under your name; to opt out, add share: false to the frontmatter)`);
27651
+ console.log(` 2. Try it: neta-search skills use ${slug} (drafts work directly and are not sent to the hub)`);
27652
+ console.log(` 3. When it is ready: neta-search skills add ${slug} moves it to .agents/skills/`);
27653
+ console.log(` 4. Submit it: neta-search skills submit ${slug} sends this version to the hub and tells you whether it was collected.`);
27654
+ console.log(` Others can use it only after it passes review. To keep it to yourself, add share: false to the frontmatter.`);
27584
27655
  return 0;
27585
27656
  }
27586
27657
  function template(slug) {
@@ -27589,7 +27660,7 @@ name: ${slug}
27589
27660
  description: One sentence on when to use this skill (what a user would search for). Search matches on this field.
27590
27661
  metadata:
27591
27662
  neta-scope: universal # character|world|image|video|audio|text|universal
27592
- # neta-tags: [] # optional: your own tags (the server adds coarse tags too)
27663
+ # neta-tags: [] # optional: your own tags
27593
27664
  # disable-model-invocation: true # optional: hide from the agent's skill list; only runs when named explicitly or via use
27594
27665
  ---
27595
27666
 
@@ -27605,15 +27676,197 @@ metadata:
27605
27676
  `;
27606
27677
  }
27607
27678
 
27608
- // cli/src/skills/manage.ts
27609
- import { existsSync as existsSync5, readdirSync as readdirSync3, rmSync as rmSync2 } from "node:fs";
27679
+ // cli/src/skills/submit.ts
27680
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
27610
27681
  import { join as join6 } from "node:path";
27682
+ var SUBMISSION_PATH = "/v1/skills/submission";
27683
+ var MAX_FILES = 1e3;
27684
+ var MAX_FILE_BYTES = 20 << 20;
27685
+ var MAX_TOTAL_BYTES = 100 << 20;
27686
+ function precheck(dir) {
27687
+ const problems = [];
27688
+ const md = join6(dir, "SKILL.md");
27689
+ if (!existsSync5(md)) return ["there is no SKILL.md"];
27690
+ const raw = readFileSync5(md, "utf8");
27691
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
27692
+ if (!fm) problems.push("SKILL.md has no frontmatter (it must start with --- and have name / description)");
27693
+ else if (!/^description\s*:\s*\S/m.test(fm[1])) problems.push("the frontmatter has no description (agents decide when to use a skill by it)");
27694
+ if (sharedOptOut(dir)) problems.push("SKILL.md sets share: false; remove it to submit");
27695
+ const links = [];
27696
+ const walk = (rel) => {
27697
+ for (const ent of readdirSync3(join6(dir, rel), { withFileTypes: true })) {
27698
+ const childRel = rel ? `${rel}/${ent.name}` : ent.name;
27699
+ const st = lstatSync2(join6(dir, childRel));
27700
+ if (st.isSymbolicLink()) links.push(childRel);
27701
+ else if (st.isDirectory() && !skipSegment(ent.name, true)) walk(childRel);
27702
+ }
27703
+ };
27704
+ walk("");
27705
+ if (links.length) problems.push(`symbolic links are not accepted: ${links.slice(0, 5).join(", ")}${links.length > 5 ? ` (+${links.length - 5} more)` : ""}`);
27706
+ const files = readSkillFiles(dir);
27707
+ if (files.size > MAX_FILES) problems.push(`too many files: ${files.size} (at most ${MAX_FILES})`);
27708
+ let total = 0;
27709
+ for (const [p, b] of files) {
27710
+ total += b.length;
27711
+ if (b.length > MAX_FILE_BYTES) problems.push(`${p} is ${mb(b.length)} (at most ${mb(MAX_FILE_BYTES)} per file)`);
27712
+ }
27713
+ if (total > MAX_TOTAL_BYTES) problems.push(`the skill is ${mb(total)} in total (at most ${mb(MAX_TOTAL_BYTES)})`);
27714
+ return problems;
27715
+ }
27716
+ var mb = (n) => `${(n / (1 << 20)).toFixed(1)} MB`;
27717
+ function blockLabel(reason) {
27718
+ const head = reason.split(":")[0];
27719
+ switch (head) {
27720
+ case "draft":
27721
+ return "it is in the drafts directory";
27722
+ case "opt_out":
27723
+ return "SKILL.md sets share: false";
27724
+ case "pointer":
27725
+ return "SKILL.md is a hub pointer, not a skill";
27726
+ case "frontmatter":
27727
+ return `the frontmatter is incomplete (${reason.slice(head.length + 1).trim()})`;
27728
+ case "secret":
27729
+ return `a file looks like it contains a secret (${reason.slice(head.length + 1).trim()}); remove it and submit again`;
27730
+ default:
27731
+ return reason;
27732
+ }
27733
+ }
27734
+ async function fetchStatus(env, id, digest) {
27735
+ const url = `${env.apiBase}${SUBMISSION_PATH}?id=${encodeURIComponent(id)}&digest=${encodeURIComponent(digest)}`;
27736
+ const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
27737
+ if (!res.ok) return { http: res.status };
27738
+ return await res.json();
27739
+ }
27740
+ async function cmdSubmit(name, opts) {
27741
+ const env = detectEnv();
27742
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
27743
+ console.error(`Skill names are kebab-case directory names under ${SKILLS_DIR}/ (e.g. brawl-creator), got "${name}".`);
27744
+ return 2;
27745
+ }
27746
+ if (env.anonymous) {
27747
+ console.error("submit only works inside a Cohub sandbox: it submits a skill from the space you are in.");
27748
+ return 2;
27749
+ }
27750
+ const dir = join6(SKILLS_DIR, name);
27751
+ if (!existsSync5(join6(dir, "SKILL.md"))) {
27752
+ if (existsSync5(join6(DRAFTS_DIR, name, "SKILL.md"))) {
27753
+ console.error(`${name} is still a draft (${DRAFTS_DIR}/${name}/). Drafts are not submitted; run neta-search skills add ${name} to move it to ${SKILLS_DIR}/ first.`);
27754
+ } else {
27755
+ console.error(`There is no skill at ${dir}/ (it needs a SKILL.md).`);
27756
+ }
27757
+ return 1;
27758
+ }
27759
+ const src = readSource(dir);
27760
+ if (src) {
27761
+ console.error(`${dir}/ was installed from ${src.handle ?? src.id}; only skills written in this space can be submitted.`);
27762
+ return 1;
27763
+ }
27764
+ if (pointerTarget(dir)) {
27765
+ console.error(`${dir}/SKILL.md is a hub pointer, not a skill; there is nothing to submit.`);
27766
+ return 1;
27767
+ }
27768
+ const problems = precheck(dir);
27769
+ if (problems.length) {
27770
+ console.error(`Not submitted: ${dir}/ has problems the hub would reject:`);
27771
+ for (const p of problems) console.error(` - ${p}`);
27772
+ return 1;
27773
+ }
27774
+ const files = readSkillFiles(dir);
27775
+ const digest = digestOf(files);
27776
+ const id = ownId(env, name);
27777
+ const total = [...files.values()].reduce((a, b) => a + b.length, 0);
27778
+ if (!opts.json) console.error(`Checked: ${files.size} files, ${mb(total)}. Submitting ${digest} ...`);
27779
+ const sent = await reportSkillEventDetail(env, "use", id, digest, name, { via: "submit", where: "local" });
27780
+ if (sent === "unreachable") {
27781
+ console.error("Not submitted: could not reach the server. Try again in a moment.");
27782
+ return 1;
27783
+ }
27784
+ if (sent === "dropped") {
27785
+ console.error("Not submitted: the server received the request but did not record it (it may be over its daily limit or having trouble). Try again later.");
27786
+ return 1;
27787
+ }
27788
+ const deadline = Date.now() + (opts.wait ?? 60) * 1e3;
27789
+ const poll = opts.pollMs ?? 2e3;
27790
+ let s;
27791
+ let unknown = 0;
27792
+ let toldWaiting = false;
27793
+ for (; ; ) {
27794
+ try {
27795
+ s = await fetchStatus(env, id, digest);
27796
+ } catch {
27797
+ s = { http: 0 };
27798
+ }
27799
+ if ("http" in s) {
27800
+ if (s.http === 404 || s.http === 403) {
27801
+ return done(opts, { id, digest, state: "no_result" }, `Submitted ${digest}, but this server cannot report the result (older server, or not reachable from here). Whether it was collected is unknown.`, 4);
27802
+ }
27803
+ } else if (s.state === "unknown") {
27804
+ if (++unknown >= 3) {
27805
+ return done(
27806
+ opts,
27807
+ { id, digest, state: s.state },
27808
+ `Not collected: when the server read ${dir}/ it was no longer this version (was it edited right after submitting?). Finish editing, then run neta-search skills submit ${name} again.`,
27809
+ 1
27810
+ );
27811
+ }
27812
+ } else if (s.state !== "queued") {
27813
+ return finish(opts, id, digest, name, s);
27814
+ } else if (!toldWaiting && !opts.json) {
27815
+ console.error(`Waiting for the server to collect it${s.reason ? ` (last attempt failed: ${s.reason}; it retries)` : ""} ...`);
27816
+ toldWaiting = true;
27817
+ }
27818
+ if (Date.now() + poll > deadline) {
27819
+ return done(
27820
+ opts,
27821
+ { id, digest, state: "queued" },
27822
+ `Still waiting after ${opts.wait ?? 60}s: the server has not collected ${digest} yet. Run the same command again later; resubmitting the same version is safe.`,
27823
+ 3
27824
+ );
27825
+ }
27826
+ await new Promise((r) => setTimeout(r, poll));
27827
+ }
27828
+ }
27829
+ function finish(opts, id, digest, name, s) {
27830
+ const base = { id, digest, state: s.state, handle: s.handle || void 0, gated: s.gated, released: s.released, reason: s.reason || void 0, duplicate_of: s.duplicate_of || void 0 };
27831
+ switch (s.state) {
27832
+ case "rejected":
27833
+ return done(opts, base, `Not collected: ${s.reason ?? "the server rejected the directory"}. Fix it and submit again.`, 1);
27834
+ case "blocked":
27835
+ return done(opts, base, `Not collected: ${blockLabel(s.reason ?? "")}.`, 1);
27836
+ case "superseded":
27837
+ return done(opts, base, `A newer version of ${name} was submitted meanwhile; that one is being collected instead of ${digest}.`, 1);
27838
+ default: {
27839
+ const lines = [`Collected: ${s.handle || id} ${digest}`];
27840
+ if (s.duplicate_of) {
27841
+ lines.push(`Its content is identical to ${s.duplicate_of}, so the hub treats it as a copy of that skill; it will not be listed on its own.`);
27842
+ } else if (s.released) {
27843
+ lines.push("This version is already live.");
27844
+ } else if (s.gated) {
27845
+ lines.push("It is waiting for review: it goes live once the gate writes back pass for this version (publishing runs every 30 minutes). Until then the current live version, if any, stays.");
27846
+ } else if (s.trusted) {
27847
+ lines.push("It goes live at the next publish (every 30 minutes).");
27848
+ } else {
27849
+ lines.push("Skills from personal spaces are not offered to other people yet (that needs the review gate, which is not open yet). You can keep using it here.");
27850
+ }
27851
+ return done(opts, base, lines.join("\n"), 0);
27852
+ }
27853
+ }
27854
+ }
27855
+ function done(opts, json, text, code) {
27856
+ if (opts.json) console.log(JSON.stringify(json));
27857
+ else (code === 0 ? console.log : console.error)(text);
27858
+ return code;
27859
+ }
27860
+
27861
+ // cli/src/skills/manage.ts
27862
+ import { existsSync as existsSync6, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
27863
+ import { join as join7 } from "node:path";
27611
27864
  function scanLocal(dir) {
27612
- if (!existsSync5(dir)) return [];
27865
+ if (!existsSync6(dir)) return [];
27613
27866
  const out = [];
27614
- for (const e of readdirSync3(dir, { withFileTypes: true })) {
27867
+ for (const e of readdirSync4(dir, { withFileTypes: true })) {
27615
27868
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
27616
- const path = join6(dir, e.name);
27869
+ const path = join7(dir, e.name);
27617
27870
  if (!skillDirExists(path)) continue;
27618
27871
  const src = readSource(path);
27619
27872
  if (src) {
@@ -27746,7 +27999,7 @@ async function cmdRemove(names, opts) {
27746
27999
  const local = scanLocal(opts.dir);
27747
28000
  const results = [];
27748
28001
  for (const n of names) {
27749
- const found = (await findLocal(env, local, n)).filter((s) => existsSync5(s.path));
28002
+ const found = (await findLocal(env, local, n)).filter((s) => existsSync6(s.path));
27750
28003
  if (found.length === 0) results.push({ name: n, result: "not_found" });
27751
28004
  for (const s of found) {
27752
28005
  if (s.kind === "own" && !opts.force) {
@@ -27761,8 +28014,8 @@ async function cmdRemove(names, opts) {
27761
28014
  if (opts.json) console.log(JSON.stringify({ results }));
27762
28015
  else
27763
28016
  for (const r of results) {
27764
- if (r.result === "removed") console.log(`Removed ${join6(opts.dir, r.name)}`);
27765
- else if (r.result === "not_found") console.error(`${join6(opts.dir, r.name)} does not exist`);
28017
+ if (r.result === "removed") console.log(`Removed ${join7(opts.dir, r.name)}`);
28018
+ else if (r.result === "not_found") console.error(`${join7(opts.dir, r.name)} does not exist`);
27766
28019
  else console.error(`${r.name} is a skill you wrote yourself (no install source), so it was not removed. Add --force if you really want to delete it.`);
27767
28020
  }
27768
28021
  return failed > 0 ? 1 : 0;
@@ -27771,16 +28024,20 @@ async function cmdRemove(names, opts) {
27771
28024
  // cli/src/cli.ts
27772
28025
  var program2 = new Command();
27773
28026
  program2.name("neta-search").description("neta-search: search the Cohub skill ecosystem").version(cliVersion());
27774
- var skills = program2.command("skills").description("skills: find / add / use / list / update / remove / init");
28027
+ var skills = program2.command("skills").description("skills: find / add / use / list / update / remove / init / submit");
27775
28028
  skills.command("find").description("search the ecosystem for skills").argument("<query>", "what you want to do (natural language or keywords)").option("-n, --limit <n>", "max results", "20").option("--json", "compact JSON output (for agents)").action(async (query, opts) => {
27776
28029
  await run(cmdFind(query, { limit: Number(opts.limit) || 20, json: opts.json }));
27777
28030
  });
27778
28031
  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
28032
  await run(cmdAdd(idOrName, opts));
27780
28033
  });
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) => {
28034
+ 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
28035
  await run(cmdUse(idOrName, opts));
27783
28036
  });
28037
+ skills.command("submit").description("submit the current version of one of your skills (.agents/skills/<name>/ in this space) to the hub and wait for the result; it goes live for others only after review").argument("<name>", "skill directory name under .agents/skills/").option("--wait <seconds>", "how long to wait for the server's result", "60").option("--json", "compact JSON output (for agents)").action(async (name, opts) => {
28038
+ const wait = Number(opts.wait);
28039
+ await run(cmdSubmit(name, { json: !!opts.json, wait: Number.isFinite(wait) && wait >= 0 ? wait : 60 }));
28040
+ });
27784
28041
  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) => {
27785
28042
  await run(cmdList({ dir: opts.dir, offline: !!opts.offline, json: !!opts.json }));
27786
28043
  });
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.26",
4
4
  "description": "neta-search CLI — find, try and install skills from the Cohub / Neta skill registry",
5
5
  "type": "module",
6
6
  "bin": {