@neta-art/neta-search-cli 0.2.8 → 0.2.14
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 -1
- package/neta-search.mjs +124 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,13 +5,15 @@ Command-line client for **neta-search**, the skill registry of the Cohub / Neta
|
|
|
5
5
|
```bash
|
|
6
6
|
npx -y @neta-art/neta-search-cli@0.2 skills find "generate video" # search (Chinese or English)
|
|
7
7
|
npx -y @neta-art/neta-search-cli@0.2 skills find "browser automation" --json
|
|
8
|
-
npx -y @neta-art/neta-search-cli@0.2 skills use <id> #
|
|
8
|
+
npx -y @neta-art/neta-search-cli@0.2 skills use <id> # print the SKILL.md (pulled into a temp dir; no project files written)
|
|
9
9
|
npx -y @neta-art/neta-search-cli@0.2 skills add <id> # install into .agents/skills/
|
|
10
10
|
npx -y @neta-art/neta-search-cli@0.2 skills init <name> # scaffold your own skill
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
`<id>` comes from `find` results, e.g. `cohub:<spaceId>/<name>`.
|
|
14
14
|
|
|
15
|
+
`use` prints the skill directory and the full SKILL.md, so an agent can follow it right away; relative paths in SKILL.md resolve against that directory. If the local entry is a pointer (frontmatter `metadata.neta-skill-id`), `use` fetches the full skill it points to.
|
|
16
|
+
|
|
15
17
|
Inside a Cohub sandbox the CLI picks up the sandbox identity automatically; elsewhere it runs anonymously.
|
|
16
18
|
|
|
17
19
|
Single file, no runtime dependencies. Node.js 20+.
|
package/neta-search.mjs
CHANGED
|
@@ -3247,13 +3247,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3247
3247
|
contextOptions = void 0;
|
|
3248
3248
|
}
|
|
3249
3249
|
const outputContext = this._getOutputContext(contextOptions);
|
|
3250
|
-
const
|
|
3250
|
+
const eventContext2 = {
|
|
3251
3251
|
error: outputContext.error,
|
|
3252
3252
|
write: outputContext.write,
|
|
3253
3253
|
command: this
|
|
3254
3254
|
};
|
|
3255
|
-
this._getCommandAndAncestors().reverse().forEach((command) => command.emit("beforeAllHelp",
|
|
3256
|
-
this.emit("beforeHelp",
|
|
3255
|
+
this._getCommandAndAncestors().reverse().forEach((command) => command.emit("beforeAllHelp", eventContext2));
|
|
3256
|
+
this.emit("beforeHelp", eventContext2);
|
|
3257
3257
|
let helpInformation = this.helpInformation({ error: outputContext.error });
|
|
3258
3258
|
if (deprecatedCallback) {
|
|
3259
3259
|
helpInformation = deprecatedCallback(helpInformation);
|
|
@@ -3265,9 +3265,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3265
3265
|
if (this._getHelpOption()?.long) {
|
|
3266
3266
|
this.emit(this._getHelpOption().long);
|
|
3267
3267
|
}
|
|
3268
|
-
this.emit("afterHelp",
|
|
3268
|
+
this.emit("afterHelp", eventContext2);
|
|
3269
3269
|
this._getCommandAndAncestors().forEach(
|
|
3270
|
-
(command) => command.emit("afterAllHelp",
|
|
3270
|
+
(command) => command.emit("afterAllHelp", eventContext2)
|
|
3271
3271
|
);
|
|
3272
3272
|
}
|
|
3273
3273
|
/**
|
|
@@ -26912,10 +26912,18 @@ function detectEnv() {
|
|
|
26912
26912
|
const spaceId = process.env["COHUB_SPACE_ID"] ?? "";
|
|
26913
26913
|
const apiBase = process.env["NETASEARCH_API"] ?? // sandbox 内(平台注入 COHUB_*):默认 prod Service——真实用户的事件该写
|
|
26914
26914
|
// prod 账本;否则 prod sandbox 的事件会全部写进 dev 账本(review #76 抓过的
|
|
26915
|
-
// 隐患)。sandbox
|
|
26915
|
+
// 隐患)。sandbox 外:默认 prod 公网地址(2026-10-09 起;此前是 dev,公网 npm 版发布前改),
|
|
26916
|
+
// 本地验收用 NETASEARCH_API 指 dev。
|
|
26916
26917
|
// 不依赖平台注入 NETASEARCH_API——COHUB_SPACE_ID 就是平台已给的信号。
|
|
26917
|
-
(spaceId !== "" ? "http://neta-search-api.recsys.svc.cluster.local" : "https://
|
|
26918
|
-
return {
|
|
26918
|
+
(spaceId !== "" ? "http://neta-search-api.recsys.svc.cluster.local" : "https://search.neta.art");
|
|
26919
|
+
return {
|
|
26920
|
+
apiBase,
|
|
26921
|
+
actorId,
|
|
26922
|
+
spaceId,
|
|
26923
|
+
sessionId: process.env["COHUB_SESSION_ID"] ?? "",
|
|
26924
|
+
turnId: process.env["COHUB_TURN_ID"] ?? "",
|
|
26925
|
+
anonymous: actorId === "" || spaceId === ""
|
|
26926
|
+
};
|
|
26919
26927
|
}
|
|
26920
26928
|
function searchClient(env) {
|
|
26921
26929
|
const transport = createConnectTransport({ baseUrl: env.apiBase, httpVersion: "1.1" });
|
|
@@ -26925,12 +26933,27 @@ function feedbackClient(env) {
|
|
|
26925
26933
|
const transport = createConnectTransport({ baseUrl: env.apiBase, httpVersion: "1.1" });
|
|
26926
26934
|
return createClient(SkillFeedbackService, transport);
|
|
26927
26935
|
}
|
|
26928
|
-
|
|
26936
|
+
function eventContext(env, extra = {}) {
|
|
26937
|
+
const ctx = { source: "cli", ...extra };
|
|
26938
|
+
if (env.sessionId) ctx["session_id"] = env.sessionId;
|
|
26939
|
+
if (env.turnId) ctx["turn_id"] = env.turnId;
|
|
26940
|
+
return ctx;
|
|
26941
|
+
}
|
|
26942
|
+
async function reportSkillEvent(env, event, skillId, digest, skillName, extra = {}) {
|
|
26929
26943
|
if (env.anonymous) return false;
|
|
26930
26944
|
try {
|
|
26931
26945
|
await feedbackClient(env).reportSkillEvents({
|
|
26932
26946
|
events: [
|
|
26933
|
-
{
|
|
26947
|
+
{
|
|
26948
|
+
event,
|
|
26949
|
+
skillId,
|
|
26950
|
+
skillName,
|
|
26951
|
+
contentHash: digest,
|
|
26952
|
+
actorId: env.actorId,
|
|
26953
|
+
spaceId: env.spaceId,
|
|
26954
|
+
anonymous: false,
|
|
26955
|
+
context: eventContext(env, extra)
|
|
26956
|
+
}
|
|
26934
26957
|
]
|
|
26935
26958
|
});
|
|
26936
26959
|
return true;
|
|
@@ -27041,21 +27064,29 @@ var ArchiveNotFound = class extends Error {
|
|
|
27041
27064
|
};
|
|
27042
27065
|
async function fetchArchive(env, id) {
|
|
27043
27066
|
const url = `${env.apiBase}${ARCHIVE_PATH}?id=${encodeURIComponent(id)}`;
|
|
27067
|
+
const res = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
27068
|
+
if (res.status === 404) throw new ArchiveNotFound(`${id} \u4E0D\u5B58\u5728\uFF0C\u6216\u8FD8\u6CA1\u6709\u4E0A\u67B6\u7684\u7248\u672C`);
|
|
27069
|
+
const location = res.headers.get("location");
|
|
27070
|
+
if (res.status < 300 || res.status >= 400 || !location) throw new Error(`\u4E0B\u8F7D ${id} \u5931\u8D25\uFF1AHTTP ${res.status}`);
|
|
27071
|
+
const digest = res.headers.get("x-skill-digest") ?? "";
|
|
27072
|
+
const name = res.headers.get("x-skill-name") ?? id.split("/").pop();
|
|
27073
|
+
if (!digest) throw new Error(`\u4E0B\u8F7D ${id} \u5931\u8D25\uFF1A\u670D\u52A1\u7AEF\u6CA1\u6709\u7ED9\u51FA\u7248\u672C\uFF08x-skill-digest\uFF09`);
|
|
27074
|
+
const body = await download(new URL(location, url).toString(), id);
|
|
27075
|
+
const files = untarGz(body);
|
|
27076
|
+
const got = digestOf(new Map(files.map((f) => [f.path, f.data])));
|
|
27077
|
+
if (got !== digest) throw new Error(`\u4E0B\u8F7D\u7684 ${id} \u5185\u5BB9\u6821\u9A8C\u5931\u8D25\uFF08\u671F\u671B ${digest}\uFF0C\u5B9E\u9645 ${got}\uFF09`);
|
|
27078
|
+
return { id, name, digest, files };
|
|
27079
|
+
}
|
|
27080
|
+
async function download(url, id) {
|
|
27044
27081
|
for (let attempt = 1; ; attempt++) {
|
|
27045
27082
|
const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
27046
|
-
if (res.status ===
|
|
27047
|
-
|
|
27048
|
-
|
|
27049
|
-
await new Promise((r) => setTimeout(r, wait * 1e3));
|
|
27083
|
+
if (res.status === 202 && attempt < MAX_TRIES) {
|
|
27084
|
+
const body = await res.json().catch(() => ({}));
|
|
27085
|
+
await new Promise((r) => setTimeout(r, Math.min(Math.max(body.retryAfterMs ?? 2e3, 250), 5e3)));
|
|
27050
27086
|
continue;
|
|
27051
27087
|
}
|
|
27052
|
-
if (!res.ok) throw new Error(`\u4E0B\u8F7D ${id} \u5931\u8D25\
|
|
27053
|
-
|
|
27054
|
-
const name = res.headers.get("x-skill-name") ?? id.split("/").pop();
|
|
27055
|
-
const files = untarGz(Buffer.from(await res.arrayBuffer()));
|
|
27056
|
-
const got = digestOf(new Map(files.map((f) => [f.path, f.data])));
|
|
27057
|
-
if (!digest || got !== digest) throw new Error(`\u4E0B\u8F7D\u7684 ${id} \u5185\u5BB9\u6821\u9A8C\u5931\u8D25\uFF08\u671F\u671B ${digest}\uFF0C\u5B9E\u9645 ${got}\uFF09`);
|
|
27058
|
-
return { id, name, digest, files };
|
|
27088
|
+
if (!res.ok) throw new Error(`\u4E0B\u8F7D ${id} \u5931\u8D25\uFF1Aregistry \u8FD4\u56DE HTTP ${res.status}`);
|
|
27089
|
+
return Buffer.from(await res.arrayBuffer());
|
|
27059
27090
|
}
|
|
27060
27091
|
}
|
|
27061
27092
|
function untarGz(gz) {
|
|
@@ -27156,6 +27187,23 @@ function writeSkill(target, files, source) {
|
|
|
27156
27187
|
renameSync(staging, target);
|
|
27157
27188
|
if (hadOld) rmSync(trash, { recursive: true, force: true });
|
|
27158
27189
|
}
|
|
27190
|
+
var POINTER_RE = /^\s*neta-skill-id:\s*["']?(cohub:[^"'\s]+)["']?\s*$/m;
|
|
27191
|
+
function frontmatterOf(raw) {
|
|
27192
|
+
if (!raw.startsWith("---\n")) return "";
|
|
27193
|
+
const end = raw.indexOf("\n---", 4);
|
|
27194
|
+
return end < 0 ? "" : raw.slice(4, end);
|
|
27195
|
+
}
|
|
27196
|
+
function pointerTargetOf(raw) {
|
|
27197
|
+
const m = POINTER_RE.exec(frontmatterOf(raw));
|
|
27198
|
+
return m ? m[1] : null;
|
|
27199
|
+
}
|
|
27200
|
+
function pointerTarget(dir) {
|
|
27201
|
+
try {
|
|
27202
|
+
return pointerTargetOf(readFileSync2(join2(dir, "SKILL.md"), "utf8"));
|
|
27203
|
+
} catch {
|
|
27204
|
+
return null;
|
|
27205
|
+
}
|
|
27206
|
+
}
|
|
27159
27207
|
function sharedOptOut(dir) {
|
|
27160
27208
|
try {
|
|
27161
27209
|
const raw = readFileSync2(join2(dir, "SKILL.md"), "utf8");
|
|
@@ -27227,10 +27275,23 @@ async function graduate(env, name, target, opts) {
|
|
|
27227
27275
|
}
|
|
27228
27276
|
|
|
27229
27277
|
// cli/src/skills/use.ts
|
|
27230
|
-
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
27278
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
27279
|
+
import { randomUUID } from "node:crypto";
|
|
27231
27280
|
import { join as join4 } from "node:path";
|
|
27281
|
+
var FETCH_ROOT = process.env["NETASEARCH_FETCH_ROOT"] ?? join4("/tmp", "neta-skills");
|
|
27282
|
+
var HANDLE_RE = /^(@[a-z0-9_.-]{1,64}|[a-z0-9][a-z0-9-]{0,31})\/[a-z0-9][a-z0-9-]{0,63}$/i;
|
|
27283
|
+
async function resolveHandle(env, input) {
|
|
27284
|
+
const r = await fetch(`${env.apiBase}/v1/skills/resolve?handle=${encodeURIComponent(input)}`, { signal: AbortSignal.timeout(5e3) });
|
|
27285
|
+
if (!r.ok) return null;
|
|
27286
|
+
const out = await r.json();
|
|
27287
|
+
return typeof out.id === "string" && parseId(out.id) ? out.id : null;
|
|
27288
|
+
}
|
|
27232
27289
|
async function cmdUse(idOrName, opts) {
|
|
27233
27290
|
const env = detectEnv();
|
|
27291
|
+
if (!parseId(idOrName) && HANDLE_RE.test(idOrName)) {
|
|
27292
|
+
const resolved = await resolveHandle(env, idOrName).catch(() => null);
|
|
27293
|
+
if (resolved) idOrName = resolved;
|
|
27294
|
+
}
|
|
27234
27295
|
const parsed = parseId(idOrName);
|
|
27235
27296
|
const name = parsed?.name ?? idOrName;
|
|
27236
27297
|
const mine = !parsed || parsed.spaceId === env.spaceId;
|
|
@@ -27238,6 +27299,8 @@ async function cmdUse(idOrName, opts) {
|
|
|
27238
27299
|
let where = "local";
|
|
27239
27300
|
let reportId = null;
|
|
27240
27301
|
let notice = null;
|
|
27302
|
+
let fetchId = parsed && !mine ? idOrName : null;
|
|
27303
|
+
let via = opts.via ? opts.via.slice(0, 32) : null;
|
|
27241
27304
|
const installed = join4(SKILLS_DIR, name);
|
|
27242
27305
|
const draft = join4(DRAFTS_DIR, name);
|
|
27243
27306
|
if (skillDirExists(installed)) {
|
|
@@ -27263,11 +27326,32 @@ async function cmdUse(idOrName, opts) {
|
|
|
27263
27326
|
where = "mod";
|
|
27264
27327
|
}
|
|
27265
27328
|
}
|
|
27266
|
-
if (
|
|
27329
|
+
if (path) {
|
|
27330
|
+
const target = pointerTarget(path);
|
|
27331
|
+
if (target !== null) {
|
|
27332
|
+
if (!parseId(target)) {
|
|
27333
|
+
console.error(`${join4(path, "SKILL.md")} \u662F\u6307\u9488\uFF0C\u4F46\u5B83\u7684 neta-skill-id \u4E0D\u662F\u5408\u6CD5 id\uFF1A${target}`);
|
|
27334
|
+
return 1;
|
|
27335
|
+
}
|
|
27336
|
+
via = "pointer";
|
|
27337
|
+
fetchId = target;
|
|
27338
|
+
path = null;
|
|
27339
|
+
reportId = null;
|
|
27340
|
+
notice = null;
|
|
27341
|
+
}
|
|
27342
|
+
}
|
|
27343
|
+
if (!path && fetchId) {
|
|
27267
27344
|
try {
|
|
27268
|
-
const a = await fetchArchive(env,
|
|
27269
|
-
|
|
27270
|
-
if (
|
|
27345
|
+
const a = await fetchArchive(env, fetchId);
|
|
27346
|
+
const skillMd2 = a.files.find((f) => f.path === "SKILL.md");
|
|
27347
|
+
if (skillMd2 && pointerTargetOf(skillMd2.data.toString("utf8")) !== null) {
|
|
27348
|
+
console.error(`${fetchId} \u4E0B\u8F7D\u5230\u7684\u5185\u5BB9\u8FD8\u662F\u6307\u9488\uFF0C\u505C\u5728\u8FD9\u91CC\uFF08\u4E0D\u8DDF\u7B2C\u4E8C\u5C42\uFF0C\u9632\u6B62\u7ED5\u5708\uFF09\u3002`);
|
|
27349
|
+
return 1;
|
|
27350
|
+
}
|
|
27351
|
+
const p = parseId(a.id);
|
|
27352
|
+
path = join4(FETCH_ROOT, p.spaceId, p.name);
|
|
27353
|
+
const have = readSource(path);
|
|
27354
|
+
if (!skillDirExists(path) || have?.digest !== a.digest) writeSkill(path, a.files, { id: a.id, digest: a.digest });
|
|
27271
27355
|
reportId = a.id;
|
|
27272
27356
|
where = "fetched";
|
|
27273
27357
|
} catch (e) {
|
|
@@ -27279,14 +27363,21 @@ async function cmdUse(idOrName, opts) {
|
|
|
27279
27363
|
console.error(`skill "${idOrName}" \u672C\u5730\u6CA1\u6709\u2014\u2014\u5148 neta-search skills find "${name}" \u62FF\u5230 id\u3002`);
|
|
27280
27364
|
return 1;
|
|
27281
27365
|
}
|
|
27282
|
-
const
|
|
27283
|
-
|
|
27284
|
-
|
|
27366
|
+
const useId = randomUUID();
|
|
27367
|
+
const extra = { use_id: useId };
|
|
27368
|
+
if (via) extra["via"] = via;
|
|
27369
|
+
const reported = reportId ? await reportSkillEvent(env, "use", reportId, skillDirDigest(path), name, extra) : false;
|
|
27370
|
+
const skillMd = readFileSync3(join4(path, "SKILL.md"), "utf8");
|
|
27371
|
+
if (opts.json) {
|
|
27372
|
+
console.log(JSON.stringify({ skill: reportId ?? name, path, where, use_id: reportId ? useId : void 0, skill_md: skillMd }));
|
|
27373
|
+
} else {
|
|
27285
27374
|
const label = { local: "\u672C\u5730", mod: "\u5E73\u53F0 mod", fetched: "\u5DF2\u4E0B\u8F7D\u5230\u4E34\u65F6\u76EE\u5F55" }[where];
|
|
27286
|
-
console.log(`${name} \u5C31\u7EEA\uFF08${label}\uFF09
|
|
27287
|
-
console.log(`
|
|
27375
|
+
console.log(`${reportId ?? name} \u5C31\u7EEA\uFF08${label}${via === "pointer" ? "\uFF0C\u7ECF\u6307\u9488" : ""}\uFF09`);
|
|
27376
|
+
console.log(`skill \u76EE\u5F55\uFF1A${path}`);
|
|
27377
|
+
console.log(`SKILL.md \u91CC\u7684\u76F8\u5BF9\u8DEF\u5F84\u90FD\u4ECE\u8FD9\u4E2A\u76EE\u5F55\u7B97\u8D77\u3002\u4E0B\u9762\u5C31\u662F SKILL.md \u7684\u5168\u6587\uFF0C\u7167\u7740\u505A\u5373\u53EF\uFF08\u4E0D\u7528\u518D\u8BFB\u4E00\u6B21\u6587\u4EF6\uFF09\u3002`);
|
|
27288
27378
|
console.log(`
|
|
27289
|
-
|
|
27379
|
+
----- SKILL.md -----
|
|
27380
|
+
${skillMd}${skillMd.endsWith("\n") ? "" : "\n"}----- end -----`);
|
|
27290
27381
|
}
|
|
27291
27382
|
if (notice && reportId && !env.anonymous) shareNotice(reportId, path, notice === "draft", reported);
|
|
27292
27383
|
return 0;
|
|
@@ -27377,7 +27468,7 @@ skills.command("find").description("\u627E skill\uFF08\u751F\u6001\u8D26\u672C+\
|
|
|
27377
27468
|
skills.command("add").description("\u88C5 skill\uFF08find \u7ED3\u679C\u7684 id\uFF1A\u4E0B\u8F7D\u4F5C\u8005\u7528\u8FC7\u7684\u90A3\u4E00\u7248\uFF1B\u6216\u81EA\u5DF1\u8349\u7A3F\u533A\u7684\u540D\u5B57\uFF1A\u79FB\u5230 .agents/skills/\uFF09").argument("<id-or-name>", "find \u8FD4\u56DE\u7684 skill id\uFF0C\u6216\u81EA\u5DF1\u8349\u7A3F\u533A\u7684 skill \u540D").option("--dir <dir>", "\u5B89\u88C5\u76EE\u5F55\uFF08\u9ED8\u8BA4 .agents/skills/\uFF09", ".agents/skills").option("--json", "\u7D27\u51D1 JSON \u8F93\u51FA\uFF08agent \u6D88\u8D39\uFF09").action(async (idOrName, opts) => {
|
|
27378
27469
|
await run(cmdAdd(idOrName, opts));
|
|
27379
27470
|
});
|
|
27380
|
-
skills.command("use").description("\u7528 skill\uFF08\u4E0D\u88C5\uFF1A\u672C\u5730\u6709\u2192\
|
|
27471
|
+
skills.command("use").description("\u7528 skill\uFF08\u4E0D\u88C5\uFF1A\u6253\u5370 SKILL.md \u5168\u6587\u548C skill \u76EE\u5F55\uFF1B\u672C\u5730\u6709\u2192\u7528\u672C\u5730\u7684\uFF1B\u672C\u5730\u662F\u6307\u9488\u6216\u6CA1\u6709\u2192\u6309 id \u4E0B\u8F7D\u5230\u4E34\u65F6\u76EE\u5F55\uFF09").argument("<id-or-name>", "skill id \u6216 name").option("--json", "\u7D27\u51D1 JSON \u8F93\u51FA\uFF08agent \u6D88\u8D39\uFF09").option("--via <channel>", "\u4ECE\u54EA\u4E2A\u6E20\u9053\u6765\u7684\uFF08\u8BB0\u8FDB\u4F7F\u7528\u8BB0\u5F55\uFF1Bhub mod \u7684\u6307\u9488\u5199 pointer\uFF09").action(async (idOrName, opts) => {
|
|
27381
27472
|
await run(cmdUse(idOrName, opts));
|
|
27382
27473
|
});
|
|
27383
27474
|
skills.command("init").description("\u521B\u5EFA skill \u811A\u624B\u67B6\uFF08SKILL.md + frontmatter \u89C4\u8303\uFF09").argument("[name]", "skill \u540D\uFF08kebab-case\uFF09").option("--here", "\u5728\u5F53\u524D\u76EE\u5F55\u751F\u6210\uFF08\u9ED8\u8BA4\u751F\u6210\u5230 ./<name>/ \u5B50\u76EE\u5F55\uFF09").action(async (name, opts) => {
|