@neta-art/neta-search-cli 0.2.23 → 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
 
@@ -27292,14 +27295,15 @@ function sharedOptOut(dir) {
27292
27295
  }
27293
27296
  }
27294
27297
  function shareNotice(id, dir, draft, reported) {
27298
+ const name = id.split("/").pop();
27295
27299
  if (draft) {
27296
- 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}.`);
27297
27301
  } else if (sharedOptOut(dir)) {
27298
- 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.`);
27299
27303
  } else if (!reported) {
27300
- 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.`);
27301
27305
  } else {
27302
- 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.`);
27303
27307
  }
27304
27308
  }
27305
27309
 
@@ -27644,9 +27648,10 @@ async function cmdInit(name, opts) {
27644
27648
  console.log(`
27645
27649
  Next steps:`);
27646
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.`);
27647
- console.log(` 2. Try it: neta-search skills use ${slug} (drafts work directly and are not shared)`);
27648
- 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`);
27649
- 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.`);
27650
27655
  return 0;
27651
27656
  }
27652
27657
  function template(slug) {
@@ -27655,7 +27660,7 @@ name: ${slug}
27655
27660
  description: One sentence on when to use this skill (what a user would search for). Search matches on this field.
27656
27661
  metadata:
27657
27662
  neta-scope: universal # character|world|image|video|audio|text|universal
27658
- # neta-tags: [] # optional: your own tags (the server adds coarse tags too)
27663
+ # neta-tags: [] # optional: your own tags
27659
27664
  # disable-model-invocation: true # optional: hide from the agent's skill list; only runs when named explicitly or via use
27660
27665
  ---
27661
27666
 
@@ -27671,15 +27676,197 @@ metadata:
27671
27676
  `;
27672
27677
  }
27673
27678
 
27674
- // cli/src/skills/manage.ts
27675
- 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";
27676
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";
27677
27864
  function scanLocal(dir) {
27678
- if (!existsSync5(dir)) return [];
27865
+ if (!existsSync6(dir)) return [];
27679
27866
  const out = [];
27680
- for (const e of readdirSync3(dir, { withFileTypes: true })) {
27867
+ for (const e of readdirSync4(dir, { withFileTypes: true })) {
27681
27868
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
27682
- const path = join6(dir, e.name);
27869
+ const path = join7(dir, e.name);
27683
27870
  if (!skillDirExists(path)) continue;
27684
27871
  const src = readSource(path);
27685
27872
  if (src) {
@@ -27812,7 +27999,7 @@ async function cmdRemove(names, opts) {
27812
27999
  const local = scanLocal(opts.dir);
27813
28000
  const results = [];
27814
28001
  for (const n of names) {
27815
- 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));
27816
28003
  if (found.length === 0) results.push({ name: n, result: "not_found" });
27817
28004
  for (const s of found) {
27818
28005
  if (s.kind === "own" && !opts.force) {
@@ -27827,8 +28014,8 @@ async function cmdRemove(names, opts) {
27827
28014
  if (opts.json) console.log(JSON.stringify({ results }));
27828
28015
  else
27829
28016
  for (const r of results) {
27830
- if (r.result === "removed") console.log(`Removed ${join6(opts.dir, r.name)}`);
27831
- 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`);
27832
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.`);
27833
28020
  }
27834
28021
  return failed > 0 ? 1 : 0;
@@ -27837,7 +28024,7 @@ async function cmdRemove(names, opts) {
27837
28024
  // cli/src/cli.ts
27838
28025
  var program2 = new Command();
27839
28026
  program2.name("neta-search").description("neta-search: search the Cohub skill ecosystem").version(cliVersion());
27840
- 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");
27841
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) => {
27842
28029
  await run(cmdFind(query, { limit: Number(opts.limit) || 20, json: opts.json }));
27843
28030
  });
@@ -27847,6 +28034,10 @@ skills.command("add").description("install a skill: an id or handle from find do
27847
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) => {
27848
28035
  await run(cmdUse(idOrName, opts));
27849
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
+ });
27850
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) => {
27851
28042
  await run(cmdList({ dir: opts.dir, offline: !!opts.offline, json: !!opts.json }));
27852
28043
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/neta-search-cli",
3
- "version": "0.2.23",
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": {