@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 +3 -0
- package/neta-search.mjs +309 -52
- package/package.json +1 -1
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
27088
|
-
|
|
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
|
-
|
|
27121
|
-
|
|
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}:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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}
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
|
27582
|
-
console.log(` 3. When it is ready: neta-search skills add ${slug} moves it to .agents/skills
|
|
27583
|
-
console.log(`
|
|
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
|
|
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/
|
|
27609
|
-
import { existsSync as existsSync5, readdirSync as readdirSync3,
|
|
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 (!
|
|
27865
|
+
if (!existsSync6(dir)) return [];
|
|
27613
27866
|
const out = [];
|
|
27614
|
-
for (const e of
|
|
27867
|
+
for (const e of readdirSync4(dir, { withFileTypes: true })) {
|
|
27615
27868
|
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
27616
|
-
const path =
|
|
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) =>
|
|
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 ${
|
|
27765
|
-
else if (r.result === "not_found") console.error(`${
|
|
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
|
});
|